]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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("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 | module.exports = { | |
37 | meta: { | |
38 | type: "suggestion", | |
39 | ||
40 | docs: { | |
41 | description: "disallow multiple spaces in regular expressions", | |
eb39fafa DC |
42 | recommended: true, |
43 | url: "https://eslint.org/docs/rules/no-regex-spaces" | |
44 | }, | |
45 | ||
46 | schema: [], | |
47 | fixable: "code", | |
48 | ||
49 | messages: { | |
50 | multipleSpaces: "Spaces are hard to count. Use {{{length}}}." | |
51 | } | |
52 | }, | |
53 | ||
54 | create(context) { | |
55 | ||
56 | /** | |
57 | * Validate regular expression | |
58 | * @param {ASTNode} nodeToReport Node to report. | |
59 | * @param {string} pattern Regular expression pattern to validate. | |
60 | * @param {string} rawPattern Raw representation of the pattern in the source code. | |
61 | * @param {number} rawPatternStartRange Start range of the pattern in the source code. | |
62 | * @param {string} flags Regular expression flags. | |
63 | * @returns {void} | |
64 | * @private | |
65 | */ | |
66 | function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) { | |
67 | ||
68 | // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ '). | |
69 | if (!DOUBLE_SPACE.test(rawPattern)) { | |
70 | return; | |
71 | } | |
72 | ||
73 | const characterClassNodes = []; | |
74 | let regExpAST; | |
75 | ||
76 | try { | |
77 | regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u")); | |
d3726936 | 78 | } catch { |
eb39fafa DC |
79 | |
80 | // Ignore regular expressions with syntax errors | |
81 | return; | |
82 | } | |
83 | ||
84 | regexpp.visitRegExpAST(regExpAST, { | |
85 | onCharacterClassEnter(ccNode) { | |
86 | characterClassNodes.push(ccNode); | |
87 | } | |
88 | }); | |
89 | ||
90 | const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu; | |
91 | let match; | |
92 | ||
93 | while ((match = spacesPattern.exec(pattern))) { | |
94 | const { 1: { length }, index } = match; | |
95 | ||
96 | // Report only consecutive spaces that are not in character classes. | |
97 | if ( | |
98 | characterClassNodes.every(({ start, end }) => index < start || end <= index) | |
99 | ) { | |
100 | context.report({ | |
101 | node: nodeToReport, | |
102 | messageId: "multipleSpaces", | |
103 | data: { length }, | |
104 | fix(fixer) { | |
105 | if (pattern !== rawPattern) { | |
106 | return null; | |
107 | } | |
108 | return fixer.replaceTextRange( | |
109 | [rawPatternStartRange + index, rawPatternStartRange + index + length], | |
110 | ` {${length}}` | |
111 | ); | |
112 | } | |
113 | }); | |
114 | ||
115 | // Report only the first occurrence of consecutive spaces | |
116 | return; | |
117 | } | |
118 | } | |
119 | } | |
120 | ||
121 | /** | |
122 | * Validate regular expression literals | |
123 | * @param {ASTNode} node node to validate | |
124 | * @returns {void} | |
125 | * @private | |
126 | */ | |
127 | function checkLiteral(node) { | |
128 | if (node.regex) { | |
129 | const pattern = node.regex.pattern; | |
130 | const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/")); | |
131 | const rawPatternStartRange = node.range[0] + 1; | |
132 | const flags = node.regex.flags; | |
133 | ||
134 | checkRegex( | |
135 | node, | |
136 | pattern, | |
137 | rawPattern, | |
138 | rawPatternStartRange, | |
139 | flags | |
140 | ); | |
141 | } | |
142 | } | |
143 | ||
144 | /** | |
145 | * Validate strings passed to the RegExp constructor | |
146 | * @param {ASTNode} node node to validate | |
147 | * @returns {void} | |
148 | * @private | |
149 | */ | |
150 | function checkFunction(node) { | |
151 | const scope = context.getScope(); | |
152 | const regExpVar = astUtils.getVariableByName(scope, "RegExp"); | |
153 | const shadowed = regExpVar && regExpVar.defs.length > 0; | |
154 | const patternNode = node.arguments[0]; | |
155 | const flagsNode = node.arguments[1]; | |
156 | ||
157 | if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) { | |
158 | const pattern = patternNode.value; | |
159 | const rawPattern = patternNode.raw.slice(1, -1); | |
160 | const rawPatternStartRange = patternNode.range[0] + 1; | |
161 | const flags = isString(flagsNode) ? flagsNode.value : ""; | |
162 | ||
163 | checkRegex( | |
164 | node, | |
165 | pattern, | |
166 | rawPattern, | |
167 | rawPatternStartRange, | |
168 | flags | |
169 | ); | |
170 | } | |
171 | } | |
172 | ||
173 | return { | |
174 | Literal: checkLiteral, | |
175 | CallExpression: checkFunction, | |
176 | NewExpression: checkFunction | |
177 | }; | |
178 | } | |
179 | }; |