eslint-plugin-regexp
Version:
ESLint plugin for finding RegExp mistakes and RegExp style guide violations.
496 lines (495 loc) • 21.8 kB
JavaScript
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,
});
},
});
;