Skip to content

Commit ec59ec0

Browse files
g-planeplatinumazure
authored andcommittedMar 2, 2019
New: add rule "prefer-named-capture-group" (fixes #11381) (#11392)
* New: add rule "require-named-capture-group" (fixes #11381) * Chore: fix linting errors * Update: change rule name * Update: use `ReferenceTracker` * Update: ignore regexp syntax errors * Update: detect "unicode" flag * Update: report group instead of AST node * Docs: improve words * Chore: enable early return * Update: improve the message * Chore: simplify message Co-Authored-By: g-plane <g-plane@hotmail.com>
1 parent a44f750 commit ec59ec0

File tree

5 files changed

+264
-1
lines changed

5 files changed

+264
-1
lines changed
 
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Suggest using named capture group in regular expression (prefer-named-capture-group)
2+
3+
With the landing of ECMAScript 2018, named capture groups can be used in regular expressions, which can improve their readability.
4+
5+
```js
6+
const regex = /(?<year>[0-9]{4})/;
7+
```
8+
9+
## Rule Details
10+
11+
This rule is aimed at using named capture groups instead of numbered capture groups in regular expressions.
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```js
16+
/*eslint prefer-named-capture-group: "error"*/
17+
18+
const foo = /(ba[rz])/;
19+
const bar = new RegExp('(ba[rz])');
20+
const baz = RegExp('(ba[rz])');
21+
22+
foo.exec('bar')[1]; // Retrieve the group result.
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```js
28+
/*eslint prefer-named-capture-group: "error"*/
29+
30+
const foo = /(?<id>ba[rz])/;
31+
const bar = new RegExp('(?<id>ba[rz])');
32+
const baz = RegExp('(?<id>ba[rz])');
33+
34+
foo.exec('bar').groups.id; // Retrieve the group result.
35+
```
36+
37+
## When Not To Use It
38+
39+
If you are targeting ECMAScript 2017 and/or older environments, you can disable this rule, because this ECMAScript feature is only supported in ECMAScript 2018 and/or newer environments.
40+
41+
## Related Rules
42+
43+
* [no-invalid-regexp](./no-invalid-regexp.md)

‎lib/built-in-rules-index.js

+1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ module.exports = {
232232
"prefer-arrow-callback": require("./rules/prefer-arrow-callback"),
233233
"prefer-const": require("./rules/prefer-const"),
234234
"prefer-destructuring": require("./rules/prefer-destructuring"),
235+
"prefer-named-capture-group": require("./rules/prefer-named-capture-group"),
235236
"prefer-numeric-literals": require("./rules/prefer-numeric-literals"),
236237
"prefer-object-spread": require("./rules/prefer-object-spread"),
237238
"prefer-promise-reject-errors": require("./rules/prefer-promise-reject-errors"),
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* @fileoverview Rule to enforce requiring named capture groups in regular expression.
3+
* @author Pig Fang <https://github.com/g-plane>
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const {
13+
CALL,
14+
CONSTRUCT,
15+
ReferenceTracker,
16+
getStringIfConstant
17+
} = require("eslint-utils");
18+
const regexpp = require("regexpp");
19+
20+
//------------------------------------------------------------------------------
21+
// Helpers
22+
//------------------------------------------------------------------------------
23+
24+
const parser = new regexpp.RegExpParser();
25+
26+
//------------------------------------------------------------------------------
27+
// Rule Definition
28+
//------------------------------------------------------------------------------
29+
30+
module.exports = {
31+
meta: {
32+
type: "suggestion",
33+
34+
docs: {
35+
description: "enforce using named capture group in regular expression",
36+
category: "Best Practices",
37+
recommended: false,
38+
url: "https://eslint.org/docs/rules/prefer-named-capture-group"
39+
},
40+
41+
schema: [],
42+
43+
messages: {
44+
required: "Capture group '{{group}}' should be converted to a named or non-capturing group."
45+
}
46+
},
47+
48+
create(context) {
49+
50+
/**
51+
* Function to check regular expression.
52+
*
53+
* @param {string} regex The regular expression to be check.
54+
* @param {ASTNode} node AST node which contains regular expression.
55+
* @param {boolean} uFlag Flag indicates whether unicode mode is enabled or not.
56+
* @returns {void}
57+
*/
58+
function checkRegex(regex, node, uFlag) {
59+
let ast;
60+
61+
try {
62+
ast = parser.parsePattern(regex, 0, regex.length, uFlag);
63+
} catch (_) {
64+
65+
// ignore regex syntax errors
66+
return;
67+
}
68+
69+
regexpp.visitRegExpAST(ast, {
70+
onCapturingGroupEnter(group) {
71+
if (!group.name) {
72+
const locNode = node.type === "Literal" ? node : node.arguments[0];
73+
74+
context.report({
75+
node,
76+
messageId: "required",
77+
loc: {
78+
start: {
79+
line: locNode.loc.start.line,
80+
column: locNode.loc.start.column + group.start + 1
81+
},
82+
end: {
83+
line: locNode.loc.start.line,
84+
column: locNode.loc.start.column + group.end + 1
85+
}
86+
},
87+
data: {
88+
group: group.raw
89+
}
90+
});
91+
}
92+
}
93+
});
94+
}
95+
96+
return {
97+
Literal(node) {
98+
if (node.regex) {
99+
checkRegex(node.regex.pattern, node, node.regex.flags.includes("u"));
100+
}
101+
},
102+
Program() {
103+
const scope = context.getScope();
104+
const tracker = new ReferenceTracker(scope);
105+
const traceMap = {
106+
RegExp: {
107+
[CALL]: true,
108+
[CONSTRUCT]: true
109+
}
110+
};
111+
112+
for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
113+
const regex = getStringIfConstant(node.arguments[0]);
114+
const flags = getStringIfConstant(node.arguments[1]);
115+
116+
if (regex) {
117+
checkRegex(regex, node, flags && flags.includes("u"));
118+
}
119+
}
120+
}
121+
};
122+
}
123+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @fileoverview Tests for prefer-named-capture-group rule.
3+
* @author Pig Fang <https://github.com/g-plane>
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const rule = require("../../../lib/rules/prefer-named-capture-group"),
13+
RuleTester = require("../../../lib/testers/rule-tester");
14+
15+
//------------------------------------------------------------------------------
16+
// Tests
17+
//------------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
20+
21+
ruleTester.run("prefer-named-capture-group", rule, {
22+
valid: [
23+
"/normal_regex/",
24+
"/(?:[0-9]{4})/",
25+
"/(?<year>[0-9]{4})/",
26+
"/\\u{1F680}/u",
27+
"new RegExp()",
28+
"new RegExp(foo)",
29+
"new RegExp('')",
30+
"new RegExp('(?<year>[0-9]{4})')",
31+
"RegExp()",
32+
"RegExp(foo)",
33+
"RegExp('')",
34+
"RegExp('(?<year>[0-9]{4})')",
35+
"RegExp('(')", // invalid regexp should be ignored
36+
"RegExp('\\\\u{1F680}', 'u')"
37+
],
38+
39+
invalid: [
40+
{
41+
code: "/([0-9]{4})/",
42+
errors: [{
43+
messageId: "required",
44+
type: "Literal",
45+
data: { group: "([0-9]{4})" },
46+
line: 1,
47+
column: 2,
48+
endColumn: 12
49+
}]
50+
},
51+
{
52+
code: "new RegExp('([0-9]{4})')",
53+
errors: [{
54+
messageId: "required",
55+
type: "NewExpression",
56+
data: { group: "([0-9]{4})" },
57+
line: 1,
58+
column: 13,
59+
endColumn: 23
60+
}]
61+
},
62+
{
63+
code: "RegExp('([0-9]{4})')",
64+
errors: [{
65+
messageId: "required",
66+
type: "CallExpression",
67+
data: { group: "([0-9]{4})" },
68+
line: 1,
69+
column: 9,
70+
endColumn: 19
71+
}]
72+
},
73+
{
74+
code: "/([0-9]{4})-(\\w{5})/",
75+
errors: [
76+
{
77+
messageId: "required",
78+
type: "Literal",
79+
data: { group: "([0-9]{4})" },
80+
line: 1,
81+
column: 2,
82+
endColumn: 12
83+
},
84+
{
85+
messageId: "required",
86+
type: "Literal",
87+
data: { group: "(\\w{5})" },
88+
line: 1,
89+
column: 13,
90+
endColumn: 20
91+
}
92+
]
93+
}
94+
]
95+
});

‎tools/rule-types.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@
222222
"prefer-arrow-callback": "suggestion",
223223
"prefer-const": "suggestion",
224224
"prefer-destructuring": "suggestion",
225+
"prefer-named-capture-group": "suggestion",
225226
"prefer-numeric-literals": "suggestion",
226227
"prefer-object-spread": "suggestion",
227228
"prefer-promise-reject-errors": "suggestion",
@@ -264,4 +265,4 @@
264265
"wrap-regex": "layout",
265266
"yield-star-spacing": "layout",
266267
"yoda": "suggestion"
267-
}
268+
}

0 commit comments

Comments
 (0)
Please sign in to comment.