prefer-array-flat.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. 'use strict';
  2. const {
  3. methodCallSelector,
  4. arrayPrototypeMethodSelector,
  5. emptyArraySelector,
  6. callExpressionSelector,
  7. } = require('./selectors/index.js');
  8. const needsSemicolon = require('./utils/needs-semicolon.js');
  9. const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
  10. const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches.js');
  11. const {getParenthesizedText, isParenthesized} = require('./utils/parentheses.js');
  12. const {fixSpaceAroundKeyword} = require('./fix/index.js');
  13. const MESSAGE_ID = 'prefer-array-flat';
  14. const messages = {
  15. [MESSAGE_ID]: 'Prefer `Array#flat()` over `{{description}}` to flatten an array.',
  16. };
  17. // `array.flatMap(x => x)`
  18. const arrayFlatMap = {
  19. selector: [
  20. methodCallSelector({
  21. method: 'flatMap',
  22. argumentsLength: 1,
  23. }),
  24. '[arguments.0.type="ArrowFunctionExpression"]',
  25. '[arguments.0.async!=true]',
  26. '[arguments.0.generator!=true]',
  27. '[arguments.0.params.length=1]',
  28. '[arguments.0.params.0.type="Identifier"]',
  29. '[arguments.0.body.type="Identifier"]',
  30. ].join(''),
  31. testFunction: node => node.arguments[0].params[0].name === node.arguments[0].body.name,
  32. getArrayNode: node => node.callee.object,
  33. description: 'Array#flatMap()',
  34. };
  35. // `array.reduce((a, b) => a.concat(b), [])`
  36. const arrayReduce = {
  37. selector: [
  38. methodCallSelector({
  39. method: 'reduce',
  40. argumentsLength: 2,
  41. }),
  42. '[arguments.0.type="ArrowFunctionExpression"]',
  43. '[arguments.0.async!=true]',
  44. '[arguments.0.generator!=true]',
  45. '[arguments.0.params.length=2]',
  46. '[arguments.0.params.0.type="Identifier"]',
  47. '[arguments.0.params.1.type="Identifier"]',
  48. methodCallSelector({
  49. method: 'concat',
  50. argumentsLength: 1,
  51. path: 'arguments.0.body',
  52. }),
  53. '[arguments.0.body.callee.object.type="Identifier"]',
  54. '[arguments.0.body.arguments.0.type="Identifier"]',
  55. emptyArraySelector('arguments.1'),
  56. ].join(''),
  57. testFunction: node =>
  58. node.arguments[0].params[0].name === node.arguments[0].body.callee.object.name
  59. && node.arguments[0].params[1].name === node.arguments[0].body.arguments[0].name,
  60. getArrayNode: node => node.callee.object,
  61. description: 'Array#reduce()',
  62. };
  63. // `array.reduce((a, b) => [...a, ...b], [])`
  64. const arrayReduce2 = {
  65. selector: [
  66. methodCallSelector({
  67. method: 'reduce',
  68. argumentsLength: 2,
  69. }),
  70. '[arguments.0.type="ArrowFunctionExpression"]',
  71. '[arguments.0.async!=true]',
  72. '[arguments.0.generator!=true]',
  73. '[arguments.0.params.length=2]',
  74. '[arguments.0.params.0.type="Identifier"]',
  75. '[arguments.0.params.1.type="Identifier"]',
  76. '[arguments.0.body.type="ArrayExpression"]',
  77. '[arguments.0.body.elements.length=2]',
  78. '[arguments.0.body.elements.0.type="SpreadElement"]',
  79. '[arguments.0.body.elements.0.argument.type="Identifier"]',
  80. '[arguments.0.body.elements.1.type="SpreadElement"]',
  81. '[arguments.0.body.elements.1.argument.type="Identifier"]',
  82. emptyArraySelector('arguments.1'),
  83. ].join(''),
  84. testFunction: node =>
  85. node.arguments[0].params[0].name === node.arguments[0].body.elements[0].argument.name
  86. && node.arguments[0].params[1].name === node.arguments[0].body.elements[1].argument.name,
  87. getArrayNode: node => node.callee.object,
  88. description: 'Array#reduce()',
  89. };
  90. // `[].concat(maybeArray)` and `[].concat(...array)`
  91. const emptyArrayConcat = {
  92. selector: [
  93. methodCallSelector({
  94. method: 'concat',
  95. argumentsLength: 1,
  96. allowSpreadElement: true,
  97. }),
  98. emptyArraySelector('callee.object'),
  99. ].join(''),
  100. getArrayNode(node) {
  101. const argumentNode = node.arguments[0];
  102. return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
  103. },
  104. description: '[].concat()',
  105. shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement',
  106. };
  107. // - `[].concat.apply([], array)` and `Array.prototype.concat.apply([], array)`
  108. // - `[].concat.call([], maybeArray)` and `Array.prototype.concat.call([], maybeArray)`
  109. // - `[].concat.call([], ...array)` and `Array.prototype.concat.call([], ...array)`
  110. const arrayPrototypeConcat = {
  111. selector: [
  112. methodCallSelector({
  113. methods: ['apply', 'call'],
  114. argumentsLength: 2,
  115. allowSpreadElement: true,
  116. }),
  117. emptyArraySelector('arguments.0'),
  118. arrayPrototypeMethodSelector({
  119. path: 'callee.object',
  120. method: 'concat',
  121. }),
  122. ].join(''),
  123. testFunction: node => node.arguments[1].type !== 'SpreadElement' || node.callee.property.name === 'call',
  124. getArrayNode(node) {
  125. const argumentNode = node.arguments[1];
  126. return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
  127. },
  128. description: 'Array.prototype.concat()',
  129. shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call',
  130. };
  131. const lodashFlattenFunctions = [
  132. '_.flatten',
  133. 'lodash.flatten',
  134. 'underscore.flatten',
  135. ];
  136. const anyCall = {
  137. selector: callExpressionSelector({argumentsLength: 1}),
  138. getArrayNode: node => node.arguments[0],
  139. };
  140. function fix(node, array, sourceCode, shouldSwitchToArray) {
  141. if (typeof shouldSwitchToArray === 'function') {
  142. shouldSwitchToArray = shouldSwitchToArray(node);
  143. }
  144. return function * (fixer) {
  145. let fixed = getParenthesizedText(array, sourceCode);
  146. if (shouldSwitchToArray) {
  147. // `array` is an argument, when it changes to `array[]`, we don't need add extra parentheses
  148. fixed = `[${fixed}]`;
  149. // And we don't need to add parentheses to the new array to call `.flat()`
  150. } else if (
  151. !isParenthesized(array, sourceCode)
  152. && shouldAddParenthesesToMemberExpressionObject(array, sourceCode)
  153. ) {
  154. fixed = `(${fixed})`;
  155. }
  156. fixed = `${fixed}.flat()`;
  157. const tokenBefore = sourceCode.getTokenBefore(node);
  158. if (needsSemicolon(tokenBefore, sourceCode, fixed)) {
  159. fixed = `;${fixed}`;
  160. }
  161. yield fixer.replaceText(node, fixed);
  162. yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
  163. };
  164. }
  165. function create(context) {
  166. const {functions: configFunctions} = {
  167. functions: [],
  168. ...context.options[0],
  169. };
  170. const functions = [...configFunctions, ...lodashFlattenFunctions];
  171. const sourceCode = context.getSourceCode();
  172. const listeners = {};
  173. const cases = [
  174. arrayFlatMap,
  175. arrayReduce,
  176. arrayReduce2,
  177. emptyArrayConcat,
  178. arrayPrototypeConcat,
  179. {
  180. ...anyCall,
  181. testFunction: node => isNodeMatches(node.callee, functions),
  182. description: node => `${functions.find(nameOrPath => isNodeMatchesNameOrPath(node.callee, nameOrPath)).trim()}()`,
  183. },
  184. ];
  185. for (const {selector, testFunction, description, getArrayNode, shouldSwitchToArray} of cases) {
  186. listeners[selector] = function (node) {
  187. if (testFunction && !testFunction(node)) {
  188. return;
  189. }
  190. const array = getArrayNode(node);
  191. const data = {
  192. description: typeof description === 'string' ? description : description(node),
  193. };
  194. const problem = {
  195. node,
  196. messageId: MESSAGE_ID,
  197. data,
  198. };
  199. // Don't fix if it has comments.
  200. if (
  201. sourceCode.getCommentsInside(node).length
  202. === sourceCode.getCommentsInside(array).length
  203. ) {
  204. problem.fix = fix(node, array, sourceCode, shouldSwitchToArray);
  205. }
  206. return problem;
  207. };
  208. }
  209. return listeners;
  210. }
  211. const schema = [
  212. {
  213. type: 'object',
  214. additionalProperties: false,
  215. properties: {
  216. functions: {
  217. type: 'array',
  218. uniqueItems: true,
  219. },
  220. },
  221. },
  222. ];
  223. /** @type {import('eslint').Rule.RuleModule} */
  224. module.exports = {
  225. create,
  226. meta: {
  227. type: 'suggestion',
  228. docs: {
  229. description: 'Prefer `Array#flat()` over legacy techniques to flatten arrays.',
  230. },
  231. fixable: 'code',
  232. schema,
  233. messages,
  234. },
  235. };