eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
210 lines (176 loc) • 6.33 kB
JavaScript
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
import {functionTypes} from './ast/index.js';
import {getFunctionReturnExpression, unwrapTypeScriptExpression} from './utils/index.js';
const MESSAGE_ID = 'prefer-native-coercion-functions';
const MESSAGE_ID_SUGGESTION = 'prefer-native-coercion-functions/suggestion';
const messages = {
[MESSAGE_ID]: '{{functionNameWithKind}} is equivalent to `{{replacementFunction}}`. Use `{{replacementFunction}}` directly.',
[MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacementFunction}}`.',
};
const nativeCoercionFunctionNames = new Set(['String', 'Number', 'BigInt', 'Boolean', 'Symbol']);
const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findLast', 'findIndex', 'findLastIndex', 'some']);
const isNativeCoercionFunctionCall = (node, firstArgumentName) =>
node?.type === 'CallExpression'
&& !node.optional
&& node.callee.type === 'Identifier'
&& nativeCoercionFunctionNames.has(node.callee.name)
&& node.arguments[0]?.type === 'Identifier'
&& node.arguments[0].name === firstArgumentName;
// `v => value` or `function (v) {return value;}`, with TypeScript expression wrappers around the value removed
const getReturnedExpression = node => unwrapTypeScriptExpression(getFunctionReturnExpression(node));
// `v => v`
const isIdentityFunction = node => {
const returnedExpression = getReturnedExpression(node);
return returnedExpression?.type === 'Identifier'
&& returnedExpression.name === node.params[0].name;
};
const isArrayIdentityCallback = node =>
isIdentityFunction(node)
&& node.parent.type === 'CallExpression'
&& !node.parent.optional
&& node.parent.arguments[0] === node
&& node.parent.callee.type === 'MemberExpression'
&& !node.parent.callee.computed
&& !node.parent.callee.optional
&& node.parent.callee.property.type === 'Identifier'
&& arrayMethodsWithBooleanCallback.has(node.parent.callee.property.name);
const isTypeScriptTypePredicateFunction = node =>
node.returnType?.type === 'TSTypeAnnotation'
&& node.returnType.typeAnnotation.type === 'TSTypePredicate';
// `v => String(v)`
function getCallExpression(node) {
const returnedExpression = getReturnedExpression(node);
if (isNativeCoercionFunctionCall(returnedExpression, node.params[0].name)) {
return returnedExpression;
}
}
function getArrayCallbackProblem(node) {
if (!isArrayIdentityCallback(node)) {
return;
}
return {
replacementFunction: 'Boolean',
fix: fixer => fixer.replaceText(node, 'Boolean'),
};
}
// The tokens that would continue the expression the replacement ends with. A regular expression is matched on its type, since its `value` is the whole literal.
const expressionContinuingTokens = new Set(['(', '[', '+', '-']);
const expressionContinuingTokenTypes = new Set(['RegularExpression', 'Template']);
// The replacement ends with a bare identifier, so a `(`, `[`, `+`, `-`, regular expression, or template that ASI separated from the function on the next line would be absorbed into it. A token inside the parent already belongs to the same expression, like the arguments of a called function expression, so it keeps its meaning.
const needsSemicolonAfter = (node, sourceCode) => {
const nextToken = sourceCode.getTokenAfter(node);
return nextToken !== null
&& sourceCode.getRange(nextToken)[0] >= sourceCode.getRange(node.parent)[1]
&& (
expressionContinuingTokens.has(nextToken.value)
|| expressionContinuingTokenTypes.has(nextToken.type)
);
};
function getCoercionFunctionProblem(node, context) {
const callExpression = getCallExpression(node);
if (!callExpression) {
return;
}
const {name} = callExpression.callee;
const problem = {replacementFunction: name};
if (node.type === 'FunctionDeclaration' || callExpression.arguments.length !== 1) {
return problem;
}
/**
@param {import('eslint').Rule.RuleFixer} fixer
*/
problem.fix = fixer => {
let text = name;
if (
node.parent.type === 'Property'
&& node.parent.method
&& node.parent.value === node
) {
text = `: ${text}`;
} else if (node.parent.type === 'MethodDefinition') {
text = ` = ${text};`;
} else if (needsSemicolonAfter(node, context.sourceCode)) {
text += ';';
}
return fixer.replaceText(node, text);
};
return problem;
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on(functionTypes, node => {
if (
node.async
|| node.generator
|| isTypeScriptTypePredicateFunction(node)
|| node.params.length === 0
|| node.params[0].type !== 'Identifier'
|| (
(
(
node.parent.type === 'MethodDefinition'
&& (node.parent.kind === 'constructor' || node.parent.kind === 'set')
)
|| (node.parent.type === 'Property' && node.parent.kind === 'set')
)
&& node.parent.value === node
)
) {
return;
}
let problem = getArrayCallbackProblem(node) || getCoercionFunctionProblem(node, context);
if (!problem) {
return;
}
const {sourceCode} = context;
const {replacementFunction, fix} = problem;
problem = {
node,
loc: getFunctionHeadLocation(node, sourceCode),
messageId: MESSAGE_ID,
data: {
functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
replacementFunction,
},
};
/*
We do not fix if there are:
- Comments: No proper place to put them.
- Extra parameters: Removing them may break types.
*/
if (!fix || node.params.length !== 1 || sourceCode.getCommentsInside(node).length > 0) {
return problem;
}
// Rewriting a class method into a class field moves it off the prototype, which changes property lookup, enumerability and `super` dispatch, so that can only be a suggestion.
if (node.parent.type === 'MethodDefinition') {
problem.suggest = [
{messageId: MESSAGE_ID_SUGGESTION, data: {replacementFunction}, fix},
];
return problem;
}
problem.fix = fix;
return problem;
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
languages: [
'js/js',
],
},
};
export default config;