[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to disallow useless backreferences in regular expressions
|
---|
| 3 | * @author Milos Djermanovic
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | //------------------------------------------------------------------------------
|
---|
| 9 | // Requirements
|
---|
| 10 | //------------------------------------------------------------------------------
|
---|
| 11 |
|
---|
| 12 | const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils");
|
---|
| 13 | const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp");
|
---|
| 14 |
|
---|
| 15 | //------------------------------------------------------------------------------
|
---|
| 16 | // Helpers
|
---|
| 17 | //------------------------------------------------------------------------------
|
---|
| 18 |
|
---|
| 19 | const parser = new RegExpParser();
|
---|
| 20 |
|
---|
| 21 | /**
|
---|
| 22 | * Finds the path from the given `regexpp` AST node to the root node.
|
---|
| 23 | * @param {regexpp.Node} node Node.
|
---|
| 24 | * @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
|
---|
| 25 | */
|
---|
| 26 | function getPathToRoot(node) {
|
---|
| 27 | const path = [];
|
---|
| 28 | let current = node;
|
---|
| 29 |
|
---|
| 30 | do {
|
---|
| 31 | path.push(current);
|
---|
| 32 | current = current.parent;
|
---|
| 33 | } while (current);
|
---|
| 34 |
|
---|
| 35 | return path;
|
---|
| 36 | }
|
---|
| 37 |
|
---|
| 38 | /**
|
---|
| 39 | * Determines whether the given `regexpp` AST node is a lookaround node.
|
---|
| 40 | * @param {regexpp.Node} node Node.
|
---|
| 41 | * @returns {boolean} `true` if it is a lookaround node.
|
---|
| 42 | */
|
---|
| 43 | function isLookaround(node) {
|
---|
| 44 | return node.type === "Assertion" &&
|
---|
| 45 | (node.kind === "lookahead" || node.kind === "lookbehind");
|
---|
| 46 | }
|
---|
| 47 |
|
---|
| 48 | /**
|
---|
| 49 | * Determines whether the given `regexpp` AST node is a negative lookaround node.
|
---|
| 50 | * @param {regexpp.Node} node Node.
|
---|
| 51 | * @returns {boolean} `true` if it is a negative lookaround node.
|
---|
| 52 | */
|
---|
| 53 | function isNegativeLookaround(node) {
|
---|
| 54 | return isLookaround(node) && node.negate;
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | //------------------------------------------------------------------------------
|
---|
| 58 | // Rule Definition
|
---|
| 59 | //------------------------------------------------------------------------------
|
---|
| 60 |
|
---|
| 61 | /** @type {import('../shared/types').Rule} */
|
---|
| 62 | module.exports = {
|
---|
| 63 | meta: {
|
---|
| 64 | type: "problem",
|
---|
| 65 |
|
---|
| 66 | docs: {
|
---|
| 67 | description: "Disallow useless backreferences in regular expressions",
|
---|
| 68 | recommended: true,
|
---|
| 69 | url: "https://eslint.org/docs/latest/rules/no-useless-backreference"
|
---|
| 70 | },
|
---|
| 71 |
|
---|
| 72 | schema: [],
|
---|
| 73 |
|
---|
| 74 | messages: {
|
---|
| 75 | nested: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
|
---|
| 76 | forward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
|
---|
| 77 | backward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
|
---|
| 78 | disjunctive: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
|
---|
| 79 | intoNegativeLookaround: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround."
|
---|
| 80 | }
|
---|
| 81 | },
|
---|
| 82 |
|
---|
| 83 | create(context) {
|
---|
| 84 |
|
---|
| 85 | const sourceCode = context.sourceCode;
|
---|
| 86 |
|
---|
| 87 | /**
|
---|
| 88 | * Checks and reports useless backreferences in the given regular expression.
|
---|
| 89 | * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
|
---|
| 90 | * @param {string} pattern Regular expression pattern.
|
---|
| 91 | * @param {string} flags Regular expression flags.
|
---|
| 92 | * @returns {void}
|
---|
| 93 | */
|
---|
| 94 | function checkRegex(node, pattern, flags) {
|
---|
| 95 | let regExpAST;
|
---|
| 96 |
|
---|
| 97 | try {
|
---|
| 98 | regExpAST = parser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") });
|
---|
| 99 | } catch {
|
---|
| 100 |
|
---|
| 101 | // Ignore regular expressions with syntax errors
|
---|
| 102 | return;
|
---|
| 103 | }
|
---|
| 104 |
|
---|
| 105 | visitRegExpAST(regExpAST, {
|
---|
| 106 | onBackreferenceEnter(bref) {
|
---|
| 107 | const group = bref.resolved,
|
---|
| 108 | brefPath = getPathToRoot(bref),
|
---|
| 109 | groupPath = getPathToRoot(group);
|
---|
| 110 | let messageId = null;
|
---|
| 111 |
|
---|
| 112 | if (brefPath.includes(group)) {
|
---|
| 113 |
|
---|
| 114 | // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
|
---|
| 115 | messageId = "nested";
|
---|
| 116 | } else {
|
---|
| 117 |
|
---|
| 118 | // Start from the root to find the lowest common ancestor.
|
---|
| 119 | let i = brefPath.length - 1,
|
---|
| 120 | j = groupPath.length - 1;
|
---|
| 121 |
|
---|
| 122 | do {
|
---|
| 123 | i--;
|
---|
| 124 | j--;
|
---|
| 125 | } while (brefPath[i] === groupPath[j]);
|
---|
| 126 |
|
---|
| 127 | const indexOfLowestCommonAncestor = j + 1,
|
---|
| 128 | groupCut = groupPath.slice(0, indexOfLowestCommonAncestor),
|
---|
| 129 | commonPath = groupPath.slice(indexOfLowestCommonAncestor),
|
---|
| 130 | lowestCommonLookaround = commonPath.find(isLookaround),
|
---|
| 131 | isMatchingBackward = lowestCommonLookaround && lowestCommonLookaround.kind === "lookbehind";
|
---|
| 132 |
|
---|
| 133 | if (!isMatchingBackward && bref.end <= group.start) {
|
---|
| 134 |
|
---|
| 135 | // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
|
---|
| 136 | messageId = "forward";
|
---|
| 137 | } else if (isMatchingBackward && group.end <= bref.start) {
|
---|
| 138 |
|
---|
| 139 | // the opposite of the previous when the regex is matching backward in a lookbehind context.
|
---|
| 140 | messageId = "backward";
|
---|
| 141 | } else if (groupCut[groupCut.length - 1].type === "Alternative") {
|
---|
| 142 |
|
---|
| 143 | // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
|
---|
| 144 | messageId = "disjunctive";
|
---|
| 145 | } else if (groupCut.some(isNegativeLookaround)) {
|
---|
| 146 |
|
---|
| 147 | // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
|
---|
| 148 | messageId = "intoNegativeLookaround";
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | if (messageId) {
|
---|
| 153 | context.report({
|
---|
| 154 | node,
|
---|
| 155 | messageId,
|
---|
| 156 | data: {
|
---|
| 157 | bref: bref.raw,
|
---|
| 158 | group: group.raw
|
---|
| 159 | }
|
---|
| 160 | });
|
---|
| 161 | }
|
---|
| 162 | }
|
---|
| 163 | });
|
---|
| 164 | }
|
---|
| 165 |
|
---|
| 166 | return {
|
---|
| 167 | "Literal[regex]"(node) {
|
---|
| 168 | const { pattern, flags } = node.regex;
|
---|
| 169 |
|
---|
| 170 | checkRegex(node, pattern, flags);
|
---|
| 171 | },
|
---|
| 172 | Program(node) {
|
---|
| 173 | const scope = sourceCode.getScope(node),
|
---|
| 174 | tracker = new ReferenceTracker(scope),
|
---|
| 175 | traceMap = {
|
---|
| 176 | RegExp: {
|
---|
| 177 | [CALL]: true,
|
---|
| 178 | [CONSTRUCT]: true
|
---|
| 179 | }
|
---|
| 180 | };
|
---|
| 181 |
|
---|
| 182 | for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
|
---|
| 183 | const [patternNode, flagsNode] = refNode.arguments,
|
---|
| 184 | pattern = getStringIfConstant(patternNode, scope),
|
---|
| 185 | flags = getStringIfConstant(flagsNode, scope);
|
---|
| 186 |
|
---|
| 187 | if (typeof pattern === "string") {
|
---|
| 188 | checkRegex(refNode, pattern, flags || "");
|
---|
| 189 | }
|
---|
| 190 | }
|
---|
| 191 | }
|
---|
| 192 | };
|
---|
| 193 | }
|
---|
| 194 | };
|
---|