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