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-community/eslint-utils");
|
---|
18 | const regexpp = require("@eslint-community/regexpp");
|
---|
19 |
|
---|
20 | //------------------------------------------------------------------------------
|
---|
21 | // Helpers
|
---|
22 | //------------------------------------------------------------------------------
|
---|
23 |
|
---|
24 | const parser = new regexpp.RegExpParser();
|
---|
25 |
|
---|
26 | /**
|
---|
27 | * Creates fixer suggestions for the regex, if statically determinable.
|
---|
28 | * @param {number} groupStart Starting index of the regex group.
|
---|
29 | * @param {string} pattern The regular expression pattern to be checked.
|
---|
30 | * @param {string} rawText Source text of the regexNode.
|
---|
31 | * @param {ASTNode} regexNode AST node which contains the regular expression.
|
---|
32 | * @returns {Array<SuggestionResult>} Fixer suggestions for the regex, if statically determinable.
|
---|
33 | */
|
---|
34 | function suggestIfPossible(groupStart, pattern, rawText, regexNode) {
|
---|
35 | switch (regexNode.type) {
|
---|
36 | case "Literal":
|
---|
37 | if (typeof regexNode.value === "string" && rawText.includes("\\")) {
|
---|
38 | return null;
|
---|
39 | }
|
---|
40 | break;
|
---|
41 | case "TemplateLiteral":
|
---|
42 | if (regexNode.expressions.length || rawText.slice(1, -1) !== pattern) {
|
---|
43 | return null;
|
---|
44 | }
|
---|
45 | break;
|
---|
46 | default:
|
---|
47 | return null;
|
---|
48 | }
|
---|
49 |
|
---|
50 | const start = regexNode.range[0] + groupStart + 2;
|
---|
51 |
|
---|
52 | return [
|
---|
53 | {
|
---|
54 | fix(fixer) {
|
---|
55 | const existingTemps = pattern.match(/temp\d+/gu) || [];
|
---|
56 | const highestTempCount = existingTemps.reduce(
|
---|
57 | (previous, next) =>
|
---|
58 | Math.max(previous, Number(next.slice("temp".length))),
|
---|
59 | 0
|
---|
60 | );
|
---|
61 |
|
---|
62 | return fixer.insertTextBeforeRange(
|
---|
63 | [start, start],
|
---|
64 | `?<temp${highestTempCount + 1}>`
|
---|
65 | );
|
---|
66 | },
|
---|
67 | messageId: "addGroupName"
|
---|
68 | },
|
---|
69 | {
|
---|
70 | fix(fixer) {
|
---|
71 | return fixer.insertTextBeforeRange(
|
---|
72 | [start, start],
|
---|
73 | "?:"
|
---|
74 | );
|
---|
75 | },
|
---|
76 | messageId: "addNonCapture"
|
---|
77 | }
|
---|
78 | ];
|
---|
79 | }
|
---|
80 |
|
---|
81 | //------------------------------------------------------------------------------
|
---|
82 | // Rule Definition
|
---|
83 | //------------------------------------------------------------------------------
|
---|
84 |
|
---|
85 | /** @type {import('../shared/types').Rule} */
|
---|
86 | module.exports = {
|
---|
87 | meta: {
|
---|
88 | type: "suggestion",
|
---|
89 |
|
---|
90 | docs: {
|
---|
91 | description: "Enforce using named capture group in regular expression",
|
---|
92 | recommended: false,
|
---|
93 | url: "https://eslint.org/docs/latest/rules/prefer-named-capture-group"
|
---|
94 | },
|
---|
95 |
|
---|
96 | hasSuggestions: true,
|
---|
97 |
|
---|
98 | schema: [],
|
---|
99 |
|
---|
100 | messages: {
|
---|
101 | addGroupName: "Add name to capture group.",
|
---|
102 | addNonCapture: "Convert group to non-capturing.",
|
---|
103 | required: "Capture group '{{group}}' should be converted to a named or non-capturing group."
|
---|
104 | }
|
---|
105 | },
|
---|
106 |
|
---|
107 | create(context) {
|
---|
108 | const sourceCode = context.sourceCode;
|
---|
109 |
|
---|
110 | /**
|
---|
111 | * Function to check regular expression.
|
---|
112 | * @param {string} pattern The regular expression pattern to be checked.
|
---|
113 | * @param {ASTNode} node AST node which contains the regular expression or a call/new expression.
|
---|
114 | * @param {ASTNode} regexNode AST node which contains the regular expression.
|
---|
115 | * @param {string|null} flags The regular expression flags to be checked.
|
---|
116 | * @returns {void}
|
---|
117 | */
|
---|
118 | function checkRegex(pattern, node, regexNode, flags) {
|
---|
119 | let ast;
|
---|
120 |
|
---|
121 | try {
|
---|
122 | ast = parser.parsePattern(pattern, 0, pattern.length, {
|
---|
123 | unicode: Boolean(flags && flags.includes("u")),
|
---|
124 | unicodeSets: Boolean(flags && flags.includes("v"))
|
---|
125 | });
|
---|
126 | } catch {
|
---|
127 |
|
---|
128 | // ignore regex syntax errors
|
---|
129 | return;
|
---|
130 | }
|
---|
131 |
|
---|
132 | regexpp.visitRegExpAST(ast, {
|
---|
133 | onCapturingGroupEnter(group) {
|
---|
134 | if (!group.name) {
|
---|
135 | const rawText = sourceCode.getText(regexNode);
|
---|
136 | const suggest = suggestIfPossible(group.start, pattern, rawText, regexNode);
|
---|
137 |
|
---|
138 | context.report({
|
---|
139 | node,
|
---|
140 | messageId: "required",
|
---|
141 | data: {
|
---|
142 | group: group.raw
|
---|
143 | },
|
---|
144 | suggest
|
---|
145 | });
|
---|
146 | }
|
---|
147 | }
|
---|
148 | });
|
---|
149 | }
|
---|
150 |
|
---|
151 | return {
|
---|
152 | Literal(node) {
|
---|
153 | if (node.regex) {
|
---|
154 | checkRegex(node.regex.pattern, node, node, node.regex.flags);
|
---|
155 | }
|
---|
156 | },
|
---|
157 | Program(node) {
|
---|
158 | const scope = sourceCode.getScope(node);
|
---|
159 | const tracker = new ReferenceTracker(scope);
|
---|
160 | const traceMap = {
|
---|
161 | RegExp: {
|
---|
162 | [CALL]: true,
|
---|
163 | [CONSTRUCT]: true
|
---|
164 | }
|
---|
165 | };
|
---|
166 |
|
---|
167 | for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
|
---|
168 | const regex = getStringIfConstant(refNode.arguments[0]);
|
---|
169 | const flags = getStringIfConstant(refNode.arguments[1]);
|
---|
170 |
|
---|
171 | if (regex) {
|
---|
172 | checkRegex(regex, refNode, refNode.arguments[0], flags);
|
---|
173 | }
|
---|
174 | }
|
---|
175 | }
|
---|
176 | };
|
---|
177 | }
|
---|
178 | };
|
---|