Skip to content

Commit

Permalink
Refactor to avoid use complex selectors (#2101)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed May 16, 2023
1 parent d253cb1 commit 2684c62
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 112 deletions.
37 changes: 26 additions & 11 deletions rules/prefer-array-flat-map.js
@@ -1,27 +1,42 @@
'use strict';
const {isNodeMatches} = require('./utils/is-node-matches.js');
const {methodCallSelector, matches} = require('./selectors/index.js');
const {isMethodCall} = require('./ast/index.js');
const {removeMethodCall} = require('./fix/index.js');

const MESSAGE_ID = 'prefer-array-flat-map';
const messages = {
[MESSAGE_ID]: 'Prefer `.flatMap(…)` over `.map(…).flat()`.',
};

const selector = [
methodCallSelector('flat'),
matches([
'[arguments.length=0]',
'[arguments.length=1][arguments.0.type="Literal"][arguments.0.raw="1"]',
]),
methodCallSelector({path: 'callee.object', method: 'map'}),
].join('');

const ignored = ['React.Children', 'Children'];

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[selector](flatCallExpression) {
CallExpression(callExpression) {
if (!(
isMethodCall(callExpression, {
method: 'flat',
optionalCall: false,
optionalMember: false,
})
&& (
callExpression.arguments.length === 0
|| (
callExpression.arguments.length === 1
&& callExpression.arguments[0].type === 'Literal'
&& callExpression.arguments[0].raw === '1'
)
)
&& isMethodCall(callExpression.callee.object, {
method: 'map',
optionalCall: false,
optionalMember: false,
})
)) {
return;
}

const flatCallExpression = callExpression;
const mapCallExpression = flatCallExpression.callee.object;
if (isNodeMatches(mapCallExpression.callee.object, ignored)) {
return;
Expand Down
79 changes: 45 additions & 34 deletions rules/prefer-code-point.js
@@ -1,46 +1,57 @@
'use strict';
const {methodCallSelector} = require('./selectors/index.js');
const {isMethodCall} = require('./ast/index.js');

const messages = {
'error/charCodeAt': 'Prefer `String#codePointAt()` over `String#charCodeAt()`.',
'error/fromCharCode': 'Prefer `String.fromCodePoint()` over `String.fromCharCode()`.',
'suggestion/charCodeAt': 'Use `String#codePointAt()`.',
'suggestion/fromCharCode': 'Use `String.fromCodePoint()`.',
'suggestion/codePointAt': 'Use `String#codePointAt()`.',
'suggestion/fromCodePoint': 'Use `String.fromCodePoint()`.',
};

const cases = [
{
selector: methodCallSelector('charCodeAt'),
replacement: 'codePointAt',
},
{
selector: methodCallSelector({object: 'String', method: 'fromCharCode'}),
replacement: 'fromCodePoint',
},
];
const getReplacement = node => {
if (isMethodCall(node, {
method: 'charCodeAt',
optionalCall: false,
optionalMember: false,
})) {
return 'codePointAt';
}

if (isMethodCall(node, {
object: 'String',
method: 'fromCharCode',
optionalCall: false,
optionalMember: false,
})) {
return 'fromCodePoint';
}
};

/** @param {import('eslint').Rule.RuleContext} context */
const create = () => Object.fromEntries(
cases.map(({selector, replacement}) => [
selector,
node => {
const method = node.callee.property;
const methodName = method.name;
const fix = fixer => fixer.replaceText(method, replacement);

return {
node: method,
messageId: `error/${methodName}`,
suggest: [
{
messageId: `suggestion/${methodName}`,
fix,
},
],
};
},
]),
);
const create = () => ({
CallExpression(node) {
const replacement = getReplacement(node);

if (!replacement) {
return;
}

const method = node.callee.property;
const methodName = method.name;
const fix = fixer => fixer.replaceText(method, replacement);

return {
node: method,
messageId: `error/${methodName}`,
suggest: [
{
messageId: `suggestion/${replacement}`,
fix,
},
],
};
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
Expand Down
19 changes: 11 additions & 8 deletions rules/prefer-string-replace-all.js
Expand Up @@ -2,8 +2,7 @@
const {getStaticValue} = require('@eslint-community/eslint-utils');
const {parse: parseRegExp} = require('regjsparser');
const escapeString = require('./utils/escape-string.js');
const {methodCallSelector} = require('./selectors/index.js');
const {isRegexLiteral, isNewExpression} = require('./ast/index.js');
const {isRegexLiteral, isNewExpression, isMethodCall} = require('./ast/index.js');

const MESSAGE_ID_USE_REPLACE_ALL = 'method';
const MESSAGE_ID_USE_STRING = 'pattern';
Expand All @@ -12,11 +11,6 @@ const messages = {
[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
};

const selector = methodCallSelector({
methods: ['replace', 'replaceAll'],
argumentsLength: 2,
});

function getPatternReplacement(node) {
if (!isRegexLiteral(node)) {
return;
Expand Down Expand Up @@ -80,7 +74,16 @@ const isRegExpWithGlobalFlag = (node, scope) => {

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[selector](node) {
CallExpression(node) {
if (!isMethodCall(node, {
methods: ['replace', 'replaceAll'],
argumentsLength: 2,
optionalCall: false,
optionalMember: false,
})) {
return;
}

const {
arguments: [pattern],
callee: {property},
Expand Down
15 changes: 6 additions & 9 deletions rules/prefer-string-slice.js
@@ -1,10 +1,9 @@
'use strict';
const {getStaticValue} = require('@eslint-community/eslint-utils');
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');
const {methodCallSelector} = require('./selectors/index.js');
const isNumber = require('./utils/is-number.js');
const {replaceArgument} = require('./fix/index.js');
const {isNumberLiteral} = require('./ast/index.js');
const {isNumberLiteral, isMethodCall} = require('./ast/index.js');

const MESSAGE_ID_SUBSTR = 'substr';
const MESSAGE_ID_SUBSTRING = 'substring';
Expand All @@ -13,12 +12,6 @@ const messages = {
[MESSAGE_ID_SUBSTRING]: 'Prefer `String#slice()` over `String#substring()`.',
};

const selector = methodCallSelector({
methods: ['substr', 'substring'],
includeOptionalMember: true,
includeOptionalCall: true,
});

const getNumericValue = node => {
if (isNumberLiteral(node)) {
return node.value;
Expand Down Expand Up @@ -144,7 +137,11 @@ function * fixSubstringArguments({node, fixer, context, abort}) {

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[selector](node) {
CallExpression(node) {
if (!isMethodCall(node, {methods: ['substr', 'substring']})) {
return;
}

const method = node.callee.property.name;

return {
Expand Down
21 changes: 14 additions & 7 deletions rules/prefer-string-starts-ends-with.js
@@ -1,10 +1,10 @@
'use strict';
const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils');
const {methodCallSelector} = require('./selectors/index.js');
const escapeString = require('./utils/escape-string.js');
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
const shouldAddParenthesesToLogicalExpressionChild = require('./utils/should-add-parentheses-to-logical-expression-child.js');
const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');
const {isMethodCall, isRegexLiteral} = require('./ast/index.js');

const MESSAGE_STARTS_WITH = 'prefer-starts-with';
const MESSAGE_ENDS_WITH = 'prefer-ends-with';
Expand All @@ -26,11 +26,6 @@ const isSimpleString = string => doesNotContain(
);
const addParentheses = text => `(${text})`;

const regexTestSelector = [
methodCallSelector({method: 'test', argumentsLength: 1}),
'[callee.object.regex]',
].join('');

const checkRegex = ({pattern, flags}) => {
if (flags.includes('i') || flags.includes('m')) {
return;
Expand Down Expand Up @@ -64,7 +59,19 @@ const create = context => {
const {sourceCode} = context;

return {
[regexTestSelector](node) {
CallExpression(node) {
if (
!isMethodCall(node, {
method: 'test',
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
|| !isRegexLiteral(node.callee.object)
) {
return;
}

const regexNode = node.callee.object;
const {regex} = regexNode;
const result = checkRegex(regex);
Expand Down
23 changes: 11 additions & 12 deletions rules/prefer-string-trim-start-end.js
@@ -1,24 +1,23 @@
'use strict';
const {methodCallSelector} = require('./selectors/index.js');
const {isMethodCall} = require('./ast/index.js');

const MESSAGE_ID = 'prefer-string-trim-start-end';
const messages = {
[MESSAGE_ID]: 'Prefer `String#{{replacement}}()` over `String#{{method}}()`.',
};

const selector = [
methodCallSelector({
methods: ['trimLeft', 'trimRight'],
argumentsLength: 0,
includeOptionalMember: true,
}),
' > .callee',
' > .property',
].join(' ');

/** @param {import('eslint').Rule.RuleContext} context */
const create = () => ({
[selector](node) {
CallExpression(callExpression) {
if (!isMethodCall(callExpression, {
methods: ['trimLeft', 'trimRight'],
argumentsLength: 0,
optionalCall: false,
})) {
return;
}

const node = callExpression.callee.property;
const method = node.name;
const replacement = method === 'trimLeft' ? 'trimStart' : 'trimEnd';

Expand Down
60 changes: 31 additions & 29 deletions rules/require-number-to-fixed-digits-argument.js
@@ -1,42 +1,44 @@
'use strict';
const {methodCallSelector, not} = require('./selectors/index.js');
const {appendArgument} = require('./fix/index.js');
const {isMethodCall} = require('./ast/index.js');

const MESSAGE_ID = 'require-number-to-fixed-digits-argument';
const messages = {
[MESSAGE_ID]: 'Missing the digits argument.',
};

const mathToFixed = [
methodCallSelector({
method: 'toFixed',
argumentsLength: 0,
}),
not('[callee.object.type="NewExpression"]'),
].join('');

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
return {
[mathToFixed](node) {
const [
openingParenthesis,
closingParenthesis,
] = sourceCode.getLastTokens(node, 2);
const create = context => ({
CallExpression(node) {
if (
!isMethodCall(node, {
method: 'toFixed',
argumentsLength: 0,
optionalCall: false,
optionalMember: false,
})
|| node.callee.object.type === 'NewExpression'
) {
return;
}

return {
loc: {
start: openingParenthesis.loc.start,
end: closingParenthesis.loc.end,
},
messageId: MESSAGE_ID,
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => appendArgument(fixer, node, '0', sourceCode),
};
},
};
};
const {sourceCode} = context;
const [
openingParenthesis,
closingParenthesis,
] = sourceCode.getLastTokens(node, 2);

return {
loc: {
start: openingParenthesis.loc.start,
end: closingParenthesis.loc.end,
},
messageId: MESSAGE_ID,
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => appendArgument(fixer, node, '0', sourceCode),
};
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
Expand Down

0 comments on commit 2684c62

Please sign in to comment.