UNPKG

eslint-plugin-regexp

Version:

ESLint plugin for finding RegExp mistakes and RegExp style guide violations.

376 lines (375 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const regexp_ast_analysis_1 = require("regexp-ast-analysis"); const utils_1 = require("../utils"); const mention_1 = require("../utils/mention"); const util_1 = require("../utils/util"); function containsAssertion(n) { return (0, regexp_ast_analysis_1.hasSomeDescendant)(n, (d) => d.type === "Assertion"); } function isSingleCharacterAssertion(assertion, direction, flags) { switch (assertion.kind) { case "word": return false; case "start": return direction === "rtl"; case "end": return direction === "ltr"; default: break; } if ((0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind) !== direction) { return false; } return assertion.alternatives.every((alt) => { if (!containsAssertion(alt)) { const range = (0, regexp_ast_analysis_1.getLengthRange)(alt, flags); return range.min === 1 && range.max === 1; } let consumed = false; let asserted = false; const elements = direction === "ltr" ? alt.elements : [...alt.elements].reverse(); for (const e of elements) { if (!consumed) { if (e.type === "Assertion" && isSingleCharacterAssertion(e, direction, flags)) { asserted = true; continue; } if (containsAssertion(e)) { return false; } const range = (0, regexp_ast_analysis_1.getLengthRange)(e, flags); if (range.max === 0) { continue; } else if (range.min === 1 && range.max === 1) { consumed = true; } else { return false; } } else { const otherDir = (0, regexp_ast_analysis_1.invertMatchingDirection)(direction); if (e.type === "Assertion" && isSingleCharacterAssertion(e, otherDir, flags)) { continue; } return false; } } return consumed || asserted; }); } function firstLookCharsIntersection(a, b) { const char = a.char.intersect(b.char); return { char: a.char.intersect(b.char), exact: (a.exact && b.exact) || char.isEmpty, edge: a.edge && b.edge, }; } function createReorderingGetFirstCharAfter(forbidden) { function hasForbidden(element) { if (element.type === "Assertion" && forbidden.has(element)) { return true; } for (const f of forbidden) { if ((0, regexp_ast_analysis_1.hasSomeDescendant)(element, f)) { return true; } } return false; } return (afterThis, direction, flags) => { let result = (0, regexp_ast_analysis_1.getFirstCharAfter)(afterThis, direction, flags); if (afterThis.parent.type === "Alternative") { const { elements } = afterThis.parent; const inc = direction === "ltr" ? -1 : +1; const start = elements.indexOf(afterThis); for (let i = start + inc; i >= 0 && i < elements.length; i += inc) { const other = elements[i]; if (!(0, regexp_ast_analysis_1.isZeroLength)(other, flags)) { break; } if (hasForbidden(other)) { break; } const otherResult = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(other, direction, flags)); result = firstLookCharsIntersection(result, otherResult); } } return result; }; } function removeAlternative(alternative) { const parent = alternative.parent; if (parent.alternatives.length > 1) { let { start, end } = alternative; if (parent.alternatives[0] === alternative) { end++; } else { start--; } const before = parent.raw.slice(0, start - parent.start); const after = parent.raw.slice(end - parent.start); return [parent, before + after]; } switch (parent.type) { case "Pattern": return [parent, "[]"]; case "Assertion": { const assertionParent = parent.parent; if (parent.negate) { return [ assertionParent.type === "Quantifier" ? assertionParent : parent, "", ]; } if (assertionParent.type === "Quantifier") { if (assertionParent.min === 0) { return [assertionParent, ""]; } return removeAlternative(assertionParent.parent); } return removeAlternative(assertionParent); } case "CapturingGroup": { const before = parent.raw.slice(0, alternative.start - parent.start); const after = parent.raw.slice(alternative.end - parent.start); return [parent, `${before}[]${after}`]; } case "Group": { const groupParent = parent.parent; if (groupParent.type === "Quantifier") { if (groupParent.min === 0) { return [groupParent, ""]; } return removeAlternative(groupParent.parent); } return removeAlternative(groupParent); } default: return (0, util_1.assertNever)(parent); } } const messages = { alwaysRejectByChar: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a character.", alwaysAcceptByChar: "{{assertion}} will always accept because it is never {{followedOrPreceded}} by a character.", alwaysRejectByNonLineTerminator: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a non-line-terminator character.", alwaysAcceptByLineTerminator: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character.", alwaysAcceptByLineTerminatorOrEdge: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character or the {{startOrEnd}} of the input string.", alwaysAcceptOrRejectFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a word character.", alwaysAcceptOrRejectFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a non-word character.", alwaysAcceptOrRejectPrecededByWordFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a non-word character.", alwaysAcceptOrRejectPrecededByWordFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a word character.", alwaysForLookaround: "The {{kind}} {{assertion}} will always {{acceptOrReject}}.", alwaysForNegativeLookaround: "The negative {{kind}} {{assertion}} will always {{acceptOrReject}}.", acceptSuggestion: "Remove the assertion. (Replace with empty string.)", rejectSuggestion: "Remove branch of the assertion. (Replace with empty set.)", }; exports.default = (0, utils_1.createRule)("no-useless-assertions", { meta: { docs: { description: "disallow assertions that are known to always accept (or reject)", category: "Possible Errors", recommended: true, }, hasSuggestions: true, schema: [], messages, type: "problem", }, create(context) { function createVisitor({ node, flags, getRegexpLocation, fixReplaceNode, }) { const reported = new Set(); function replaceWithEmptyString(assertion) { if (assertion.parent.type === "Quantifier") { return fixReplaceNode(assertion.parent, ""); } return fixReplaceNode(assertion, ""); } function replaceWithEmptySet(assertion) { if (assertion.parent.type === "Quantifier") { if (assertion.parent.min === 0) { return fixReplaceNode(assertion.parent, ""); } const [element, replacement] = removeAlternative(assertion.parent.parent); return fixReplaceNode(element, replacement); } const [element, replacement] = removeAlternative(assertion.parent); return fixReplaceNode(element, replacement); } function report(assertion, messageId, data) { reported.add(assertion); const { acceptOrReject } = data; context.report({ node, loc: getRegexpLocation(assertion), messageId, data: Object.assign({ assertion: (0, mention_1.mention)(assertion) }, data), suggest: [ { messageId: `${acceptOrReject}Suggestion`, fix: acceptOrReject === "accept" ? replaceWithEmptyString(assertion) : replaceWithEmptySet(assertion), }, ], }); } function verifyStartOrEnd(assertion, getFirstCharAfterFn) { const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind); const next = getFirstCharAfterFn(assertion, direction, flags); const followedOrPreceded = assertion.kind === "end" ? "followed" : "preceded"; const lineTerminator = regexp_ast_analysis_1.Chars.lineTerminator(flags); if (next.edge) { if (!flags.multiline) { if (next.char.isEmpty) { report(assertion, "alwaysAcceptByChar", { followedOrPreceded, acceptOrReject: "accept", }); } } else { if (next.char.isSubsetOf(lineTerminator)) { report(assertion, "alwaysAcceptByLineTerminatorOrEdge", { followedOrPreceded, startOrEnd: assertion.kind, acceptOrReject: "accept", }); } } } else { if (!flags.multiline) { report(assertion, "alwaysRejectByChar", { followedOrPreceded, acceptOrReject: "reject", }); } else { if (next.char.isDisjointWith(lineTerminator)) { report(assertion, "alwaysRejectByNonLineTerminator", { followedOrPreceded, acceptOrReject: "reject", }); } else if (next.char.isSubsetOf(lineTerminator)) { report(assertion, "alwaysAcceptByLineTerminator", { followedOrPreceded, acceptOrReject: "accept", }); } } } } function verifyWordBoundary(assertion, getFirstCharAfterFn) { const word = regexp_ast_analysis_1.Chars.word(flags); const next = getFirstCharAfterFn(assertion, "ltr", flags); const prev = getFirstCharAfterFn(assertion, "rtl", flags); const nextIsWord = next.char.isSubsetOf(word) && !next.edge; const prevIsWord = prev.char.isSubsetOf(word) && !prev.edge; const nextIsNonWord = next.char.isDisjointWith(word); const prevIsNonWord = prev.char.isDisjointWith(word); const accept = assertion.negate ? "reject" : "accept"; const reject = assertion.negate ? "accept" : "reject"; if (prevIsNonWord) { if (nextIsWord) { report(assertion, "alwaysAcceptOrRejectFollowedByWord", { acceptOrReject: accept, }); } if (nextIsNonWord) { report(assertion, "alwaysAcceptOrRejectFollowedByNonWord", { acceptOrReject: reject, }); } } if (prevIsWord) { if (nextIsNonWord) { report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByNonWord", { acceptOrReject: accept, }); } if (nextIsWord) { report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByWord", { acceptOrReject: reject, }); } } } function verifyLookaround(assertion, getFirstCharAfterFn) { if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(assertion.alternatives, flags)) { return; } const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind); const after = getFirstCharAfterFn(assertion, direction, flags); const firstOf = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion.alternatives, direction, flags)); const accept = assertion.negate ? "reject" : "accept"; const reject = assertion.negate ? "accept" : "reject"; if (after.char.isDisjointWith(firstOf.char) && !(after.edge && firstOf.edge)) { report(assertion, assertion.negate ? "alwaysForNegativeLookaround" : "alwaysForLookaround", { kind: assertion.kind, acceptOrReject: reject, }); } const edgeSubset = firstOf.edge || !after.edge; if (firstOf.exact && edgeSubset && after.char.isSubsetOf(firstOf.char) && isSingleCharacterAssertion(assertion, (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind), flags)) { report(assertion, assertion.negate ? "alwaysForNegativeLookaround" : "alwaysForLookaround", { kind: assertion.kind, acceptOrReject: accept, }); } } function verifyAssertion(assertion, getFirstCharAfterFn) { switch (assertion.kind) { case "start": case "end": verifyStartOrEnd(assertion, getFirstCharAfterFn); break; case "word": verifyWordBoundary(assertion, getFirstCharAfterFn); break; case "lookahead": case "lookbehind": verifyLookaround(assertion, getFirstCharAfterFn); break; default: throw (0, util_1.assertNever)(assertion); } } const allAssertions = []; return { onAssertionEnter(assertion) { verifyAssertion(assertion, regexp_ast_analysis_1.getFirstCharAfter); allAssertions.push(assertion); }, onPatternLeave() { const reorderingGetFirstCharAfter = createReorderingGetFirstCharAfter(reported); for (const assertion of allAssertions) { if (!reported.has(assertion)) { verifyAssertion(assertion, reorderingGetFirstCharAfter); } } }, }; } return (0, utils_1.defineRegexpVisitor)(context, { createVisitor, }); }, });