no-useless-spread.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. 'use strict';
  2. const {isCommaToken} = require('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 {removeParentheses, fixSpaceAroundKeyword} = require('./fix/index.js');
  10. const SPREAD_IN_LIST = 'spread-in-list';
  11. const ITERABLE_TO_ARRAY = 'iterable-to-array';
  12. const ITERABLE_TO_ARRAY_IN_FOR_OF = 'iterable-to-array-in-for-of';
  13. const ITERABLE_TO_ARRAY_IN_YIELD_STAR = 'iterable-to-array-in-yield-star';
  14. const messages = {
  15. [SPREAD_IN_LIST]: 'Spread an {{argumentType}} literal in {{parentDescription}} is unnecessary.',
  16. [ITERABLE_TO_ARRAY]: '`{{parentDescription}}` accepts iterable as argument, it\'s unnecessary to convert to an array.',
  17. [ITERABLE_TO_ARRAY_IN_FOR_OF]: '`for…of` can iterate over iterable, it\'s unnecessary to convert to an array.',
  18. [ITERABLE_TO_ARRAY_IN_YIELD_STAR]: '`yield*` can delegate iterable, it\'s unnecessary to convert to an array.',
  19. };
  20. const uselessSpreadInListSelector = matches([
  21. 'ArrayExpression > SpreadElement.elements > ArrayExpression.argument',
  22. 'ObjectExpression > SpreadElement.properties > ObjectExpression.argument',
  23. 'CallExpression > SpreadElement.arguments > ArrayExpression.argument',
  24. 'NewExpression > SpreadElement.arguments > ArrayExpression.argument',
  25. ]);
  26. const iterableToArraySelector = [
  27. 'ArrayExpression',
  28. '[elements.length=1]',
  29. '[elements.0.type="SpreadElement"]',
  30. ].join('');
  31. const uselessIterableToArraySelector = matches([
  32. [
  33. matches([
  34. newExpressionSelector({names: ['Map', 'WeakMap', 'Set', 'WeakSet'], argumentsLength: 1}),
  35. newExpressionSelector({names: typedArray, minimumArguments: 1}),
  36. methodCallSelector({
  37. object: 'Promise',
  38. methods: ['all', 'allSettled', 'any', 'race'],
  39. argumentsLength: 1,
  40. }),
  41. methodCallSelector({
  42. objects: ['Array', ...typedArray],
  43. method: 'from',
  44. argumentsLength: 1,
  45. }),
  46. methodCallSelector({object: 'Object', method: 'fromEntries', argumentsLength: 1}),
  47. ]),
  48. ' > ',
  49. `${iterableToArraySelector}.arguments:first-child`,
  50. ].join(''),
  51. `ForOfStatement > ${iterableToArraySelector}.right`,
  52. `YieldExpression[delegate=true] > ${iterableToArraySelector}.argument`,
  53. ]);
  54. const parentDescriptions = {
  55. ArrayExpression: 'array literal',
  56. ObjectExpression: 'object literal',
  57. CallExpression: 'arguments',
  58. NewExpression: 'arguments',
  59. };
  60. function getCommaTokens(arrayExpression, sourceCode) {
  61. let startToken = sourceCode.getFirstToken(arrayExpression);
  62. return arrayExpression.elements.map((element, index, elements) => {
  63. if (index === elements.length - 1) {
  64. const penultimateToken = sourceCode.getLastToken(arrayExpression, {skip: 1});
  65. if (isCommaToken(penultimateToken)) {
  66. return penultimateToken;
  67. }
  68. return;
  69. }
  70. const commaToken = sourceCode.getTokenAfter(element || startToken, isCommaToken);
  71. startToken = commaToken;
  72. return commaToken;
  73. });
  74. }
  75. /** @param {import('eslint').Rule.RuleContext} context */
  76. const create = context => {
  77. const sourceCode = context.getSourceCode();
  78. return {
  79. [uselessSpreadInListSelector](spreadObject) {
  80. const spreadElement = spreadObject.parent;
  81. const spreadToken = sourceCode.getFirstToken(spreadElement);
  82. const parentType = spreadElement.parent.type;
  83. return {
  84. node: spreadToken,
  85. messageId: SPREAD_IN_LIST,
  86. data: {
  87. argumentType: spreadObject.type === 'ArrayExpression' ? 'array' : 'object',
  88. parentDescription: parentDescriptions[parentType],
  89. },
  90. /** @param {import('eslint').Rule.RuleFixer} fixer */
  91. * fix(fixer) {
  92. // `[...[foo]]`
  93. // ^^^
  94. yield fixer.remove(spreadToken);
  95. // `[...(( [foo] ))]`
  96. // ^^ ^^
  97. yield * removeParentheses(spreadObject, fixer, sourceCode);
  98. // `[...[foo]]`
  99. // ^
  100. const firstToken = sourceCode.getFirstToken(spreadObject);
  101. yield fixer.remove(firstToken);
  102. const [
  103. penultimateToken,
  104. lastToken,
  105. ] = sourceCode.getLastTokens(spreadObject, 2);
  106. // `[...[foo]]`
  107. // ^
  108. yield fixer.remove(lastToken);
  109. // `[...[foo,]]`
  110. // ^
  111. if (isCommaToken(penultimateToken)) {
  112. yield fixer.remove(penultimateToken);
  113. }
  114. if (parentType !== 'CallExpression' && parentType !== 'NewExpression') {
  115. return;
  116. }
  117. const commaTokens = getCommaTokens(spreadObject, sourceCode);
  118. for (const [index, commaToken] of commaTokens.entries()) {
  119. if (spreadObject.elements[index]) {
  120. continue;
  121. }
  122. // `call([foo, , bar])`
  123. // ^ Replace holes with `undefined`
  124. yield fixer.insertTextBefore(commaToken, 'undefined');
  125. }
  126. },
  127. };
  128. },
  129. [uselessIterableToArraySelector](array) {
  130. const {parent} = array;
  131. let parentDescription = '';
  132. let messageId = ITERABLE_TO_ARRAY;
  133. switch (parent.type) {
  134. case 'ForOfStatement':
  135. messageId = ITERABLE_TO_ARRAY_IN_FOR_OF;
  136. break;
  137. case 'YieldExpression':
  138. messageId = ITERABLE_TO_ARRAY_IN_YIELD_STAR;
  139. break;
  140. case 'NewExpression':
  141. parentDescription = `new ${parent.callee.name}(…)`;
  142. break;
  143. case 'CallExpression':
  144. parentDescription = `${parent.callee.object.name}.${parent.callee.property.name}(…)`;
  145. break;
  146. // No default
  147. }
  148. return {
  149. node: array,
  150. messageId,
  151. data: {parentDescription},
  152. * fix(fixer) {
  153. if (parent.type === 'ForOfStatement') {
  154. yield * fixSpaceAroundKeyword(fixer, array, sourceCode);
  155. }
  156. const [
  157. openingBracketToken,
  158. spreadToken,
  159. ] = sourceCode.getFirstTokens(array, 2);
  160. // `[...iterable]`
  161. // ^
  162. yield fixer.remove(openingBracketToken);
  163. // `[...iterable]`
  164. // ^^^
  165. yield fixer.remove(spreadToken);
  166. const [
  167. commaToken,
  168. closingBracketToken,
  169. ] = sourceCode.getLastTokens(array, 2);
  170. // `[...iterable]`
  171. // ^
  172. yield fixer.remove(closingBracketToken);
  173. // `[...iterable,]`
  174. // ^
  175. if (isCommaToken(commaToken)) {
  176. yield fixer.remove(commaToken);
  177. }
  178. },
  179. };
  180. },
  181. };
  182. };
  183. /** @type {import('eslint').Rule.RuleModule} */
  184. module.exports = {
  185. create,
  186. meta: {
  187. type: 'suggestion',
  188. docs: {
  189. description: 'Disallow unnecessary spread.',
  190. },
  191. fixable: 'code',
  192. messages,
  193. },
  194. };