1 | /**
|
2 | * @fileoverview Rule to count multiple spaces in regular expressions
|
3 | * @author Matt DuVall <http://www.mattduvall.com/>
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | const astUtils = require("./utils/ast-utils");
|
9 |
|
10 | //------------------------------------------------------------------------------
|
11 | // Rule Definition
|
12 | //------------------------------------------------------------------------------
|
13 |
|
14 | module.exports = {
|
15 | meta: {
|
16 | type: "suggestion",
|
17 |
|
18 | docs: {
|
19 | description: "disallow multiple spaces in regular expressions",
|
20 | category: "Possible Errors",
|
21 | recommended: true,
|
22 | url: "https://eslint.org/docs/rules/no-regex-spaces"
|
23 | },
|
24 |
|
25 | schema: [],
|
26 | fixable: "code"
|
27 | },
|
28 |
|
29 | create(context) {
|
30 | const sourceCode = context.getSourceCode();
|
31 |
|
32 | /**
|
33 | * Validate regular expressions
|
34 | * @param {ASTNode} node node to validate
|
35 | * @param {string} value regular expression to validate
|
36 | * @param {number} valueStart The start location of the regex/string literal. It will always be the case that
|
37 | * `sourceCode.getText().slice(valueStart, valueStart + value.length) === value`
|
38 | * @returns {void}
|
39 | * @private
|
40 | */
|
41 | function checkRegex(node, value, valueStart) {
|
42 | const multipleSpacesRegex = /( {2,})( [+*{?]|[^+*{?]|$)/u,
|
43 | regexResults = multipleSpacesRegex.exec(value);
|
44 |
|
45 | if (regexResults !== null) {
|
46 | const count = regexResults[1].length;
|
47 |
|
48 | context.report({
|
49 | node,
|
50 | message: "Spaces are hard to count. Use {{{count}}}.",
|
51 | data: { count },
|
52 | fix(fixer) {
|
53 | return fixer.replaceTextRange(
|
54 | [valueStart + regexResults.index, valueStart + regexResults.index + count],
|
55 | ` {${count}}`
|
56 | );
|
57 | }
|
58 | });
|
59 |
|
60 | /*
|
61 | * TODO: (platinumazure) Fix message to use rule message
|
62 | * substitution when api.report is fixed in lib/eslint.js.
|
63 | */
|
64 | }
|
65 | }
|
66 |
|
67 | /**
|
68 | * Validate regular expression literals
|
69 | * @param {ASTNode} node node to validate
|
70 | * @returns {void}
|
71 | * @private
|
72 | */
|
73 | function checkLiteral(node) {
|
74 | const token = sourceCode.getFirstToken(node),
|
75 | nodeType = token.type,
|
76 | nodeValue = token.value;
|
77 |
|
78 | if (nodeType === "RegularExpression") {
|
79 | checkRegex(node, nodeValue, token.range[0]);
|
80 | }
|
81 | }
|
82 |
|
83 | /**
|
84 | * Check if node is a string
|
85 | * @param {ASTNode} node node to evaluate
|
86 | * @returns {boolean} True if its a string
|
87 | * @private
|
88 | */
|
89 | function isString(node) {
|
90 | return node && node.type === "Literal" && typeof node.value === "string";
|
91 | }
|
92 |
|
93 | /**
|
94 | * Validate strings passed to the RegExp constructor
|
95 | * @param {ASTNode} node node to validate
|
96 | * @returns {void}
|
97 | * @private
|
98 | */
|
99 | function checkFunction(node) {
|
100 | const scope = context.getScope();
|
101 | const regExpVar = astUtils.getVariableByName(scope, "RegExp");
|
102 | const shadowed = regExpVar && regExpVar.defs.length > 0;
|
103 |
|
104 | if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0]) && !shadowed) {
|
105 | checkRegex(node, node.arguments[0].value, node.arguments[0].range[0] + 1);
|
106 | }
|
107 | }
|
108 |
|
109 | return {
|
110 | Literal: checkLiteral,
|
111 | CallExpression: checkFunction,
|
112 | NewExpression: checkFunction
|
113 | };
|
114 |
|
115 | }
|
116 | };
|