| /** |
| * @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("@eslint-community/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('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "Disallow multiple spaces in regular expressions", |
| recommended: true, |
| url: "https://eslint.org/docs/latest/rules/no-regex-spaces", |
| }, |
| |
| schema: [], |
| fixable: "code", |
| |
| messages: { |
| multipleSpaces: "Spaces are hard to count. Use {{{length}}}.", |
| }, |
| }, |
| |
| create(context) { |
| const sourceCode = context.sourceCode; |
| |
| /** |
| * 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, |
| { |
| unicode: flags.includes("u"), |
| unicodeSets: flags.includes("v"), |
| }, |
| ); |
| } 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 = sourceCode.getScope(node); |
| const regExpVar = astUtils.getVariableByName(scope, "RegExp"); |
| const shadowed = regExpVar && regExpVar.defs.length > 0; |
| const patternNode = node.arguments[0]; |
| |
| 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; |
| let flags; |
| |
| if (node.arguments.length < 2) { |
| // It has no flags. |
| flags = ""; |
| } else { |
| const flagsNode = node.arguments[1]; |
| |
| if (isString(flagsNode)) { |
| flags = flagsNode.value; |
| } else { |
| // The flags cannot be determined. |
| return; |
| } |
| } |
| |
| checkRegex( |
| node, |
| pattern, |
| rawPattern, |
| rawPatternStartRange, |
| flags, |
| ); |
| } |
| } |
| |
| return { |
| Literal: checkLiteral, |
| CallExpression: checkFunction, |
| NewExpression: checkFunction, |
| }; |
| }, |
| }; |