123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- 'use strict';
- const {isParenthesized, getStaticValue, isCommaToken, hasSideEffect} = require('eslint-utils');
- const {methodCallSelector, not} = require('./selectors/index.js');
- const needsSemicolon = require('./utils/needs-semicolon.js');
- const {getParenthesizedRange, getParenthesizedText} = require('./utils/parentheses.js');
- const shouldAddParenthesesToSpreadElementArgument = require('./utils/should-add-parentheses-to-spread-element-argument.js');
- const isLiteralValue = require('./utils/is-literal-value.js');
- const {isNodeMatches} = require('./utils/is-node-matches.js');
- const {
- replaceNodeOrTokenAndSpacesBefore,
- removeSpacesAfter,
- removeMethodCall,
- } = require('./fix/index.js');
- const ERROR_ARRAY_FROM = 'array-from';
- const ERROR_ARRAY_CONCAT = 'array-concat';
- const ERROR_ARRAY_SLICE = 'array-slice';
- const ERROR_STRING_SPLIT = 'string-split';
- const SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE = 'argument-is-spreadable';
- const SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE = 'argument-is-not-spreadable';
- const SUGGESTION_CONCAT_TEST_ARGUMENT = 'test-argument';
- const SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS = 'spread-all-arguments';
- const SUGGESTION_USE_SPREAD = 'use-spread';
- const messages = {
- [ERROR_ARRAY_FROM]: 'Prefer the spread operator over `Array.from(…)`.',
- [ERROR_ARRAY_CONCAT]: 'Prefer the spread operator over `Array#concat(…)`.',
- [ERROR_ARRAY_SLICE]: 'Prefer the spread operator over `Array#slice()`.',
- [ERROR_STRING_SPLIT]: 'Prefer the spread operator over `String#split(\'\')`.',
- [SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE]: 'First argument is an `array`.',
- [SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE]: 'First argument is not an `array`.',
- [SUGGESTION_CONCAT_TEST_ARGUMENT]: 'Test first argument with `Array.isArray(…)`.',
- [SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS]: 'Spread all unknown arguments`.',
- [SUGGESTION_USE_SPREAD]: 'Use `...` operator.',
- };
- const arrayFromCallSelector = [
- methodCallSelector({
- object: 'Array',
- method: 'from',
- minimumArguments: 1,
- maximumArguments: 3,
- }),
- // Allow `Array.from({length})`
- '[arguments.0.type!="ObjectExpression"]',
- ].join('');
- const arrayConcatCallSelector = [
- methodCallSelector('concat'),
- not(
- [
- 'Literal',
- 'TemplateLiteral',
- ].map(type => `[callee.object.type="${type}"]`),
- ),
- ].join('');
- const arraySliceCallSelector = [
- methodCallSelector({
- method: 'slice',
- minimumArguments: 0,
- maximumArguments: 1,
- }),
- '[callee.object.type!="ArrayExpression"]',
- ].join('');
- const ignoredSliceCallee = [
- 'arrayBuffer',
- 'blob',
- 'buffer',
- 'file',
- 'this',
- ];
- const stringSplitCallSelector = methodCallSelector({
- method: 'split',
- argumentsLength: 1,
- });
- const isArrayLiteral = node => node.type === 'ArrayExpression';
- const isArrayLiteralHasTrailingComma = (node, sourceCode) => {
- if (node.elements.length === 0) {
- return false;
- }
- return isCommaToken(sourceCode.getLastToken(node, 1));
- };
- function fixConcat(node, sourceCode, fixableArguments) {
- const array = node.callee.object;
- const concatCallArguments = node.arguments;
- const arrayParenthesizedRange = getParenthesizedRange(array, sourceCode);
- const arrayIsArrayLiteral = isArrayLiteral(array);
- const arrayHasTrailingComma = arrayIsArrayLiteral && isArrayLiteralHasTrailingComma(array, sourceCode);
- const getArrayLiteralElementsText = (node, keepTrailingComma) => {
- if (
- !keepTrailingComma
- && isArrayLiteralHasTrailingComma(node, sourceCode)
- ) {
- const start = node.range[0] + 1;
- const end = sourceCode.getLastToken(node, 1).range[0];
- return sourceCode.text.slice(start, end);
- }
- return sourceCode.getText(node, -1, -1);
- };
- const getFixedText = () => {
- const nonEmptyArguments = fixableArguments
- .filter(({node, isArrayLiteral}) => (!isArrayLiteral || node.elements.length > 0));
- const lastArgument = nonEmptyArguments[nonEmptyArguments.length - 1];
- let text = nonEmptyArguments
- .map(({node, isArrayLiteral, isSpreadable, testArgument}) => {
- if (isArrayLiteral) {
- return getArrayLiteralElementsText(node, node === lastArgument.node);
- }
- let text = getParenthesizedText(node, sourceCode);
- if (testArgument) {
- return `...(Array.isArray(${text}) ? ${text} : [${text}])`;
- }
- if (isSpreadable) {
- if (
- !isParenthesized(node, sourceCode)
- && shouldAddParenthesesToSpreadElementArgument(node)
- ) {
- text = `(${text})`;
- }
- text = `...${text}`;
- }
- return text || ' ';
- })
- .join(', ');
- if (!text) {
- return '';
- }
- if (arrayIsArrayLiteral) {
- if (array.elements.length > 0) {
- text = ` ${text}`;
- if (!arrayHasTrailingComma) {
- text = `,${text}`;
- }
- if (
- arrayHasTrailingComma
- && (!lastArgument.isArrayLiteral || !isArrayLiteralHasTrailingComma(lastArgument.node, sourceCode))
- ) {
- text = `${text},`;
- }
- }
- } else {
- text = `, ${text}`;
- }
- return text;
- };
- function removeArguments(fixer) {
- const [firstArgument] = concatCallArguments;
- const lastArgument = concatCallArguments[fixableArguments.length - 1];
- const [start] = getParenthesizedRange(firstArgument, sourceCode);
- let [, end] = sourceCode.getTokenAfter(lastArgument, isCommaToken).range;
- const textAfter = sourceCode.text.slice(end);
- const [leadingSpaces] = textAfter.match(/^\s*/);
- end += leadingSpaces.length;
- return fixer.replaceTextRange([start, end], '');
- }
- return function * (fixer) {
- // Fixed code always starts with `[`
- if (
- !arrayIsArrayLiteral
- && needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')
- ) {
- yield fixer.insertTextBefore(node, ';');
- }
- if (concatCallArguments.length - fixableArguments.length === 0) {
- yield * removeMethodCall(fixer, node, sourceCode);
- } else {
- yield removeArguments(fixer);
- }
- const text = getFixedText();
- if (arrayIsArrayLiteral) {
- const closingBracketToken = sourceCode.getLastToken(array);
- yield fixer.insertTextBefore(closingBracketToken, text);
- } else {
- // The array is already accessing `.concat`, there should not any case need add extra `()`
- yield fixer.insertTextBeforeRange(arrayParenthesizedRange, '[...');
- yield fixer.insertTextAfterRange(arrayParenthesizedRange, text);
- yield fixer.insertTextAfterRange(arrayParenthesizedRange, ']');
- }
- };
- }
- const getConcatArgumentSpreadable = (node, scope) => {
- if (node.type === 'SpreadElement') {
- return;
- }
- if (isArrayLiteral(node)) {
- return {node, isArrayLiteral: true};
- }
- const result = getStaticValue(node, scope);
- if (!result) {
- return;
- }
- const isSpreadable = Array.isArray(result.value);
- return {node, isSpreadable};
- };
- function getConcatFixableArguments(argumentsList, scope) {
- const fixableArguments = [];
- for (const node of argumentsList) {
- const result = getConcatArgumentSpreadable(node, scope);
- if (result) {
- fixableArguments.push(result);
- } else {
- break;
- }
- }
- return fixableArguments;
- }
- function fixArrayFrom(node, sourceCode) {
- const [object] = node.arguments;
- function getObjectText() {
- if (isArrayLiteral(object)) {
- return sourceCode.getText(object);
- }
- const [start, end] = getParenthesizedRange(object, sourceCode);
- let text = sourceCode.text.slice(start, end);
- if (
- !isParenthesized(object, sourceCode)
- && shouldAddParenthesesToSpreadElementArgument(object)
- ) {
- text = `(${text})`;
- }
- return `[...${text}]`;
- }
- function * removeObject(fixer) {
- yield * replaceNodeOrTokenAndSpacesBefore(object, '', fixer, sourceCode);
- const commaToken = sourceCode.getTokenAfter(object, isCommaToken);
- yield * replaceNodeOrTokenAndSpacesBefore(commaToken, '', fixer, sourceCode);
- yield removeSpacesAfter(commaToken, sourceCode, fixer);
- }
- return function * (fixer) {
- // Fixed code always starts with `[`
- if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')) {
- yield fixer.insertTextBefore(node, ';');
- }
- const objectText = getObjectText();
- if (node.arguments.length === 1) {
- yield fixer.replaceText(node, objectText);
- return;
- }
- // `Array.from(object, mapFunction, thisArgument)` -> `[...object].map(mapFunction, thisArgument)`
- yield fixer.replaceText(node.callee.object, objectText);
- yield fixer.replaceText(node.callee.property, 'map');
- yield * removeObject(fixer);
- };
- }
- function methodCallToSpread(node, sourceCode) {
- return function * (fixer) {
- // Fixed code always starts with `[`
- if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')) {
- yield fixer.insertTextBefore(node, ';');
- }
- yield fixer.insertTextBefore(node, '[...');
- yield fixer.insertTextAfter(node, ']');
- // The array is already accessing `.slice` or `.split`, there should not any case need add extra `()`
- yield * removeMethodCall(fixer, node, sourceCode);
- };
- }
- function isClassName(node) {
- if (node.type === 'MemberExpression') {
- node = node.property;
- }
- if (node.type !== 'Identifier') {
- return false;
- }
- const {name} = node;
- return /^[A-Z]./.test(name) && name.toUpperCase() !== name;
- }
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => {
- const sourceCode = context.getSourceCode();
- return {
- [arrayFromCallSelector](node) {
- return {
- node,
- messageId: ERROR_ARRAY_FROM,
- fix: fixArrayFrom(node, sourceCode),
- };
- },
- [arrayConcatCallSelector](node) {
- const {object} = node.callee;
- if (isClassName(object)) {
- return;
- }
- const scope = context.getScope();
- const staticResult = getStaticValue(object, scope);
- if (staticResult && !Array.isArray(staticResult.value)) {
- return;
- }
- const problem = {
- node: node.callee.property,
- messageId: ERROR_ARRAY_CONCAT,
- };
- const fixableArguments = getConcatFixableArguments(node.arguments, scope);
- if (fixableArguments.length > 0 || node.arguments.length === 0) {
- problem.fix = fixConcat(node, sourceCode, fixableArguments);
- return problem;
- }
- const [firstArgument, ...restArguments] = node.arguments;
- if (firstArgument.type === 'SpreadElement') {
- return problem;
- }
- const fixableArgumentsAfterFirstArgument = getConcatFixableArguments(restArguments, scope);
- const suggestions = [
- {
- messageId: SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE,
- isSpreadable: true,
- },
- {
- messageId: SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE,
- isSpreadable: false,
- },
- ];
- if (!hasSideEffect(firstArgument, sourceCode)) {
- suggestions.push({
- messageId: SUGGESTION_CONCAT_TEST_ARGUMENT,
- testArgument: true,
- });
- }
- problem.suggest = suggestions.map(({messageId, isSpreadable, testArgument}) => ({
- messageId,
- fix: fixConcat(
- node,
- sourceCode,
- // When apply suggestion, we also merge fixable arguments after the first one
- [
- {
- node: firstArgument,
- isSpreadable,
- testArgument,
- },
- ...fixableArgumentsAfterFirstArgument,
- ],
- ),
- }));
- if (
- fixableArgumentsAfterFirstArgument.length < restArguments.length
- && restArguments.every(({type}) => type !== 'SpreadElement')
- ) {
- problem.suggest.push({
- messageId: SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS,
- fix: fixConcat(
- node,
- sourceCode,
- node.arguments.map(node => getConcatArgumentSpreadable(node, scope) || {node, isSpreadable: true}),
- ),
- });
- }
- return problem;
- },
- [arraySliceCallSelector](node) {
- if (isNodeMatches(node.callee.object, ignoredSliceCallee)) {
- return;
- }
- const [firstArgument] = node.arguments;
- if (firstArgument && !isLiteralValue(firstArgument, 0)) {
- return;
- }
- return {
- node: node.callee.property,
- messageId: ERROR_ARRAY_SLICE,
- fix: methodCallToSpread(node, sourceCode),
- };
- },
- [stringSplitCallSelector](node) {
- const [separator] = node.arguments;
- if (!isLiteralValue(separator, '')) {
- return;
- }
- const string = node.callee.object;
- const staticValue = getStaticValue(string, context.getScope());
- let hasSameResult = false;
- if (staticValue) {
- const {value} = staticValue;
- if (typeof value !== 'string') {
- return;
- }
- // eslint-disable-next-line unicorn/prefer-spread
- const resultBySplit = value.split('');
- const resultBySpread = [...value];
- hasSameResult = resultBySplit.length === resultBySpread.length
- && resultBySplit.every((character, index) => character === resultBySpread[index]);
- }
- const problem = {
- node: node.callee.property,
- messageId: ERROR_STRING_SPLIT,
- };
- if (hasSameResult) {
- problem.fix = methodCallToSpread(node, sourceCode);
- } else {
- problem.suggest = [
- {
- messageId: SUGGESTION_USE_SPREAD,
- fix: methodCallToSpread(node, sourceCode),
- },
- ];
- }
- return problem;
- },
- };
- };
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` and `String#split(\'\')`.',
- },
- fixable: 'code',
- hasSuggestions: true,
- messages,
- },
- };
|