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");
14 const lodash
= require("lodash");
16 //------------------------------------------------------------------------------
18 //------------------------------------------------------------------------------
20 const parser
= new RegExpParser();
23 * Finds the path from the given `regexpp` AST node to the root node.
24 * @param {regexpp.Node} node Node.
25 * @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
27 function getPathToRoot(node
) {
33 current
= current
.parent
;
40 * Determines whether the given `regexpp` AST node is a lookaround node.
41 * @param {regexpp.Node} node Node.
42 * @returns {boolean} `true` if it is a lookaround node.
44 function isLookaround(node
) {
45 return node
.type
=== "Assertion" &&
46 (node
.kind
=== "lookahead" || node
.kind
=== "lookbehind");
50 * Determines whether the given `regexpp` AST node is a negative lookaround node.
51 * @param {regexpp.Node} node Node.
52 * @returns {boolean} `true` if it is a negative lookaround node.
54 function isNegativeLookaround(node
) {
55 return isLookaround(node
) && node
.negate
;
58 //------------------------------------------------------------------------------
60 //------------------------------------------------------------------------------
67 description
: "disallow useless backreferences in regular expressions",
68 category
: "Possible Errors",
70 url
: "https://eslint.org/docs/rules/no-useless-backreference"
76 nested
: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
77 forward
: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
78 backward
: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
79 disjunctive
: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
80 intoNegativeLookaround
: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround."
87 * Checks and reports useless backreferences in the given regular expression.
88 * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
89 * @param {string} pattern Regular expression pattern.
90 * @param {string} flags Regular expression flags.
93 function checkRegex(node
, pattern
, flags
) {
97 regExpAST
= parser
.parsePattern(pattern
, 0, pattern
.length
, flags
.includes("u"));
100 // Ignore regular expressions with syntax errors
104 visitRegExpAST(regExpAST
, {
105 onBackreferenceEnter(bref
) {
106 const group
= bref
.resolved
,
107 brefPath
= getPathToRoot(bref
),
108 groupPath
= getPathToRoot(group
);
109 let messageId
= null;
111 if (brefPath
.includes(group
)) {
113 // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
114 messageId
= "nested";
117 // Start from the root to find the lowest common ancestor.
118 let i
= brefPath
.length
- 1,
119 j
= groupPath
.length
- 1;
124 } while (brefPath
[i
] === groupPath
[j
]);
126 const indexOfLowestCommonAncestor
= j
+ 1,
127 groupCut
= groupPath
.slice(0, indexOfLowestCommonAncestor
),
128 commonPath
= groupPath
.slice(indexOfLowestCommonAncestor
),
129 lowestCommonLookaround
= commonPath
.find(isLookaround
),
130 isMatchingBackward
= lowestCommonLookaround
&& lowestCommonLookaround
.kind
=== "lookbehind";
132 if (!isMatchingBackward
&& bref
.end
<= group
.start
) {
134 // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
135 messageId
= "forward";
136 } else if (isMatchingBackward
&& group
.end
<= bref
.start
) {
138 // the opposite of the previous when the regex is matching backward in a lookbehind context.
139 messageId
= "backward";
140 } else if (lodash
.last(groupCut
).type
=== "Alternative") {
142 // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
143 messageId
= "disjunctive";
144 } else if (groupCut
.some(isNegativeLookaround
)) {
146 // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
147 messageId
= "intoNegativeLookaround";
166 "Literal[regex]"(node
) {
167 const { pattern
, flags
} = node
.regex
;
169 checkRegex(node
, pattern
, flags
);
172 const scope
= context
.getScope(),
173 tracker
= new ReferenceTracker(scope
),
181 for (const { node
} of tracker
.iterateGlobalReferences(traceMap
)) {
182 const [patternNode
, flagsNode
] = node
.arguments
,
183 pattern
= getStringIfConstant(patternNode
, scope
),
184 flags
= getStringIfConstant(flagsNode
, scope
);
186 if (typeof pattern
=== "string") {
187 checkRegex(node
, pattern
, flags
|| "");