blob: ae491471282458793e8b7396c822f439d4d16388 [file] [log] [blame]
Tim van der Lippe16aca392020-11-13 11:37:13 +00001/**
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
12const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils");
13const { RegExpParser, visitRegExpAST } = require("regexpp");
Tim van der Lippe16aca392020-11-13 11:37:13 +000014
15//------------------------------------------------------------------------------
16// Helpers
17//------------------------------------------------------------------------------
18
19const 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 */
26function 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 */
43function 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 */
53function isNegativeLookaround(node) {
54 return isLookaround(node) && node.negate;
55}
56
57//------------------------------------------------------------------------------
58// Rule Definition
59//------------------------------------------------------------------------------
60
61module.exports = {
62 meta: {
63 type: "problem",
64
65 docs: {
66 description: "disallow useless backreferences in regular expressions",
Tim van der Lippe0fb47802021-11-08 16:23:10 +000067 recommended: true,
Tim van der Lippe16aca392020-11-13 11:37:13 +000068 url: "https://eslint.org/docs/rules/no-useless-backreference"
69 },
70
71 schema: [],
72
73 messages: {
74 nested: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
75 forward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
76 backward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
77 disjunctive: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
78 intoNegativeLookaround: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround."
79 }
80 },
81
82 create(context) {
83
84 /**
85 * Checks and reports useless backreferences in the given regular expression.
86 * @param {ASTNode} node Node that represents regular expression. A regex literal or RegExp constructor call.
87 * @param {string} pattern Regular expression pattern.
88 * @param {string} flags Regular expression flags.
89 * @returns {void}
90 */
91 function checkRegex(node, pattern, flags) {
92 let regExpAST;
93
94 try {
95 regExpAST = parser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
96 } catch {
97
98 // Ignore regular expressions with syntax errors
99 return;
100 }
101
102 visitRegExpAST(regExpAST, {
103 onBackreferenceEnter(bref) {
104 const group = bref.resolved,
105 brefPath = getPathToRoot(bref),
106 groupPath = getPathToRoot(group);
107 let messageId = null;
108
109 if (brefPath.includes(group)) {
110
111 // group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
112 messageId = "nested";
113 } else {
114
115 // Start from the root to find the lowest common ancestor.
116 let i = brefPath.length - 1,
117 j = groupPath.length - 1;
118
119 do {
120 i--;
121 j--;
122 } while (brefPath[i] === groupPath[j]);
123
124 const indexOfLowestCommonAncestor = j + 1,
125 groupCut = groupPath.slice(0, indexOfLowestCommonAncestor),
126 commonPath = groupPath.slice(indexOfLowestCommonAncestor),
127 lowestCommonLookaround = commonPath.find(isLookaround),
128 isMatchingBackward = lowestCommonLookaround && lowestCommonLookaround.kind === "lookbehind";
129
130 if (!isMatchingBackward && bref.end <= group.start) {
131
132 // bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
133 messageId = "forward";
134 } else if (isMatchingBackward && group.end <= bref.start) {
135
136 // the opposite of the previous when the regex is matching backward in a lookbehind context.
137 messageId = "backward";
Simon Zünd52e20202021-06-16 08:34:28 +0200138 } else if (groupCut[groupCut.length - 1].type === "Alternative") {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000139
140 // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
141 messageId = "disjunctive";
142 } else if (groupCut.some(isNegativeLookaround)) {
143
144 // group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
145 messageId = "intoNegativeLookaround";
146 }
147 }
148
149 if (messageId) {
150 context.report({
151 node,
152 messageId,
153 data: {
154 bref: bref.raw,
155 group: group.raw
156 }
157 });
158 }
159 }
160 });
161 }
162
163 return {
164 "Literal[regex]"(node) {
165 const { pattern, flags } = node.regex;
166
167 checkRegex(node, pattern, flags);
168 },
169 Program() {
170 const scope = context.getScope(),
171 tracker = new ReferenceTracker(scope),
172 traceMap = {
173 RegExp: {
174 [CALL]: true,
175 [CONSTRUCT]: true
176 }
177 };
178
179 for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
180 const [patternNode, flagsNode] = node.arguments,
181 pattern = getStringIfConstant(patternNode, scope),
182 flags = getStringIfConstant(flagsNode, scope);
183
184 if (typeof pattern === "string") {
185 checkRegex(node, pattern, flags || "");
186 }
187 }
188 }
189 };
190 }
191};