1 | /**
|
---|
2 | * @fileoverview Rule to count multiple spaces in regular expressions
|
---|
3 | * @author Matt DuVall <http://www.mattduvall.com/>
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | //------------------------------------------------------------------------------
|
---|
9 | // Requirements
|
---|
10 | //------------------------------------------------------------------------------
|
---|
11 |
|
---|
12 | const astUtils = require("./utils/ast-utils");
|
---|
13 | const regexpp = require("@eslint-community/regexpp");
|
---|
14 |
|
---|
15 | //------------------------------------------------------------------------------
|
---|
16 | // Helpers
|
---|
17 | //------------------------------------------------------------------------------
|
---|
18 |
|
---|
19 | const regExpParser = new regexpp.RegExpParser();
|
---|
20 | const DOUBLE_SPACE = / {2}/u;
|
---|
21 |
|
---|
22 | /**
|
---|
23 | * Check if node is a string
|
---|
24 | * @param {ASTNode} node node to evaluate
|
---|
25 | * @returns {boolean} True if its a string
|
---|
26 | * @private
|
---|
27 | */
|
---|
28 | function isString(node) {
|
---|
29 | return node && node.type === "Literal" && typeof node.value === "string";
|
---|
30 | }
|
---|
31 |
|
---|
32 | //------------------------------------------------------------------------------
|
---|
33 | // Rule Definition
|
---|
34 | //------------------------------------------------------------------------------
|
---|
35 |
|
---|
36 | /** @type {import('../shared/types').Rule} */
|
---|
37 | module.exports = {
|
---|
38 | meta: {
|
---|
39 | type: "suggestion",
|
---|
40 |
|
---|
41 | docs: {
|
---|
42 | description: "Disallow multiple spaces in regular expressions",
|
---|
43 | recommended: true,
|
---|
44 | url: "https://eslint.org/docs/latest/rules/no-regex-spaces"
|
---|
45 | },
|
---|
46 |
|
---|
47 | schema: [],
|
---|
48 | fixable: "code",
|
---|
49 |
|
---|
50 | messages: {
|
---|
51 | multipleSpaces: "Spaces are hard to count. Use {{{length}}}."
|
---|
52 | }
|
---|
53 | },
|
---|
54 |
|
---|
55 | create(context) {
|
---|
56 |
|
---|
57 | const sourceCode = context.sourceCode;
|
---|
58 |
|
---|
59 | /**
|
---|
60 | * Validate regular expression
|
---|
61 | * @param {ASTNode} nodeToReport Node to report.
|
---|
62 | * @param {string} pattern Regular expression pattern to validate.
|
---|
63 | * @param {string} rawPattern Raw representation of the pattern in the source code.
|
---|
64 | * @param {number} rawPatternStartRange Start range of the pattern in the source code.
|
---|
65 | * @param {string} flags Regular expression flags.
|
---|
66 | * @returns {void}
|
---|
67 | * @private
|
---|
68 | */
|
---|
69 | function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) {
|
---|
70 |
|
---|
71 | // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
|
---|
72 | if (!DOUBLE_SPACE.test(rawPattern)) {
|
---|
73 | return;
|
---|
74 | }
|
---|
75 |
|
---|
76 | const characterClassNodes = [];
|
---|
77 | let regExpAST;
|
---|
78 |
|
---|
79 | try {
|
---|
80 | regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") });
|
---|
81 | } catch {
|
---|
82 |
|
---|
83 | // Ignore regular expressions with syntax errors
|
---|
84 | return;
|
---|
85 | }
|
---|
86 |
|
---|
87 | regexpp.visitRegExpAST(regExpAST, {
|
---|
88 | onCharacterClassEnter(ccNode) {
|
---|
89 | characterClassNodes.push(ccNode);
|
---|
90 | }
|
---|
91 | });
|
---|
92 |
|
---|
93 | const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu;
|
---|
94 | let match;
|
---|
95 |
|
---|
96 | while ((match = spacesPattern.exec(pattern))) {
|
---|
97 | const { 1: { length }, index } = match;
|
---|
98 |
|
---|
99 | // Report only consecutive spaces that are not in character classes.
|
---|
100 | if (
|
---|
101 | characterClassNodes.every(({ start, end }) => index < start || end <= index)
|
---|
102 | ) {
|
---|
103 | context.report({
|
---|
104 | node: nodeToReport,
|
---|
105 | messageId: "multipleSpaces",
|
---|
106 | data: { length },
|
---|
107 | fix(fixer) {
|
---|
108 | if (pattern !== rawPattern) {
|
---|
109 | return null;
|
---|
110 | }
|
---|
111 | return fixer.replaceTextRange(
|
---|
112 | [rawPatternStartRange + index, rawPatternStartRange + index + length],
|
---|
113 | ` {${length}}`
|
---|
114 | );
|
---|
115 | }
|
---|
116 | });
|
---|
117 |
|
---|
118 | // Report only the first occurrence of consecutive spaces
|
---|
119 | return;
|
---|
120 | }
|
---|
121 | }
|
---|
122 | }
|
---|
123 |
|
---|
124 | /**
|
---|
125 | * Validate regular expression literals
|
---|
126 | * @param {ASTNode} node node to validate
|
---|
127 | * @returns {void}
|
---|
128 | * @private
|
---|
129 | */
|
---|
130 | function checkLiteral(node) {
|
---|
131 | if (node.regex) {
|
---|
132 | const pattern = node.regex.pattern;
|
---|
133 | const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/"));
|
---|
134 | const rawPatternStartRange = node.range[0] + 1;
|
---|
135 | const flags = node.regex.flags;
|
---|
136 |
|
---|
137 | checkRegex(
|
---|
138 | node,
|
---|
139 | pattern,
|
---|
140 | rawPattern,
|
---|
141 | rawPatternStartRange,
|
---|
142 | flags
|
---|
143 | );
|
---|
144 | }
|
---|
145 | }
|
---|
146 |
|
---|
147 | /**
|
---|
148 | * Validate strings passed to the RegExp constructor
|
---|
149 | * @param {ASTNode} node node to validate
|
---|
150 | * @returns {void}
|
---|
151 | * @private
|
---|
152 | */
|
---|
153 | function checkFunction(node) {
|
---|
154 | const scope = sourceCode.getScope(node);
|
---|
155 | const regExpVar = astUtils.getVariableByName(scope, "RegExp");
|
---|
156 | const shadowed = regExpVar && regExpVar.defs.length > 0;
|
---|
157 | const patternNode = node.arguments[0];
|
---|
158 |
|
---|
159 | if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
|
---|
160 | const pattern = patternNode.value;
|
---|
161 | const rawPattern = patternNode.raw.slice(1, -1);
|
---|
162 | const rawPatternStartRange = patternNode.range[0] + 1;
|
---|
163 | let flags;
|
---|
164 |
|
---|
165 | if (node.arguments.length < 2) {
|
---|
166 |
|
---|
167 | // It has no flags.
|
---|
168 | flags = "";
|
---|
169 | } else {
|
---|
170 | const flagsNode = node.arguments[1];
|
---|
171 |
|
---|
172 | if (isString(flagsNode)) {
|
---|
173 | flags = flagsNode.value;
|
---|
174 | } else {
|
---|
175 |
|
---|
176 | // The flags cannot be determined.
|
---|
177 | return;
|
---|
178 | }
|
---|
179 | }
|
---|
180 |
|
---|
181 | checkRegex(
|
---|
182 | node,
|
---|
183 | pattern,
|
---|
184 | rawPattern,
|
---|
185 | rawPatternStartRange,
|
---|
186 | flags
|
---|
187 | );
|
---|
188 | }
|
---|
189 | }
|
---|
190 |
|
---|
191 | return {
|
---|
192 | Literal: checkLiteral,
|
---|
193 | CallExpression: checkFunction,
|
---|
194 | NewExpression: checkFunction
|
---|
195 | };
|
---|
196 | }
|
---|
197 | };
|
---|