495 lines
21 KiB
JavaScript
495 lines
21 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
|
|
const utils_1 = require("../utils");
|
|
const ast_utils_1 = require("../utils/ast-utils");
|
|
const mention_1 = require("../utils/mention");
|
|
const regexp_ast_1 = require("../utils/regexp-ast");
|
|
const type_tracker_1 = require("../utils/type-tracker");
|
|
class ReplaceReferencesList {
|
|
constructor(list) {
|
|
var _a, _b;
|
|
this.list = list;
|
|
this.startRefName = (_a = list[0].startRef) === null || _a === void 0 ? void 0 : _a.ref;
|
|
this.endRefName = (_b = list[0].endRef) === null || _b === void 0 ? void 0 : _b.ref;
|
|
const otherThanStartRefNames = new Set();
|
|
const otherThanEndRefNames = new Set();
|
|
for (const { startRef, endRef, allRefs } of this.list) {
|
|
for (const ref of allRefs) {
|
|
if (ref !== startRef) {
|
|
otherThanStartRefNames.add(ref.ref);
|
|
}
|
|
if (ref !== endRef) {
|
|
otherThanEndRefNames.add(ref.ref);
|
|
}
|
|
}
|
|
}
|
|
this.otherThanStartRefNames = otherThanStartRefNames;
|
|
this.otherThanEndRefNames = otherThanEndRefNames;
|
|
}
|
|
*[Symbol.iterator]() {
|
|
yield* this.list;
|
|
}
|
|
}
|
|
function getSideEffectsWhenReplacingCapturingGroup(elements, start, end, { flags }) {
|
|
const result = new Set();
|
|
if (start) {
|
|
const { chars } = (0, regexp_ast_analysis_1.getConsumedChars)(start, flags);
|
|
if (!hasDisjoint(chars, elements.slice(1))) {
|
|
result.add(0);
|
|
}
|
|
else {
|
|
const last = elements[elements.length - 1];
|
|
const lastChar = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_1.getFirstConsumedCharPlusAfter)(last, "rtl", flags));
|
|
if (!lastChar.char.isDisjointWith(chars)) {
|
|
result.add(0);
|
|
}
|
|
}
|
|
}
|
|
if (end && flags.global) {
|
|
const first = elements[0];
|
|
if (first) {
|
|
const { chars } = (0, regexp_ast_analysis_1.getConsumedChars)(end, flags);
|
|
const firstChar = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_1.getFirstConsumedCharPlusAfter)(first, "ltr", flags));
|
|
if (!firstChar.char.isDisjointWith(chars)) {
|
|
result.add(1);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
function hasDisjoint(target, targetElements) {
|
|
for (const element of targetElements) {
|
|
if (isConstantLength(element)) {
|
|
const elementChars = (0, regexp_ast_analysis_1.getConsumedChars)(element, flags);
|
|
if (elementChars.chars.isEmpty) {
|
|
continue;
|
|
}
|
|
if (elementChars.chars.isDisjointWith(target)) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
const elementLook = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_1.getFirstConsumedCharPlusAfter)(element, "ltr", flags));
|
|
return elementLook.char.isDisjointWith(target);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isConstantLength(target) {
|
|
const range = (0, regexp_ast_analysis_1.getLengthRange)(target, flags);
|
|
return range.min === range.max;
|
|
}
|
|
}
|
|
function isCapturingGroupAndNotZeroLength(element, flags) {
|
|
return element.type === "CapturingGroup" && !(0, regexp_ast_analysis_1.isZeroLength)(element, flags);
|
|
}
|
|
function parsePatternElements(node, flags) {
|
|
if (node.alternatives.length > 1) {
|
|
return null;
|
|
}
|
|
const elements = node.alternatives[0].elements;
|
|
const leadingElements = [];
|
|
let start = null;
|
|
for (const element of elements) {
|
|
if ((0, regexp_ast_analysis_1.isZeroLength)(element, flags)) {
|
|
leadingElements.push(element);
|
|
continue;
|
|
}
|
|
if (isCapturingGroupAndNotZeroLength(element, flags)) {
|
|
const capturingGroup = element;
|
|
start = {
|
|
leadingElements,
|
|
capturingGroup,
|
|
replacedAssertion: startElementsToLookbehindAssertionText(leadingElements, capturingGroup),
|
|
range: {
|
|
start: (leadingElements[0] || capturingGroup).start,
|
|
end: capturingGroup.end,
|
|
},
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
let end = null;
|
|
const trailingElements = [];
|
|
for (const element of [...elements].reverse()) {
|
|
if ((0, regexp_ast_analysis_1.isZeroLength)(element, flags)) {
|
|
trailingElements.unshift(element);
|
|
continue;
|
|
}
|
|
if (isCapturingGroupAndNotZeroLength(element, flags)) {
|
|
const capturingGroup = element;
|
|
end = {
|
|
capturingGroup,
|
|
trailingElements,
|
|
replacedAssertion: endElementsToLookaheadAssertionText(capturingGroup, trailingElements),
|
|
range: {
|
|
start: capturingGroup.start,
|
|
end: (trailingElements[trailingElements.length - 1] ||
|
|
capturingGroup).end,
|
|
},
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
if (!start && !end) {
|
|
return null;
|
|
}
|
|
if (start && end && start.capturingGroup === end.capturingGroup) {
|
|
return null;
|
|
}
|
|
return {
|
|
elements,
|
|
start,
|
|
end,
|
|
};
|
|
}
|
|
function endElementsToLookaheadAssertionText(capturingGroup, trailingElements) {
|
|
const groupPattern = capturingGroup.alternatives.map((a) => a.raw).join("|");
|
|
const trailing = leadingTrailingElementsToLookaroundAssertionPatternText(trailingElements, "lookahead");
|
|
if (trailing && capturingGroup.alternatives.length !== 1) {
|
|
return `(?=(?:${groupPattern})${trailing})`;
|
|
}
|
|
return `(?=${groupPattern}${trailing})`;
|
|
}
|
|
function startElementsToLookbehindAssertionText(leadingElements, capturingGroup) {
|
|
const leading = leadingTrailingElementsToLookaroundAssertionPatternText(leadingElements, "lookbehind");
|
|
const groupPattern = capturingGroup.alternatives.map((a) => a.raw).join("|");
|
|
if (leading && capturingGroup.alternatives.length !== 1) {
|
|
return `(?<=${leading}(?:${groupPattern}))`;
|
|
}
|
|
return `(?<=${leading}${groupPattern})`;
|
|
}
|
|
function leadingTrailingElementsToLookaroundAssertionPatternText(leadingTrailingElements, lookaroundAssertionKind) {
|
|
if (leadingTrailingElements.length === 1 &&
|
|
leadingTrailingElements[0].type === "Assertion") {
|
|
const assertion = leadingTrailingElements[0];
|
|
if (assertion.kind === lookaroundAssertionKind &&
|
|
!assertion.negate &&
|
|
assertion.alternatives.length === 1) {
|
|
return assertion.alternatives[0].raw;
|
|
}
|
|
}
|
|
return leadingTrailingElements.map((e) => e.raw).join("");
|
|
}
|
|
function parseOption(userOption) {
|
|
var _a, _b;
|
|
return {
|
|
lookbehind: (_a = userOption === null || userOption === void 0 ? void 0 : userOption.lookbehind) !== null && _a !== void 0 ? _a : true,
|
|
strictTypes: (_b = userOption === null || userOption === void 0 ? void 0 : userOption.strictTypes) !== null && _b !== void 0 ? _b : true,
|
|
};
|
|
}
|
|
exports.default = (0, utils_1.createRule)("prefer-lookaround", {
|
|
meta: {
|
|
docs: {
|
|
description: "prefer lookarounds over capturing group that do not replace",
|
|
category: "Stylistic Issues",
|
|
recommended: false,
|
|
},
|
|
fixable: "code",
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
lookbehind: { type: "boolean" },
|
|
strictTypes: { type: "boolean" },
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
messages: {
|
|
preferLookarounds: "These capturing groups can be replaced with lookaround assertions ({{expr1}} and {{expr2}}).",
|
|
prefer: "This capturing group can be replaced with a {{kind}} ({{expr}}).",
|
|
},
|
|
type: "suggestion",
|
|
},
|
|
create(context) {
|
|
const { lookbehind, strictTypes } = parseOption(context.options[0]);
|
|
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
|
|
function createVisitor(regexpContext) {
|
|
const { regexpNode, flags, patternAst } = regexpContext;
|
|
const parsedElements = parsePatternElements(patternAst, flags);
|
|
if (!parsedElements) {
|
|
return {};
|
|
}
|
|
const replaceReferenceList = [];
|
|
for (const ref of (0, ast_utils_1.extractExpressionReferences)(regexpNode, context)) {
|
|
if (ref.type === "argument") {
|
|
if (!(0, ast_utils_1.isKnownMethodCall)(ref.callExpression, {
|
|
replace: 2,
|
|
replaceAll: 2,
|
|
})) {
|
|
return {};
|
|
}
|
|
const replaceReference = getReplaceReferenceFromCallExpression(ref.callExpression);
|
|
if (!replaceReference) {
|
|
return {};
|
|
}
|
|
replaceReferenceList.push(replaceReference);
|
|
}
|
|
else if (ref.type === "member") {
|
|
const parent = (0, ast_utils_1.getParent)(ref.memberExpression);
|
|
if ((parent === null || parent === void 0 ? void 0 : parent.type) === "CallExpression" &&
|
|
(0, ast_utils_1.isKnownMethodCall)(parent, {
|
|
test: 1,
|
|
}) &&
|
|
!regexpContext.flags.global) {
|
|
continue;
|
|
}
|
|
return {};
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
}
|
|
if (!replaceReferenceList.length) {
|
|
return {};
|
|
}
|
|
const replaceReference = replaceReferenceList[0];
|
|
if (replaceReferenceList.some((target) => {
|
|
var _a, _b, _c, _d;
|
|
return ((_a = target.startRef) === null || _a === void 0 ? void 0 : _a.ref) !==
|
|
((_b = replaceReference.startRef) === null || _b === void 0 ? void 0 : _b.ref) ||
|
|
((_c = target.endRef) === null || _c === void 0 ? void 0 : _c.ref) !== ((_d = replaceReference.endRef) === null || _d === void 0 ? void 0 : _d.ref);
|
|
})) {
|
|
return {};
|
|
}
|
|
return createVerifyVisitor(regexpContext, parsedElements, new ReplaceReferencesList(replaceReferenceList));
|
|
}
|
|
function getReplaceReferenceFromCallExpression(node) {
|
|
if (strictTypes
|
|
? !typeTracer.isString(node.callee.object)
|
|
: !typeTracer.maybeString(node.callee.object)) {
|
|
return null;
|
|
}
|
|
const replacementNode = node.arguments[1];
|
|
if (replacementNode.type === "Literal") {
|
|
return getReplaceReferenceFromLiteralReplacementArgument(replacementNode);
|
|
}
|
|
return getReplaceReferenceFromNonLiteralReplacementArgument(replacementNode);
|
|
}
|
|
function getReplaceReferenceFromLiteralReplacementArgument(node) {
|
|
if (typeof node.value !== "string") {
|
|
return null;
|
|
}
|
|
const replacements = (0, ast_utils_1.parseReplacements)(context, node);
|
|
let startRef = null;
|
|
let endRef = null;
|
|
const start = replacements[0];
|
|
if ((start === null || start === void 0 ? void 0 : start.type) === "ReferenceElement") {
|
|
startRef = start;
|
|
}
|
|
const end = replacements[replacements.length - 1];
|
|
if ((end === null || end === void 0 ? void 0 : end.type) === "ReferenceElement") {
|
|
endRef = end;
|
|
}
|
|
if (!startRef && !endRef) {
|
|
return null;
|
|
}
|
|
return {
|
|
startRef,
|
|
endRef,
|
|
allRefs: replacements.filter((e) => e.type === "ReferenceElement"),
|
|
};
|
|
}
|
|
function getReplaceReferenceFromNonLiteralReplacementArgument(node) {
|
|
const evaluated = (0, ast_utils_1.getStaticValue)(context, node);
|
|
if (!evaluated || typeof evaluated.value !== "string") {
|
|
return null;
|
|
}
|
|
const refRegex = /\$(?<ref>[1-9]\d*|<(?<named>[^>]+)>)/gu;
|
|
const allRefs = [];
|
|
let startRef = null;
|
|
let endRef = null;
|
|
let re;
|
|
while ((re = refRegex.exec(evaluated.value))) {
|
|
const ref = {
|
|
ref: re.groups.named
|
|
? re.groups.named
|
|
: Number(re.groups.ref),
|
|
};
|
|
if (re.index === 0) {
|
|
startRef = ref;
|
|
}
|
|
if (refRegex.lastIndex === evaluated.value.length) {
|
|
endRef = ref;
|
|
}
|
|
allRefs.push(ref);
|
|
}
|
|
if (!startRef && !endRef) {
|
|
return null;
|
|
}
|
|
return {
|
|
startRef,
|
|
endRef,
|
|
allRefs,
|
|
};
|
|
}
|
|
function createVerifyVisitor(regexpContext, parsedElements, replaceReferenceList) {
|
|
const startRefState = {
|
|
capturingGroups: [],
|
|
capturingNum: -1,
|
|
};
|
|
const endRefState = {
|
|
capturingGroups: [],
|
|
capturingNum: -1,
|
|
};
|
|
let refNum = 0;
|
|
return {
|
|
onCapturingGroupEnter(cgNode) {
|
|
refNum++;
|
|
processForState(replaceReferenceList.startRefName, replaceReferenceList.otherThanStartRefNames, startRefState);
|
|
processForState(replaceReferenceList.endRefName, replaceReferenceList.otherThanEndRefNames, endRefState);
|
|
function processForState(refName, otherThanRefNames, state) {
|
|
if (refName === refNum || refName === cgNode.name) {
|
|
state.capturingGroups.push(cgNode);
|
|
state.capturingNum = refNum;
|
|
state.isUseOther || (state.isUseOther = Boolean(otherThanRefNames.has(refNum) ||
|
|
(cgNode.name &&
|
|
otherThanRefNames.has(cgNode.name))));
|
|
}
|
|
}
|
|
},
|
|
onPatternLeave() {
|
|
var _a, _b;
|
|
let reportStart = null;
|
|
if (!startRefState.isUseOther &&
|
|
startRefState.capturingGroups.length === 1 &&
|
|
startRefState.capturingGroups[0] ===
|
|
((_a = parsedElements.start) === null || _a === void 0 ? void 0 : _a.capturingGroup)) {
|
|
reportStart = parsedElements.start;
|
|
}
|
|
let reportEnd = null;
|
|
if (!endRefState.isUseOther &&
|
|
endRefState.capturingGroups.length === 1 &&
|
|
endRefState.capturingGroups[0] ===
|
|
((_b = parsedElements.end) === null || _b === void 0 ? void 0 : _b.capturingGroup)) {
|
|
reportEnd = parsedElements.end;
|
|
}
|
|
const sideEffects = getSideEffectsWhenReplacingCapturingGroup(parsedElements.elements, reportStart === null || reportStart === void 0 ? void 0 : reportStart.capturingGroup, reportEnd === null || reportEnd === void 0 ? void 0 : reportEnd.capturingGroup, regexpContext);
|
|
if (sideEffects.has(0)) {
|
|
reportStart = null;
|
|
}
|
|
if (sideEffects.has(1)) {
|
|
reportEnd = null;
|
|
}
|
|
if (!lookbehind) {
|
|
reportStart = null;
|
|
}
|
|
if (reportStart && reportEnd) {
|
|
const fix = buildFixer(regexpContext, [reportStart, reportEnd], replaceReferenceList, (target) => {
|
|
var _a, _b;
|
|
if (target.allRefs.some((ref) => ref !== target.startRef &&
|
|
ref !== target.endRef)) {
|
|
return null;
|
|
}
|
|
return [
|
|
(_a = target.startRef) === null || _a === void 0 ? void 0 : _a.range,
|
|
(_b = target.endRef) === null || _b === void 0 ? void 0 : _b.range,
|
|
];
|
|
});
|
|
for (const report of [reportStart, reportEnd]) {
|
|
context.report({
|
|
loc: regexpContext.getRegexpLocation(report.range),
|
|
messageId: "preferLookarounds",
|
|
data: {
|
|
expr1: (0, mention_1.mention)(reportStart.replacedAssertion),
|
|
expr2: (0, mention_1.mention)(reportEnd.replacedAssertion),
|
|
},
|
|
fix,
|
|
});
|
|
}
|
|
}
|
|
else if (reportStart) {
|
|
const fix = buildFixer(regexpContext, [reportStart], replaceReferenceList, (target) => {
|
|
var _a;
|
|
if (target.allRefs.some((ref) => ref !== target.startRef)) {
|
|
return null;
|
|
}
|
|
return [(_a = target.startRef) === null || _a === void 0 ? void 0 : _a.range];
|
|
});
|
|
context.report({
|
|
loc: regexpContext.getRegexpLocation(reportStart.range),
|
|
messageId: "prefer",
|
|
data: {
|
|
kind: "lookbehind assertion",
|
|
expr: (0, mention_1.mention)(reportStart.replacedAssertion),
|
|
},
|
|
fix,
|
|
});
|
|
}
|
|
else if (reportEnd) {
|
|
const fix = buildFixer(regexpContext, [reportEnd], replaceReferenceList, (target) => {
|
|
var _a;
|
|
if (target.allRefs.some((ref) => {
|
|
if (ref === target.endRef ||
|
|
typeof ref.ref !== "number") {
|
|
return false;
|
|
}
|
|
return (endRefState.capturingNum <= ref.ref);
|
|
})) {
|
|
return null;
|
|
}
|
|
return [(_a = target.endRef) === null || _a === void 0 ? void 0 : _a.range];
|
|
});
|
|
context.report({
|
|
loc: regexpContext.getRegexpLocation(reportEnd.range),
|
|
messageId: "prefer",
|
|
data: {
|
|
kind: "lookahead assertion",
|
|
expr: (0, mention_1.mention)(reportEnd.replacedAssertion),
|
|
},
|
|
fix,
|
|
});
|
|
}
|
|
},
|
|
};
|
|
}
|
|
function buildFixer(regexpContext, replaceCapturingGroups, replaceReferenceList, getRemoveRanges) {
|
|
const removeRanges = [];
|
|
for (const replaceReference of replaceReferenceList) {
|
|
const targetRemoveRanges = getRemoveRanges(replaceReference);
|
|
if (!targetRemoveRanges) {
|
|
return null;
|
|
}
|
|
for (const range of targetRemoveRanges) {
|
|
if (!range) {
|
|
return null;
|
|
}
|
|
removeRanges.push(range);
|
|
}
|
|
}
|
|
const replaces = [];
|
|
for (const { range, replacedAssertion } of replaceCapturingGroups) {
|
|
const replaceRange = regexpContext.patternSource.getReplaceRange(range);
|
|
if (!replaceRange) {
|
|
return null;
|
|
}
|
|
replaces.push({
|
|
replaceRange,
|
|
replacedAssertion,
|
|
});
|
|
}
|
|
return (fixer) => {
|
|
const list = [];
|
|
for (const removeRange of removeRanges) {
|
|
list.push({
|
|
offset: removeRange[0],
|
|
fix: () => fixer.removeRange(removeRange),
|
|
});
|
|
}
|
|
for (const { replaceRange, replacedAssertion } of replaces) {
|
|
list.push({
|
|
offset: replaceRange.range[0],
|
|
fix: () => replaceRange.replace(fixer, replacedAssertion),
|
|
});
|
|
}
|
|
return list
|
|
.sort((a, b) => a.offset - b.offset)
|
|
.map((item) => item.fix());
|
|
};
|
|
}
|
|
return (0, utils_1.defineRegexpVisitor)(context, {
|
|
createVisitor,
|
|
});
|
|
},
|
|
});
|