145 lines
3.6 KiB
JavaScript
145 lines
3.6 KiB
JavaScript
'use strict';
|
|
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils');
|
|
const {checkVueTemplate} = require('./utils/rule.js');
|
|
const {methodCallSelector} = require('./selectors/index.js');
|
|
const {isRegexLiteral, isNewExpression} = require('./ast/index.js');
|
|
const {isBooleanNode} = require('./utils/boolean.js');
|
|
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
|
|
|
|
const REGEXP_EXEC = 'regexp-exec';
|
|
const STRING_MATCH = 'string-match';
|
|
const SUGGESTION = 'suggestion';
|
|
const messages = {
|
|
[REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.',
|
|
[STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.',
|
|
[SUGGESTION]: 'Switch to `RegExp#test(…)`.',
|
|
};
|
|
|
|
const cases = [
|
|
{
|
|
type: REGEXP_EXEC,
|
|
selector: methodCallSelector({
|
|
method: 'exec',
|
|
argumentsLength: 1,
|
|
}),
|
|
getNodes: node => ({
|
|
stringNode: node.arguments[0],
|
|
methodNode: node.callee.property,
|
|
regexpNode: node.callee.object,
|
|
}),
|
|
fix: (fixer, {methodNode}) => fixer.replaceText(methodNode, 'test'),
|
|
},
|
|
{
|
|
type: STRING_MATCH,
|
|
selector: methodCallSelector({
|
|
method: 'match',
|
|
argumentsLength: 1,
|
|
}),
|
|
getNodes: node => ({
|
|
stringNode: node.callee.object,
|
|
methodNode: node.callee.property,
|
|
regexpNode: node.arguments[0],
|
|
}),
|
|
* fix(fixer, {stringNode, methodNode, regexpNode}, sourceCode) {
|
|
yield fixer.replaceText(methodNode, 'test');
|
|
|
|
let stringText = sourceCode.getText(stringNode);
|
|
if (
|
|
!isParenthesized(regexpNode, sourceCode)
|
|
// Only `SequenceExpression` need add parentheses
|
|
&& stringNode.type === 'SequenceExpression'
|
|
) {
|
|
stringText = `(${stringText})`;
|
|
}
|
|
|
|
yield fixer.replaceText(regexpNode, stringText);
|
|
|
|
let regexpText = sourceCode.getText(regexpNode);
|
|
if (
|
|
!isParenthesized(stringNode, sourceCode)
|
|
&& shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode)
|
|
) {
|
|
regexpText = `(${regexpText})`;
|
|
}
|
|
|
|
// The nodes that pass `isBooleanNode` cannot have an ASI problem.
|
|
|
|
yield fixer.replaceText(stringNode, regexpText);
|
|
},
|
|
},
|
|
];
|
|
|
|
const isRegExpNode = node => isRegexLiteral(node) || isNewExpression(node, {name: 'RegExp'});
|
|
|
|
const isRegExpWithoutGlobalFlag = (node, scope) => {
|
|
const staticResult = getStaticValue(node, scope);
|
|
|
|
// Don't know if there is `g` flag
|
|
if (!staticResult) {
|
|
return false;
|
|
}
|
|
|
|
const {value} = staticResult;
|
|
return (
|
|
Object.prototype.toString.call(value) === '[object RegExp]'
|
|
&& !value.global
|
|
);
|
|
};
|
|
|
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
const create = context => Object.fromEntries(
|
|
cases.map(checkCase => [
|
|
checkCase.selector,
|
|
node => {
|
|
if (!isBooleanNode(node)) {
|
|
return;
|
|
}
|
|
|
|
const {type, getNodes, fix} = checkCase;
|
|
const nodes = getNodes(node);
|
|
const {methodNode, regexpNode} = nodes;
|
|
|
|
if (regexpNode.type === 'Literal' && !regexpNode.regex) {
|
|
return;
|
|
}
|
|
|
|
const problem = {
|
|
node: type === REGEXP_EXEC ? methodNode : node,
|
|
messageId: type,
|
|
};
|
|
|
|
const fixFunction = fixer => fix(fixer, nodes, context.getSourceCode());
|
|
|
|
if (
|
|
isRegExpNode(regexpNode)
|
|
|| isRegExpWithoutGlobalFlag(regexpNode, context.getScope())
|
|
) {
|
|
problem.fix = fixFunction;
|
|
} else {
|
|
problem.suggest = [
|
|
{
|
|
messageId: SUGGESTION,
|
|
fix: fixFunction,
|
|
},
|
|
];
|
|
}
|
|
|
|
return problem;
|
|
},
|
|
]),
|
|
);
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
module.exports = {
|
|
create: checkVueTemplate(create),
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`.',
|
|
},
|
|
fixable: 'code',
|
|
hasSuggestions: true,
|
|
messages,
|
|
},
|
|
};
|