routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
import {isCommaToken} from '@eslint-community/eslint-utils';
|
||||
import typedArray from './shared/typed-array.js';
|
||||
import {removeParentheses, fixSpaceAroundKeyword, addParenthesizesToReturnOrThrowExpression} from './fix/index.js';
|
||||
import {isParenthesized, isOnSameLine} from './utils/index.js';
|
||||
import {
|
||||
isNewExpression,
|
||||
isMethodCall,
|
||||
isCallOrNewExpression,
|
||||
isEmptyArrayExpression,
|
||||
isEmptyObjectExpression,
|
||||
} from './ast/index.js';
|
||||
|
||||
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 CLONE_ARRAY = 'clone-array';
|
||||
const messages = {
|
||||
[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.',
|
||||
[CLONE_ARRAY]: 'Unnecessarily cloning an array.',
|
||||
};
|
||||
|
||||
const isSingleArraySpread = node =>
|
||||
node.type === 'ArrayExpression'
|
||||
&& node.elements.length === 1
|
||||
&& node.elements[0]?.type === 'SpreadElement';
|
||||
|
||||
const parentDescriptions = {
|
||||
ArrayExpression: 'array literal',
|
||||
ObjectExpression: 'object literal',
|
||||
CallExpression: 'arguments',
|
||||
NewExpression: 'arguments',
|
||||
};
|
||||
|
||||
function getCommaTokens(arrayExpression, sourceCode) {
|
||||
let startToken = sourceCode.getFirstToken(arrayExpression);
|
||||
|
||||
return arrayExpression.elements.map((element, index, elements) => {
|
||||
if (index === elements.length - 1) {
|
||||
const penultimateToken = sourceCode.getLastToken(arrayExpression, {skip: 1});
|
||||
if (isCommaToken(penultimateToken)) {
|
||||
return penultimateToken;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const commaToken = sourceCode.getTokenAfter(element || startToken, isCommaToken);
|
||||
startToken = commaToken;
|
||||
return commaToken;
|
||||
});
|
||||
}
|
||||
|
||||
function * unwrapSingleArraySpread(fixer, arrayExpression, context) {
|
||||
const {sourceCode} = context;
|
||||
const [
|
||||
openingBracketToken,
|
||||
spreadToken,
|
||||
thirdToken,
|
||||
] = sourceCode.getFirstTokens(arrayExpression, 3);
|
||||
|
||||
// `[...value]`
|
||||
// ^
|
||||
yield fixer.remove(openingBracketToken);
|
||||
|
||||
// `[...value]`
|
||||
// ^^^
|
||||
yield fixer.remove(spreadToken);
|
||||
|
||||
const [
|
||||
commaToken,
|
||||
closingBracketToken,
|
||||
] = sourceCode.getLastTokens(arrayExpression, 2);
|
||||
|
||||
// `[...value]`
|
||||
// ^
|
||||
yield fixer.remove(closingBracketToken);
|
||||
|
||||
// `[...value,]`
|
||||
// ^
|
||||
if (isCommaToken(commaToken)) {
|
||||
yield fixer.remove(commaToken);
|
||||
}
|
||||
|
||||
/*
|
||||
```js
|
||||
function foo() {
|
||||
return [
|
||||
...value,
|
||||
];
|
||||
}
|
||||
```
|
||||
*/
|
||||
const {parent} = arrayExpression;
|
||||
if (
|
||||
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
|
||||
&& parent.argument === arrayExpression
|
||||
&& !isOnSameLine(openingBracketToken, thirdToken, context)
|
||||
&& !isParenthesized(arrayExpression, context)
|
||||
) {
|
||||
yield addParenthesizesToReturnOrThrowExpression(fixer, parent, context);
|
||||
return;
|
||||
}
|
||||
|
||||
yield fixSpaceAroundKeyword(fixer, arrayExpression, context);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
const {sourceCode} = context;
|
||||
|
||||
// Useless spread in list
|
||||
context.on(['ArrayExpression', 'ObjectExpression'], node => {
|
||||
if (!(
|
||||
node.parent.type === 'SpreadElement'
|
||||
&& node.parent.argument === node
|
||||
&& (
|
||||
(
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.parent.parent.type === 'ObjectExpression'
|
||||
&& node.parent.parent.properties.includes(node.parent)
|
||||
)
|
||||
|| (
|
||||
node.type === 'ArrayExpression'
|
||||
&& (
|
||||
(
|
||||
node.parent.parent.type === 'ArrayExpression'
|
||||
&& node.parent.parent.elements.includes(node.parent)
|
||||
)
|
||||
|| (
|
||||
isCallOrNewExpression(node.parent.parent)
|
||||
&& node.parent.parent.arguments.includes(node.parent)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spreadObject = node;
|
||||
const spreadElement = spreadObject.parent;
|
||||
const spreadToken = sourceCode.getFirstToken(spreadElement);
|
||||
const parentType = spreadElement.parent.type;
|
||||
|
||||
return {
|
||||
node: spreadToken,
|
||||
messageId: SPREAD_IN_LIST,
|
||||
data: {
|
||||
argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object',
|
||||
parentDescription: parentDescriptions[parentType],
|
||||
},
|
||||
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
||||
* fix(fixer) {
|
||||
// `[...[foo]]`
|
||||
// ^^^
|
||||
yield fixer.remove(spreadToken);
|
||||
|
||||
// `[...(( [foo] ))]`
|
||||
// ^^ ^^
|
||||
yield removeParentheses(spreadObject, fixer, context);
|
||||
|
||||
// `[...[foo]]`
|
||||
// ^
|
||||
const firstToken = sourceCode.getFirstToken(spreadObject);
|
||||
yield fixer.remove(firstToken);
|
||||
|
||||
const [
|
||||
penultimateToken,
|
||||
lastToken,
|
||||
] = sourceCode.getLastTokens(spreadObject, 2);
|
||||
|
||||
// `[...[foo]]`
|
||||
// ^
|
||||
yield fixer.remove(lastToken);
|
||||
|
||||
// `[...[foo,]]`
|
||||
// ^
|
||||
if (isCommaToken(penultimateToken)) {
|
||||
yield fixer.remove(penultimateToken);
|
||||
}
|
||||
|
||||
// `[...[], 1]`
|
||||
// ^
|
||||
if (isEmptyArrayExpression(node) || isEmptyObjectExpression(node)) {
|
||||
const nextToken = sourceCode.getTokenAfter(spreadElement);
|
||||
if (isCommaToken(nextToken)) {
|
||||
yield fixer.remove(nextToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentType !== 'CallExpression' && parentType !== 'NewExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
const commaTokens = getCommaTokens(spreadObject, sourceCode);
|
||||
for (const [index, commaToken] of commaTokens.entries()) {
|
||||
if (spreadObject.elements[index]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// `call(...[foo, , bar])`
|
||||
// ^ Replace holes with `undefined`
|
||||
yield fixer.insertTextBefore(commaToken, 'undefined');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Useless iterable to array
|
||||
context.on('ArrayExpression', arrayExpression => {
|
||||
if (!isSingleArraySpread(arrayExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {parent} = arrayExpression;
|
||||
if (!(
|
||||
(parent.type === 'ForOfStatement' && parent.right === arrayExpression)
|
||||
|| (parent.type === 'YieldExpression' && parent.delegate && parent.argument === arrayExpression)
|
||||
|| (
|
||||
(
|
||||
isNewExpression(parent, {names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1})
|
||||
|| isNewExpression(parent, {names: typedArray, minimumArguments: 1})
|
||||
|| isMethodCall(parent, {
|
||||
object: 'Promise',
|
||||
methods: ['all', 'allSettled', 'any', 'race'],
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
|| isMethodCall(parent, {
|
||||
objects: ['Array', ...typedArray],
|
||||
method: 'from',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
|| isMethodCall(parent, {
|
||||
object: 'Object',
|
||||
method: 'fromEntries',
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
)
|
||||
&& parent.arguments[0] === arrayExpression
|
||||
)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: arrayExpression,
|
||||
messageId,
|
||||
data: {parentDescription},
|
||||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, context),
|
||||
};
|
||||
});
|
||||
|
||||
// Useless array clone
|
||||
context.on('ArrayExpression', arrayExpression => {
|
||||
if (!isSingleArraySpread(arrayExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = arrayExpression.elements[0].argument;
|
||||
if (!(
|
||||
// Array methods returns a new array, but exclude these also exists in `Iterator`
|
||||
// `filter`, `flatMap`, and `map`
|
||||
isMethodCall(node, {
|
||||
methods: [
|
||||
'concat',
|
||||
'copyWithin',
|
||||
'flat',
|
||||
'slice',
|
||||
'splice',
|
||||
'toReversed',
|
||||
'toSorted',
|
||||
'toSpliced',
|
||||
'with',
|
||||
],
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
// `String#split()`
|
||||
|| isMethodCall(node, {
|
||||
method: 'split',
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
// `Object.keys()` and `Object.values()`
|
||||
|| isMethodCall(node, {
|
||||
object: 'Object',
|
||||
methods: ['keys', 'values'],
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
// `await Promise.all()` and `await Promise.allSettled`
|
||||
|| (
|
||||
node.type === 'AwaitExpression'
|
||||
&& isMethodCall(node.argument, {
|
||||
object: 'Promise',
|
||||
methods: ['all', 'allSettled'],
|
||||
argumentsLength: 1,
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
)
|
||||
// `Array.from()`, `Array.of()`
|
||||
|| isMethodCall(node, {
|
||||
object: 'Array',
|
||||
methods: ['from', 'of'],
|
||||
optionalCall: false,
|
||||
optionalMember: false,
|
||||
})
|
||||
// `new Array()`
|
||||
|| isNewExpression(node, {name: 'Array'})
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const problem = {
|
||||
node: arrayExpression,
|
||||
messageId: CLONE_ARRAY,
|
||||
};
|
||||
|
||||
if (
|
||||
// `[...new Array(1)]` -> `new Array(1)` is not safe to fix since there are holes
|
||||
isNewExpression(node, {name: 'Array'})
|
||||
// `[...foo.slice(1)]` -> `foo.slice(1)` is not safe to fix since `foo` can be a string
|
||||
|| (
|
||||
node.type === 'CallExpression'
|
||||
&& node.callee.type === 'MemberExpression'
|
||||
&& node.callee.property.type === 'Identifier'
|
||||
&& node.callee.property.name === 'slice'
|
||||
)
|
||||
) {
|
||||
return problem;
|
||||
}
|
||||
|
||||
return Object.assign(problem, {
|
||||
fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, context),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow unnecessary spread.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user