You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
5.9 KiB
180 lines
5.9 KiB
/** |
|
* @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 |
|
}; |
|
} |
|
};
|
|
|