UNPKG

eslint-plugin-unicorn

Version:
250 lines (225 loc) 7.48 kB
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;