2 * @fileoverview Rule to disallow useless backreferences in regular expressions
3 * @author Milos Djermanovic
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const { CALL
, CONSTRUCT
, ReferenceTracker
, getStringIfConstant
} = require("eslint-utils");
13 const { RegExpParser
, visitRegExpAST
} = require("regexpp");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
19 const parser
= new RegExpParser();
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.
26 function getPathToRoot(node
) {
32 current
= current
.parent
;
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.
43 function isLookaround(node
) {
44 return node
.type
=== "Assertion" &&
45 (node
.kind
=== "lookahead" || node
.kind
=== "lookbehind");
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.
53 function isNegativeLookaround(node
) {
54 return isLookaround(node
) && node
.negate
;
57 //------------------------------------------------------------------------------
59 //------------------------------------------------------------------------------
61 /** @type {import('../shared/types').Rule} */
67 description
: "Disallow useless backreferences in regular expressions",
69 url
: "https://eslint.org/docs/rules/no-useless-backreference"
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."
86 * Checks and reports useless backreferences in the given regular expression.
87 * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
88 * @param {string} pattern Regular expression pattern.
89 * @param {string} flags Regular expression flags.
92 function checkRegex(node
, pattern
, flags
) {
96 regExpAST
= parser
.parsePattern(pattern
, 0, pattern
.length
, flags
.includes("u"));
99 // Ignore regular expressions with syntax errors
103 visitRegExpAST(regExpAST
, {
104 onBackreferenceEnter(bref
) {
105 const group
= bref
.resolved
,
106 brefPath
= getPathToRoot(bref
),
107 groupPath
= getPathToRoot(group
);
108 let messageId
= null;
110 if (brefPath
.includes(group
)) {
112 // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
113 messageId
= "nested";
116 // Start from the root to find the lowest common ancestor.
117 let i
= brefPath
.length
- 1,
118 j
= groupPath
.length
- 1;
123 } while (brefPath
[i
] === groupPath
[j
]);
125 const indexOfLowestCommonAncestor
= j
+ 1,
126 groupCut
= groupPath
.slice(0, indexOfLowestCommonAncestor
),
127 commonPath
= groupPath
.slice(indexOfLowestCommonAncestor
),
128 lowestCommonLookaround
= commonPath
.find(isLookaround
),
129 isMatchingBackward
= lowestCommonLookaround
&& lowestCommonLookaround
.kind
=== "lookbehind";
131 if (!isMatchingBackward
&& bref
.end
<= group
.start
) {
133 // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
134 messageId
= "forward";
135 } else if (isMatchingBackward
&& group
.end
<= bref
.start
) {
137 // the opposite of the previous when the regex is matching backward in a lookbehind context.
138 messageId
= "backward";
139 } else if (groupCut
[groupCut
.length
- 1].type
=== "Alternative") {
141 // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
142 messageId
= "disjunctive";
143 } else if (groupCut
.some(isNegativeLookaround
)) {
145 // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
146 messageId
= "intoNegativeLookaround";
165 "Literal[regex]"(node
) {
166 const { pattern
, flags
} = node
.regex
;
168 checkRegex(node
, pattern
, flags
);
171 const scope
= context
.getScope(),
172 tracker
= new ReferenceTracker(scope
),
180 for (const { node
} of tracker
.iterateGlobalReferences(traceMap
)) {
181 const [patternNode
, flagsNode
] = node
.arguments
,
182 pattern
= getStringIfConstant(patternNode
, scope
),
183 flags
= getStringIfConstant(flagsNode
, scope
);
185 if (typeof pattern
=== "string") {
186 checkRegex(node
, pattern
, flags
|| "");