| /** | |
|  * @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('../shared/types').Rule} */ | |
| 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 | |
|         }; | |
|     } | |
| };
 |