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