eslint-plugin-unicorn
Version:
More than 100 powerful ESLint rules
250 lines (225 loc) • 7.48 kB
JavaScript
import typedArray from './shared/typed-array.js';
import {removeMethodCall} from './fix/index.js';
import {
isNewExpression,
isMethodCall,
} from './ast/index.js';
const MESSAGE_ID_ITERABLE_ACCEPTING = 'iterable-accepting';
const MESSAGE_ID_FOR_OF = 'for-of';
const MESSAGE_ID_YIELD_STAR = 'yield-star';
const MESSAGE_ID_SPREAD = 'spread';
const MESSAGE_ID_ITERATOR_METHOD = 'iterator-method';
const MESSAGE_ID_SUGGESTION_ITERABLE_ACCEPTING = 'iterable-accepting/suggestion';
const MESSAGE_ID_SUGGESTION_ITERATOR_METHOD = 'iterator-method/suggestion';
const messages = {
[MESSAGE_ID_ITERABLE_ACCEPTING]: '`{{description}}` accepts an iterable, `.toArray()` is unnecessary.',
[MESSAGE_ID_FOR_OF]: '`for…of` can iterate over an iterable, `.toArray()` is unnecessary.',
[MESSAGE_ID_YIELD_STAR]: '`yield*` can delegate to an iterable, `.toArray()` is unnecessary.',
[MESSAGE_ID_SPREAD]: 'Spread works on iterables, `.toArray()` is unnecessary.',
[MESSAGE_ID_ITERATOR_METHOD]: '`Iterator` has a `.{{method}}()` method, `.toArray()` is unnecessary.',
[MESSAGE_ID_SUGGESTION_ITERABLE_ACCEPTING]: 'Remove `.toArray()`.',
[MESSAGE_ID_SUGGESTION_ITERATOR_METHOD]: 'Remove `.toArray()` and use `Iterator#{{method}}()`.',
};
// Iterator methods that share semantics with Array methods.
// Excluded: filter, flatMap, map — these return Iterator, not Array.
// Methods where Array accepts `thisArg` but Iterator does not:
const callbackOnlyIteratorMethods = [
'every',
'find',
'forEach',
'some',
];
// `reduce` — both Array and Iterator accept (callback, initialValue),
// but Array.reduce without initialValue uses the first element while
// Iterator.reduce without initialValue throws.
const reduceMethod = 'reduce';
const isToArrayCall = node => isMethodCall(node, {
method: 'toArray',
argumentsLength: 0,
optionalCall: false,
optionalMember: false,
});
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
// Case 1: `new Set(iterator.toArray())`, `new Map(iterator.toArray())`, etc.
context.on('NewExpression', node => {
if (
!(
isNewExpression(node, {names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1})
|| isNewExpression(node, {names: typedArray, minimumArguments: 1})
)
|| !isToArrayCall(node.arguments[0])
) {
return;
}
const toArrayCall = node.arguments[0];
return {
node: toArrayCall.callee.property,
messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
data: {description: `new ${node.callee.name}(…)`},
fix: fixer => removeMethodCall(fixer, toArrayCall, context),
};
});
// Case 2: Call expressions — static methods and iterator prototype methods.
context.on('CallExpression', node => {
// Case 2a: `Array.from(iterator.toArray())`, `TypedArray.from(…)`, `Object.fromEntries(…)`
// Autofix — these methods materialize their first argument into an array/object
// regardless of extra arguments (e.g. mapFn), so .toArray() is always unnecessary.
if (
(
isMethodCall(node, {
objects: ['Array', ...typedArray],
method: 'from',
minimumArguments: 1,
optionalCall: false,
optionalMember: false,
})
|| isMethodCall(node, {
object: 'Object',
method: 'fromEntries',
minimumArguments: 1,
optionalCall: false,
optionalMember: false,
})
)
&& isToArrayCall(node.arguments[0])
) {
const toArrayCall = node.arguments[0];
return {
node: toArrayCall.callee.property,
messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
fix: fixer => removeMethodCall(fixer, toArrayCall, context),
};
}
// Case 2b: `Promise.all(iterator.toArray())`, etc.
// Suggestion only — passing an iterator directly can change a sync throw
// into an async rejection when iteration fails.
if (
isMethodCall(node, {
object: 'Promise',
methods: ['all', 'allSettled', 'any', 'race'],
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& isToArrayCall(node.arguments[0])
) {
const toArrayCall = node.arguments[0];
return {
node: toArrayCall.callee.property,
messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION_ITERABLE_ACCEPTING,
fix: fixer => removeMethodCall(fixer, toArrayCall, context),
},
],
};
}
// Case 2c: `iterator.toArray().every(fn)`, `.find(fn)`, `.forEach(fn)`, `.some(fn)`, `.reduce(fn, init)`
// Suggestion only — Array callbacks receive a 3rd `array` argument
// (and `reduce` a 4th) that Iterator callbacks do not.
if (
(
isMethodCall(node, {
methods: callbackOnlyIteratorMethods,
maximumArguments: 1,
optionalCall: false,
optionalMember: false,
})
|| isMethodCall(node, {
method: reduceMethod,
argumentsLength: 2,
optionalCall: false,
optionalMember: false,
})
)
&& isToArrayCall(node.callee.object)
) {
// If the callback is a function/arrow with enough parameters to reference
// the `array` argument, `.toArray()` may be intentional.
const callback = node.arguments[0];
const isReduceCall = node.callee.property.name === reduceMethod;
const arrayParameterIndex = isReduceCall ? 3 : 2;
if (
callback
&& (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
&& callback.params.length > arrayParameterIndex
) {
return;
}
const callerObject = node.callee.object;
return {
node: callerObject.callee.property,
messageId: MESSAGE_ID_ITERATOR_METHOD,
data: {method: node.callee.property.name},
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION_ITERATOR_METHOD,
data: {method: node.callee.property.name},
fix: fixer => removeMethodCall(fixer, callerObject, context),
},
],
};
}
});
// Case 3: `for (const x of iterator.toArray())`
context.on('ForOfStatement', node => {
if (!isToArrayCall(node.right)) {
return;
}
return {
node: node.right.callee.property,
messageId: MESSAGE_ID_FOR_OF,
fix: fixer => removeMethodCall(fixer, node.right, context),
};
});
// Case 4: `yield* iterator.toArray()`
context.on('YieldExpression', node => {
if (!node.delegate || !isToArrayCall(node.argument)) {
return;
}
return {
node: node.argument.callee.property,
messageId: MESSAGE_ID_YIELD_STAR,
fix: fixer => removeMethodCall(fixer, node.argument, context),
};
});
// Case 5: `[...iterator.toArray()]`, `call(...iterator.toArray())`
// Spread works on iterables — `.toArray()` is unnecessary.
context.on('SpreadElement', node => {
if (!isToArrayCall(node.argument)) {
return;
}
const {parent} = node;
if (
parent.type !== 'ArrayExpression'
&& parent.type !== 'CallExpression'
&& parent.type !== 'NewExpression'
) {
return;
}
return {
node: node.argument.callee.property,
messageId: MESSAGE_ID_SPREAD,
fix: fixer => removeMethodCall(fixer, node.argument, context),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow unnecessary `.toArray()` on iterators.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;