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