Skip to content

Commit

Permalink
no-useless-spread: Check useless "iterable to array" (#1414)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Jul 13, 2021
1 parent c4bfc42 commit 61bc6a3
Show file tree
Hide file tree
Showing 8 changed files with 742 additions and 32 deletions.
60 changes: 56 additions & 4 deletions docs/rules/no-useless-spread.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
# Disallow useless spread

Using spread syntax in the following cases is unnecessary:
- Using spread syntax in the following cases is unnecessary:

- Spread an array literal as elements of an array literal
- Spread an array literal as arguments of a call or a `new` call
- Spread an object literal as properties of an object literal
- Spread an array literal as elements of an array literal
- Spread an array literal as arguments of a call or a `new` call
- Spread an object literal as properties of an object literal

- The following builtins accept an iterable, so it's unnecessary to convert the iterable to an array:

- `Map` constructor
- `WeakMap` constructor
- `Set` constructor
- `WeakSet` constructor
- `TypedArray` constructor
- `Array.from(…)`
- `TypedArray.from(…)`
- `Promise.{all,allSettled,any,race}(…)`
- `Object.fromEntries(…)`

- `for…of` loop can iterate over any iterable object not just array, so it's unnecessary to convert the iterable to an array.

- `yield*` can delegate to another iterable, so it's unnecessary to convert the iterable to an array.

This rule is fixable.

Expand All @@ -26,6 +42,24 @@ foo(firstArgument, ...[secondArgument], thirdArgument);
const object = new Foo(firstArgument, ...[secondArgument], thirdArgument);
```

```js
const set = new Set([...iterable]);
```

```js
const results = await Promise.all([...iterable]);
```

```js
for (const foo of [...set]);
```

```js
function * foo() {
yield * [...anotherGenerator()];
}
```

## Pass

```js
Expand Down Expand Up @@ -59,3 +93,21 @@ foo(foo, ...bar);
```js
const object = new Foo(...foo, bar);
```

```js
const set = new Set(iterable);
```

```js
const results = await Promise.all(iterable);
```

```js
for (const foo of set);
```

```js
function * foo() {
yield * anotherGenerator();
}
```
99 changes: 93 additions & 6 deletions rules/no-useless-spread.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
'use strict';
const {isCommaToken} = require('eslint-utils');
const {matches} = require('./selectors/index.js');
const {
matches,
newExpressionSelector,
methodCallSelector,
} = require('./selectors/index.js');
const {getParentheses} = require('./utils/parentheses.js');
const typedArray = require('./shared/typed-array.js');

const MESSAGE_ID = 'no-useless-spread';
const SPREAD_IN_LIST = 'spread-in-list';
const ITERABLE_TO_ARRAY = 'iterable-to-array';
const ITERABLE_TO_ARRAY_IN_FOR_OF = 'iterable-to-array-in-for-of';
const ITERABLE_TO_ARRAY_IN_YIELD_STAR = 'iterable-to-array-in-yield-star';
const messages = {
[MESSAGE_ID]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.',
[SPREAD_IN_LIST]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.',
[ITERABLE_TO_ARRAY]: '`{{parentDescription}}` accepts iterable as argument, it\'s unnecessary to convert to an array.',
[ITERABLE_TO_ARRAY_IN_FOR_OF]: '`for…of` can iterate over iterable, it\'s unnecessary to convert to an array.',
[ITERABLE_TO_ARRAY_IN_YIELD_STAR]: '`yield*` can delegate iterable, it\'s unnecessary to convert to an array.',
};

const selector = matches([
const uselessSpreadInListSelector = matches([
'ArrayExpression > SpreadElement.elements > ArrayExpression.argument',
'ObjectExpression > SpreadElement.properties > ObjectExpression.argument',
'CallExpression > SpreadElement.arguments > ArrayExpression.argument',
'NewExpression > SpreadElement.arguments > ArrayExpression.argument',
]);

const iterableToArraySelector = [
'ArrayExpression',
'[elements.length=1]',
'[elements.0.type="SpreadElement"]',
].join('');
const uselessIterableToArraySelector = matches([
[
matches([
newExpressionSelector({names: ['Map', 'WeakMap', 'Set', 'WeakSet'], length: 1}),
newExpressionSelector({names: typedArray, min: 1}),
methodCallSelector({object: 'Promise', names: ['all', 'allSettled', 'any', 'race'], length: 1}),
methodCallSelector({objects: ['Array', ...typedArray], name: 'from', length: 1}),
methodCallSelector({object: 'Object', name: 'fromEntries', length: 1}),
]),
' > ',
`${iterableToArraySelector}.arguments:first-child`,
].join(''),
`ForOfStatement > ${iterableToArraySelector}.right`,
`YieldExpression[delegate=true] > ${iterableToArraySelector}.argument`,
]);

const parentDescriptions = {
ArrayExpression: 'array literal',
ObjectExpression: 'object literal',
Expand Down Expand Up @@ -46,14 +78,14 @@ const create = context => {
const sourceCode = context.getSourceCode();

return {
[selector](spreadObject) {
[uselessSpreadInListSelector](spreadObject) {
const spreadElement = spreadObject.parent;
const spreadToken = sourceCode.getFirstToken(spreadElement);
const parentType = spreadElement.parent.type;

return {
node: spreadToken,
messageId: MESSAGE_ID,
messageId: SPREAD_IN_LIST,
data: {
argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object',
parentDescription: parentDescriptions[parentType],
Expand Down Expand Up @@ -108,6 +140,61 @@ const create = context => {
},
};
},
[uselessIterableToArraySelector](array) {
const {parent} = array;
let parentDescription = '';
let messageId = ITERABLE_TO_ARRAY;
switch (parent.type) {
case 'ForOfStatement':
messageId = ITERABLE_TO_ARRAY_IN_FOR_OF;
break;
case 'YieldExpression':
messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR;
break;
case 'NewExpression':
parentDescription = `new ${parent.callee.name}(…)`;
break;
case 'CallExpression':
parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`;
break;
// No default
}

return {
node: array,
messageId,
data: {parentDescription},
* fix(fixer) {
const [
openingBracketToken,
spreadToken,
] = sourceCode.getFirstTokens(array, 2);

// `[...iterable]`
// ^
yield fixer.remove(openingBracketToken);

// `[...iterable]`
// ^^^
yield fixer.remove(spreadToken);

const [
commaToken,
closingBracketToken,
] = sourceCode.getLastTokens(array, 2);

// `[...iterable]`
// ^
yield fixer.remove(closingBracketToken);

// `[...iterable,]`
// ^
if (isCommaToken(commaToken)) {
yield fixer.remove(commaToken);
}
},
};
},
};
};

Expand Down
13 changes: 2 additions & 11 deletions rules/prefer-negative-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
getNegativeIndexLengthNode,
removeLengthNode,
} = require('./shared/negative-index.js');
const typedArray = require('./shared/typed-array.js');

const MESSAGE_ID = 'prefer-negative-index';
const messages = {
Expand All @@ -19,17 +20,7 @@ const methods = new Map([
'Array',
'String',
'ArrayBuffer',
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array',
...typedArray,
// `{Blob,File}#slice()` are not generally used
// 'Blob'
// 'File'
Expand Down
16 changes: 16 additions & 0 deletions rules/shared/typed-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
module.exports = [
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array',
];
13 changes: 2 additions & 11 deletions rules/utils/builtins.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
'use strict';
const typedArray = require('../shared/typed-array.js');

const enforceNew = [
'Object',
'Array',
'ArrayBuffer',
'BigInt64Array',
'BigUint64Array',
'DataView',
'Date',
'Error',
'Float32Array',
'Float64Array',
'Function',
'Int8Array',
'Int16Array',
'Int32Array',
'Map',
'WeakMap',
'Set',
'WeakSet',
'Promise',
'RegExp',
'Uint8Array',
'Uint16Array',
'Uint32Array',
'Uint8ClampedArray',
...typedArray,
];

const disallowNew = [
Expand Down

0 comments on commit 61bc6a3

Please sign in to comment.