no-useless-spread.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. 'use strict';
  2. const {isCommaToken} = require('@eslint-community/eslint-utils');
  3. const {
  4. matches,
  5. newExpressionSelector,
  6. methodCallSelector,
  7. } = require('./selectors/index.js');
  8. const typedArray = require('./shared/typed-array.js');
  9. const {
  10. removeParentheses,
  11. fixSpaceAroundKeyword,
  12. addParenthesizesToReturnOrThrowExpression,
  13. } = require('./fix/index.js');
  14. const isOnSameLine = require('./utils/is-on-same-line.js');
  15. const {
  16. isParenthesized,
  17. } = require('./utils/parentheses.js');
  18. const {isNewExpression} = require('./ast/index.js');
  19. const SPREAD_IN_LIST = 'spread-in-list';
  20. const ITERABLE_TO_ARRAY = 'iterable-to-array';
  21. const ITERABLE_TO_ARRAY_IN_FOR_OF = 'iterable-to-array-in-for-of';
  22. const ITERABLE_TO_ARRAY_IN_YIELD_STAR = 'iterable-to-array-in-yield-star';
  23. const CLONE_ARRAY = 'clone-array';
  24. const messages = {
  25. [SPREAD_IN_LIST]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.',
  26. [ITERABLE_TO_ARRAY]: '`{{parentDescription}}` accepts iterable as argument, it\'s unnecessary to convert to an array.',
  27. [ITERABLE_TO_ARRAY_IN_FOR_OF]: '`for…of` can iterate over iterable, it\'s unnecessary to convert to an array.',
  28. [ITERABLE_TO_ARRAY_IN_YIELD_STAR]: '`yield*` can delegate iterable, it\'s unnecessary to convert to an array.',
  29. [CLONE_ARRAY]: 'Unnecessarily cloning an array.',
  30. };
  31. const uselessSpreadInListSelector = matches([
  32. 'ArrayExpression > SpreadElement.elements > ArrayExpression.argument',
  33. 'ObjectExpression > SpreadElement.properties > ObjectExpression.argument',
  34. 'CallExpression > SpreadElement.arguments > ArrayExpression.argument',
  35. 'NewExpression > SpreadElement.arguments > ArrayExpression.argument',
  36. ]);
  37. const singleArraySpreadSelector = [
  38. 'ArrayExpression',
  39. '[elements.length=1]',
  40. '[elements.0.type="SpreadElement"]',
  41. ].join('');
  42. const uselessIterableToArraySelector = matches([
  43. [
  44. matches([
  45. newExpressionSelector({names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1}),
  46. newExpressionSelector({names: typedArray, minimumArguments: 1}),
  47. methodCallSelector({
  48. object: 'Promise',
  49. methods: ['all', 'allSettled', 'any', 'race'],
  50. argumentsLength: 1,
  51. }),
  52. methodCallSelector({
  53. objects: ['Array', ...typedArray],
  54. method: 'from',
  55. argumentsLength: 1,
  56. }),
  57. methodCallSelector({object: 'Object', method: 'fromEntries', argumentsLength: 1}),
  58. ]),
  59. ' > ',
  60. `${singleArraySpreadSelector}.arguments:first-child`,
  61. ].join(''),
  62. `ForOfStatement > ${singleArraySpreadSelector}.right`,
  63. `YieldExpression[delegate=true] > ${singleArraySpreadSelector}.argument`,
  64. ]);
  65. const uselessArrayCloneSelector = [
  66. `${singleArraySpreadSelector} > .elements:first-child > .argument`,
  67. matches([
  68. // Array methods returns a new array
  69. methodCallSelector([
  70. 'concat',
  71. 'copyWithin',
  72. 'filter',
  73. 'flat',
  74. 'flatMap',
  75. 'map',
  76. 'slice',
  77. 'splice',
  78. ]),
  79. // `String#split()`
  80. methodCallSelector('split'),
  81. // `Object.keys()` and `Object.values()`
  82. methodCallSelector({object: 'Object', methods: ['keys', 'values'], argumentsLength: 1}),
  83. // `await Promise.all()` and `await Promise.allSettled`
  84. [
  85. 'AwaitExpression',
  86. methodCallSelector({
  87. object: 'Promise',
  88. methods: ['all', 'allSettled'],
  89. argumentsLength: 1,
  90. path: 'argument',
  91. }),
  92. ].join(''),
  93. // `Array.from()`, `Array.of()`
  94. methodCallSelector({object: 'Array', methods: ['from', 'of']}),
  95. // `new Array()`
  96. newExpressionSelector('Array'),
  97. ]),
  98. ].join('');
  99. const parentDescriptions = {
  100. ArrayExpression: 'array literal',
  101. ObjectExpression: 'object literal',
  102. CallExpression: 'arguments',
  103. NewExpression: 'arguments',
  104. };
  105. function getCommaTokens(arrayExpression, sourceCode) {
  106. let startToken = sourceCode.getFirstToken(arrayExpression);
  107. return arrayExpression.elements.map((element, index, elements) => {
  108. if (index === elements.length - 1) {
  109. const penultimateToken = sourceCode.getLastToken(arrayExpression, {skip: 1});
  110. if (isCommaToken(penultimateToken)) {
  111. return penultimateToken;
  112. }
  113. return;
  114. }
  115. const commaToken = sourceCode.getTokenAfter(element || startToken, isCommaToken);
  116. startToken = commaToken;
  117. return commaToken;
  118. });
  119. }
  120. function * unwrapSingleArraySpread(fixer, arrayExpression, sourceCode) {
  121. const [
  122. openingBracketToken,
  123. spreadToken,
  124. thirdToken,
  125. ] = sourceCode.getFirstTokens(arrayExpression, 3);
  126. // `[...value]`
  127. // ^
  128. yield fixer.remove(openingBracketToken);
  129. // `[...value]`
  130. // ^^^
  131. yield fixer.remove(spreadToken);
  132. const [
  133. commaToken,
  134. closingBracketToken,
  135. ] = sourceCode.getLastTokens(arrayExpression, 2);
  136. // `[...value]`
  137. // ^
  138. yield fixer.remove(closingBracketToken);
  139. // `[...value,]`
  140. // ^
  141. if (isCommaToken(commaToken)) {
  142. yield fixer.remove(commaToken);
  143. }
  144. /*
  145. ```js
  146. function foo() {
  147. return [
  148. ...value,
  149. ];
  150. }
  151. ```
  152. */
  153. const {parent} = arrayExpression;
  154. if (
  155. (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
  156. && parent.argument === arrayExpression
  157. && !isOnSameLine(openingBracketToken, thirdToken)
  158. && !isParenthesized(arrayExpression, sourceCode)
  159. ) {
  160. yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
  161. return;
  162. }
  163. yield * fixSpaceAroundKeyword(fixer, arrayExpression, sourceCode);
  164. }
  165. /** @param {import('eslint').Rule.RuleContext} context */
  166. const create = context => {
  167. const sourceCode = context.getSourceCode();
  168. return {
  169. [uselessSpreadInListSelector](spreadObject) {
  170. const spreadElement = spreadObject.parent;
  171. const spreadToken = sourceCode.getFirstToken(spreadElement);
  172. const parentType = spreadElement.parent.type;
  173. return {
  174. node: spreadToken,
  175. messageId: SPREAD_IN_LIST,
  176. data: {
  177. argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object',
  178. parentDescription: parentDescriptions[parentType],
  179. },
  180. /** @param {import('eslint').Rule.RuleFixer} fixer */
  181. * fix(fixer) {
  182. // `[...[foo]]`
  183. // ^^^
  184. yield fixer.remove(spreadToken);
  185. // `[...(( [foo] ))]`
  186. // ^^ ^^
  187. yield * removeParentheses(spreadObject, fixer, sourceCode);
  188. // `[...[foo]]`
  189. // ^
  190. const firstToken = sourceCode.getFirstToken(spreadObject);
  191. yield fixer.remove(firstToken);
  192. const [
  193. penultimateToken,
  194. lastToken,
  195. ] = sourceCode.getLastTokens(spreadObject, 2);
  196. // `[...[foo]]`
  197. // ^
  198. yield fixer.remove(lastToken);
  199. // `[...[foo,]]`
  200. // ^
  201. if (isCommaToken(penultimateToken)) {
  202. yield fixer.remove(penultimateToken);
  203. }
  204. if (parentType !== 'CallExpression' && parentType !== 'NewExpression') {
  205. return;
  206. }
  207. const commaTokens = getCommaTokens(spreadObject, sourceCode);
  208. for (const [index, commaToken] of commaTokens.entries()) {
  209. if (spreadObject.elements[index]) {
  210. continue;
  211. }
  212. // `call(...[foo, , bar])`
  213. // ^ Replace holes with `undefined`
  214. yield fixer.insertTextBefore(commaToken, 'undefined');
  215. }
  216. },
  217. };
  218. },
  219. [uselessIterableToArraySelector](arrayExpression) {
  220. const {parent} = arrayExpression;
  221. let parentDescription = '';
  222. let messageId = ITERABLE_TO_ARRAY;
  223. switch (parent.type) {
  224. case 'ForOfStatement': {
  225. messageId = ITERABLE_TO_ARRAY_IN_FOR_OF;
  226. break;
  227. }
  228. case 'YieldExpression': {
  229. messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR;
  230. break;
  231. }
  232. case 'NewExpression': {
  233. parentDescription = `new ${parent.callee.name}(…)`;
  234. break;
  235. }
  236. case 'CallExpression': {
  237. parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`;
  238. break;
  239. }
  240. // No default
  241. }
  242. return {
  243. node: arrayExpression,
  244. messageId,
  245. data: {parentDescription},
  246. fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode),
  247. };
  248. },
  249. [uselessArrayCloneSelector](node) {
  250. const arrayExpression = node.parent.parent;
  251. const problem = {
  252. node: arrayExpression,
  253. messageId: CLONE_ARRAY,
  254. };
  255. if (
  256. // `[...new Array(1)]` -> `new Array(1)` is not safe to fix since there are holes
  257. isNewExpression(node, {name: 'Array'})
  258. // `[...foo.slice(1)]` -> `foo.slice(1)` is not safe to fix since `foo` can be a string
  259. || (
  260. node.type === 'CallExpression'
  261. && node.callee.type === 'MemberExpression'
  262. && node.callee.property.type === 'Identifier'
  263. && node.callee.property.name === 'slice'
  264. )
  265. ) {
  266. return problem;
  267. }
  268. return Object.assign(problem, {
  269. fix: fixer => unwrapSingleArraySpread(fixer, arrayExpression, sourceCode),
  270. });
  271. },
  272. };
  273. };
  274. /** @type {import('eslint').Rule.RuleModule} */
  275. module.exports = {
  276. create,
  277. meta: {
  278. type: 'suggestion',
  279. docs: {
  280. description: 'Disallow unnecessary spread.',
  281. },
  282. fixable: 'code',
  283. messages,
  284. },
  285. };