| /** |
| * @fileoverview Rule to count multiple spaces in regular expressions |
| * @author Matt DuVall <http://www.mattduvall.com/> |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| const regexpp = require("regexpp"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const regExpParser = new regexpp.RegExpParser(); |
| const DOUBLE_SPACE = / {2}/u; |
| |
| /** |
| * Check if node is a string |
| * @param {ASTNode} node node to evaluate |
| * @returns {boolean} True if its a string |
| * @private |
| */ |
| function isString(node) { |
| return node && node.type === "Literal" && typeof node.value === "string"; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../shared/types').Rule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "disallow multiple spaces in regular expressions", |
| recommended: true, |
| url: "https://eslint.org/docs/rules/no-regex-spaces" |
| }, |
| |
| schema: [], |
| fixable: "code", |
| |
| messages: { |
| multipleSpaces: "Spaces are hard to count. Use {{{length}}}." |
| } |
| }, |
| |
| create(context) { |
| |
| /** |
| * Validate regular expression |
| * @param {ASTNode} nodeToReport Node to report. |
| * @param {string} pattern Regular expression pattern to validate. |
| * @param {string} rawPattern Raw representation of the pattern in the source code. |
| * @param {number} rawPatternStartRange Start range of the pattern in the source code. |
| * @param {string} flags Regular expression flags. |
| * @returns {void} |
| * @private |
| */ |
| function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) { |
| |
| // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ '). |
| if (!DOUBLE_SPACE.test(rawPattern)) { |
| return; |
| } |
| |
| const characterClassNodes = []; |
| let regExpAST; |
| |
| try { |
| regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u")); |
| } catch { |
| |
| // Ignore regular expressions with syntax errors |
| return; |
| } |
| |
| regexpp.visitRegExpAST(regExpAST, { |
| onCharacterClassEnter(ccNode) { |
| characterClassNodes.push(ccNode); |
| } |
| }); |
| |
| const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu; |
| let match; |
| |
| while ((match = spacesPattern.exec(pattern))) { |
| const { 1: { length }, index } = match; |
| |
| // Report only consecutive spaces that are not in character classes. |
| if ( |
| characterClassNodes.every(({ start, end }) => index < start || end <= index) |
| ) { |
| context.report({ |
| node: nodeToReport, |
| messageId: "multipleSpaces", |
| data: { length }, |
| fix(fixer) { |
| if (pattern !== rawPattern) { |
| return null; |
| } |
| return fixer.replaceTextRange( |
| [rawPatternStartRange + index, rawPatternStartRange + index + length], |
| ` {${length}}` |
| ); |
| } |
| }); |
| |
| // Report only the first occurrence of consecutive spaces |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Validate regular expression literals |
| * @param {ASTNode} node node to validate |
| * @returns {void} |
| * @private |
| */ |
| function checkLiteral(node) { |
| if (node.regex) { |
| const pattern = node.regex.pattern; |
| const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/")); |
| const rawPatternStartRange = node.range[0] + 1; |
| const flags = node.regex.flags; |
| |
| checkRegex( |
| node, |
| pattern, |
| rawPattern, |
| rawPatternStartRange, |
| flags |
| ); |
| } |
| } |
| |
| /** |
| * Validate strings passed to the RegExp constructor |
| * @param {ASTNode} node node to validate |
| * @returns {void} |
| * @private |
| */ |
| function checkFunction(node) { |
| const scope = context.getScope(); |
| const regExpVar = astUtils.getVariableByName(scope, "RegExp"); |
| const shadowed = regExpVar && regExpVar.defs.length > 0; |
| const patternNode = node.arguments[0]; |
| const flagsNode = node.arguments[1]; |
| |
| if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) { |
| const pattern = patternNode.value; |
| const rawPattern = patternNode.raw.slice(1, -1); |
| const rawPatternStartRange = patternNode.range[0] + 1; |
| const flags = isString(flagsNode) ? flagsNode.value : ""; |
| |
| checkRegex( |
| node, |
| pattern, |
| rawPattern, |
| rawPatternStartRange, |
| flags |
| ); |
| } |
| } |
| |
| return { |
| Literal: checkLiteral, |
| CallExpression: checkFunction, |
| NewExpression: checkFunction |
| }; |
| } |
| }; |