routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+345
@@ -0,0 +1,345 @@
|
||||
import {findVariable} from '@eslint-community/eslint-utils';
|
||||
import {isMethodCall} from './ast/index.js';
|
||||
import {
|
||||
isNodeMatches,
|
||||
isNodeValueNotFunction,
|
||||
isParenthesized,
|
||||
getParenthesizedRange,
|
||||
getParenthesizedText,
|
||||
shouldAddParenthesesToCallExpressionCallee,
|
||||
} from './utils/index.js';
|
||||
|
||||
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
|
||||
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
|
||||
const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
|
||||
const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
|
||||
const messages = {
|
||||
[ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
|
||||
[ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
|
||||
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
|
||||
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.',
|
||||
};
|
||||
|
||||
const isAwaitExpressionArgument = node => node.parent.type === 'AwaitExpression' && node.parent.argument === node;
|
||||
|
||||
const iteratorMethods = new Map([
|
||||
{
|
||||
method: 'every',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'filter',
|
||||
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'find',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findLast',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findIndex',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'findLastIndex',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'flatMap',
|
||||
},
|
||||
{
|
||||
method: 'forEach',
|
||||
returnsUndefined: true,
|
||||
},
|
||||
{
|
||||
method: 'map',
|
||||
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
|
||||
ignore: [
|
||||
'String',
|
||||
'Number',
|
||||
'BigInt',
|
||||
'Boolean',
|
||||
'Symbol',
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'reduce',
|
||||
parameters: [
|
||||
'accumulator',
|
||||
'element',
|
||||
'index',
|
||||
'array',
|
||||
],
|
||||
minParameters: 2,
|
||||
},
|
||||
{
|
||||
method: 'reduceRight',
|
||||
parameters: [
|
||||
'accumulator',
|
||||
'element',
|
||||
'index',
|
||||
'array',
|
||||
],
|
||||
minParameters: 2,
|
||||
},
|
||||
{
|
||||
method: 'some',
|
||||
ignore: [
|
||||
'Boolean',
|
||||
],
|
||||
},
|
||||
].map(({
|
||||
method,
|
||||
parameters = ['element', 'index', 'array'],
|
||||
ignore = [],
|
||||
minParameters = 1,
|
||||
returnsUndefined = false,
|
||||
shouldIgnoreCallExpression,
|
||||
}) => [method, {
|
||||
minParameters,
|
||||
parameters,
|
||||
returnsUndefined,
|
||||
shouldIgnoreCallExpression(callExpression) {
|
||||
if (
|
||||
method !== 'reduce'
|
||||
&& method !== 'reduceRight'
|
||||
&& isAwaitExpressionArgument(callExpression)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNodeMatches(callExpression.callee.object, ignoredCallee)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
callExpression.callee.object.type === 'CallExpression'
|
||||
&& isNodeMatches(callExpression.callee.object.callee, ignoredCallee)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shouldIgnoreCallExpression?.(callExpression) ?? false;
|
||||
},
|
||||
shouldIgnoreCallback(callback) {
|
||||
if (callback.type === 'Identifier' && ignore.includes(callback.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}]));
|
||||
|
||||
const ignoredCallee = [
|
||||
// http://bluebirdjs.com/docs/api/promise.map.html
|
||||
'Promise',
|
||||
'React.Children',
|
||||
'Children',
|
||||
'lodash',
|
||||
'underscore',
|
||||
'_',
|
||||
'Async',
|
||||
'async',
|
||||
'this',
|
||||
'$',
|
||||
'jQuery',
|
||||
];
|
||||
|
||||
function getProblem(context, node, method, options) {
|
||||
const {type} = node;
|
||||
|
||||
const name = type === 'Identifier' ? node.name : '';
|
||||
|
||||
const problem = {
|
||||
node,
|
||||
messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
|
||||
data: {
|
||||
name,
|
||||
method,
|
||||
},
|
||||
};
|
||||
|
||||
if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
|
||||
return problem;
|
||||
}
|
||||
|
||||
problem.suggest = [];
|
||||
|
||||
const {parameters, minParameters, returnsUndefined} = options;
|
||||
for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
|
||||
const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
|
||||
|
||||
const suggest = {
|
||||
messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
|
||||
data: {
|
||||
name,
|
||||
parameters: suggestionParameters,
|
||||
},
|
||||
fix(fixer) {
|
||||
let text = getParenthesizedText(node, context);
|
||||
|
||||
if (
|
||||
!isParenthesized(node, context)
|
||||
&& shouldAddParenthesesToCallExpressionCallee(node)
|
||||
) {
|
||||
text = `(${text})`;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
getParenthesizedRange(node, context),
|
||||
returnsUndefined
|
||||
? `(${suggestionParameters}) => { ${text}(${suggestionParameters}); }`
|
||||
: `(${suggestionParameters}) => ${text}(${suggestionParameters})`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
problem.suggest.push(suggest);
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
function * getTernaryConsequentAndALternate(node) {
|
||||
if (node.type === 'ConditionalExpression') {
|
||||
yield * getTernaryConsequentAndALternate(node.consequent);
|
||||
yield * getTernaryConsequentAndALternate(node.alternate);
|
||||
return;
|
||||
}
|
||||
|
||||
yield node;
|
||||
}
|
||||
|
||||
// These methods have dedicated type-predicate overloads in TypeScript's lib files.
|
||||
// Wrapping a type guard can lose narrowing, so direct references should be allowed here.
|
||||
const methodsWithTypePredicateOverloads = new Set([
|
||||
'every',
|
||||
'filter',
|
||||
'find',
|
||||
'findLast',
|
||||
]);
|
||||
|
||||
function hasTypePredicateReturnType(node) {
|
||||
return node.returnType?.typeAnnotation?.type === 'TSTypePredicate';
|
||||
}
|
||||
|
||||
function hasTypePredicateFunctionType(node) {
|
||||
return node.typeAnnotation?.typeAnnotation?.returnType?.typeAnnotation?.type === 'TSTypePredicate';
|
||||
}
|
||||
|
||||
function isTypePredicateCallback(callback, context) {
|
||||
if (callback.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep this local and syntax-based. Imported/member expressions need type-aware linting.
|
||||
const variable = findVariable(context.sourceCode.getScope(callback), callback);
|
||||
const definition = variable?.defs[0];
|
||||
|
||||
if (!definition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.type === 'FunctionName') {
|
||||
return hasTypePredicateReturnType(definition.node);
|
||||
}
|
||||
|
||||
// Imported callbacks may be type guards, but we can't inspect their predicate return
|
||||
// type without type-aware linting. Be conservative on methods with predicate overloads.
|
||||
if (definition.type === 'ImportBinding') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (definition.type === 'Parameter') {
|
||||
return hasTypePredicateFunctionType(definition.name);
|
||||
}
|
||||
|
||||
if (definition.type === 'Variable') {
|
||||
if (hasTypePredicateFunctionType(definition.node.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {init} = definition.node;
|
||||
return init
|
||||
&& (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
||||
&& hasTypePredicateReturnType(init);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('CallExpression', function * (callExpression) {
|
||||
if (
|
||||
!isMethodCall(callExpression, {
|
||||
minimumArguments: 1,
|
||||
maximumArguments: 2,
|
||||
optionalCall: false,
|
||||
computed: false,
|
||||
})
|
||||
|| callExpression.callee.property.type !== 'Identifier'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodNode = callExpression.callee.property;
|
||||
const methodName = methodNode.name;
|
||||
if (!iteratorMethods.has(methodName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = iteratorMethods.get(methodName);
|
||||
if (options.shouldIgnoreCallExpression(callExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const callback of getTernaryConsequentAndALternate(callExpression.arguments[0])) {
|
||||
if (
|
||||
callback.type === 'FunctionExpression'
|
||||
|| callback.type === 'ArrowFunctionExpression'
|
||||
// Ignore all `CallExpression`s, including `function.bind()`
|
||||
|| callback.type === 'CallExpression'
|
||||
|| options.shouldIgnoreCallback(callback)
|
||||
|| isNodeValueNotFunction(callback)
|
||||
|| (methodsWithTypePredicateOverloads.has(methodName) && isTypePredicateCallback(callback, context))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield getProblem(context, callback, methodName, options);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Prevent passing a function reference directly to iterator methods.',
|
||||
recommended: true,
|
||||
},
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user