Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7bd9839

Browse files
authored
feat: add support for es2025 duplicate named capturing groups (#18630)
* feat: add support for es2025 to `no-useless-backreference` * test: add test for es2025 to `no-invalid-regexp` * update REGEXPP_LATEST_ECMA_VERSION to 2025 * change `no-useless-backreference` to handle disjunctive * fix comment
1 parent 1381394 commit 7bd9839

File tree

5 files changed

+252
-114
lines changed

5 files changed

+252
-114
lines changed

lib/rules/no-useless-backreference.js

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ module.exports = {
7272
schema: [],
7373

7474
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."
75+
nested: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} from within that group.",
76+
forward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which appears later in the pattern.",
77+
backward: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which appears before in the same lookbehind.",
78+
disjunctive: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which is in another alternative.",
79+
intoNegativeLookaround: "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}'{{ otherGroups }} which is in a negative lookaround."
8080
}
8181
},
8282

@@ -104,16 +104,21 @@ module.exports = {
104104

105105
visitRegExpAST(regExpAST, {
106106
onBackreferenceEnter(bref) {
107-
const group = bref.resolved,
108-
brefPath = getPathToRoot(bref),
109-
groupPath = getPathToRoot(group);
110-
let messageId = null;
107+
const groups = [bref.resolved].flat(),
108+
brefPath = getPathToRoot(bref);
111109

112-
if (brefPath.includes(group)) {
110+
const problems = groups.map(group => {
111+
const groupPath = getPathToRoot(group);
112+
113+
if (brefPath.includes(group)) {
114+
115+
// group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
116+
return {
117+
messageId: "nested",
118+
group
119+
};
120+
}
113121

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 {
117122

118123
// Start from the root to find the lowest common ancestor.
119124
let i = brefPath.length - 1,
@@ -130,35 +135,80 @@ module.exports = {
130135
lowestCommonLookaround = commonPath.find(isLookaround),
131136
isMatchingBackward = lowestCommonLookaround && lowestCommonLookaround.kind === "lookbehind";
132137

138+
if (groupCut.at(-1).type === "Alternative") {
139+
140+
// group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
141+
return {
142+
messageId: "disjunctive",
143+
group
144+
};
145+
}
133146
if (!isMatchingBackward && bref.end <= group.start) {
134147

135148
// 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) {
149+
return {
150+
messageId: "forward",
151+
group
152+
};
153+
}
154+
if (isMatchingBackward && group.end <= bref.start) {
138155

139156
// the opposite of the previous when the regex is matching backward in a lookbehind context.
140-
messageId = "backward";
141-
} else if (groupCut.at(-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)) {
157+
return {
158+
messageId: "backward",
159+
group
160+
};
161+
}
162+
if (groupCut.some(isNegativeLookaround)) {
146163

147164
// group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
148-
messageId = "intoNegativeLookaround";
165+
return {
166+
messageId: "intoNegativeLookaround",
167+
group
168+
};
149169
}
170+
171+
return null;
172+
});
173+
174+
if (problems.length === 0 || problems.some(problem => !problem)) {
175+
176+
// If there are no problems or no problems with any group then do not report it.
177+
return;
150178
}
151179

152-
if (messageId) {
153-
context.report({
154-
node,
155-
messageId,
156-
data: {
157-
bref: bref.raw,
158-
group: group.raw
159-
}
160-
});
180+
let problemsToReport;
181+
182+
// Gets problems that appear in the same disjunction.
183+
const problemsInSameDisjunction = problems.filter(problem => problem.messageId !== "disjunctive");
184+
185+
if (problemsInSameDisjunction.length) {
186+
187+
// Only report problems that appear in the same disjunction.
188+
problemsToReport = problemsInSameDisjunction;
189+
} else {
190+
191+
// If all groups appear in different disjunctions, report it.
192+
problemsToReport = problems;
161193
}
194+
195+
const [{ messageId, group }, ...other] = problemsToReport;
196+
let otherGroups = "";
197+
198+
if (other.length === 1) {
199+
otherGroups = " and another group";
200+
} else if (other.length > 1) {
201+
otherGroups = ` and other ${other.length} groups`;
202+
}
203+
context.report({
204+
node,
205+
messageId,
206+
data: {
207+
bref: bref.raw,
208+
group: group.raw,
209+
otherGroups
210+
}
211+
});
162212
}
163213
});
164214
}

lib/rules/utils/regular-expressions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
const { RegExpValidator } = require("@eslint-community/regexpp");
1010

11-
const REGEXPP_LATEST_ECMA_VERSION = 2024;
11+
const REGEXPP_LATEST_ECMA_VERSION = 2025;
1212

1313
/**
1414
* Checks if the given regular expression pattern would be valid with the `u` flag.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"bugs": "https://github.com/eslint/eslint/issues/",
6868
"dependencies": {
6969
"@eslint-community/eslint-utils": "^4.2.0",
70-
"@eslint-community/regexpp": "^4.6.1",
70+
"@eslint-community/regexpp": "^4.11.0",
7171
"@eslint/config-array": "^0.17.0",
7272
"@eslint/eslintrc": "^3.1.0",
7373
"@eslint/js": "9.6.0",

tests/lib/rules/no-invalid-regexp.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ ruleTester.run("no-invalid-regexp", rule, {
8989
"new RegExp('[A--B]', flags)", // valid only with `v` flag
9090
"new RegExp('[[]\\\\u{0}*', flags)", // valid only with `u` flag
9191

92+
// ES2025
93+
"new RegExp('((?<k>a)|(?<k>b))')",
94+
9295
// allowConstructorFlags
9396
{
9497
code: "new RegExp('.', 'g')",
@@ -356,6 +359,16 @@ ruleTester.run("no-invalid-regexp", rule, {
356359
data: { message: "Invalid regular expression: /[[]\\u{0}*/v: Unterminated character class" },
357360
type: "NewExpression"
358361
}]
362+
},
363+
364+
// ES2025
365+
{
366+
code: "new RegExp('(?<k>a)(?<k>b)')",
367+
errors: [{
368+
messageId: "regexMessage",
369+
data: { message: "Invalid regular expression: /(?<k>a)(?<k>b)/: Duplicate capture group name" },
370+
type: "NewExpression"
371+
}]
359372
}
360373
]
361374
});

0 commit comments

Comments
 (0)