no-array-push-push.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. 'use strict';
  2. const {hasSideEffect, isCommaToken, isSemicolonToken} = require('eslint-utils');
  3. const {methodCallSelector} = require('./selectors/index.js');
  4. const getCallExpressionArgumentsText = require('./utils/get-call-expression-arguments-text.js');
  5. const isSameReference = require('./utils/is-same-reference.js');
  6. const {isNodeMatches} = require('./utils/is-node-matches.js');
  7. const ERROR = 'error';
  8. const SUGGESTION = 'suggestion';
  9. const messages = {
  10. [ERROR]: 'Do not call `Array#push()` multiple times.',
  11. [SUGGESTION]: 'Merge with previous one.',
  12. };
  13. const arrayPushExpressionStatement = [
  14. 'ExpressionStatement',
  15. methodCallSelector({path: 'expression', method: 'push'}),
  16. ].join('');
  17. const selector = `${arrayPushExpressionStatement} + ${arrayPushExpressionStatement}`;
  18. function getFirstExpression(node, sourceCode) {
  19. const {parent} = node;
  20. const visitorKeys = sourceCode.visitorKeys[parent.type] || Object.keys(parent);
  21. for (const property of visitorKeys) {
  22. const value = parent[property];
  23. if (Array.isArray(value)) {
  24. const index = value.indexOf(node);
  25. if (index !== -1) {
  26. return value[index - 1];
  27. }
  28. }
  29. }
  30. /* istanbul ignore next */
  31. throw new Error('Cannot find the first `Array#push()` call.\nPlease open an issue at https://github.com/sindresorhus/eslint-plugin-unicorn/issues/new?title=%60no-array-push-push%60%3A%20Cannot%20find%20first%20%60push()%60');
  32. }
  33. function create(context) {
  34. const {ignore} = {
  35. ignore: [],
  36. ...context.options[0],
  37. };
  38. const ignoredObjects = ['stream', 'this', 'this.stream', ...ignore];
  39. const sourceCode = context.getSourceCode();
  40. return {
  41. [selector](secondExpression) {
  42. const secondCall = secondExpression.expression;
  43. const secondCallArray = secondCall.callee.object;
  44. if (isNodeMatches(secondCallArray, ignoredObjects)) {
  45. return;
  46. }
  47. const firstExpression = getFirstExpression(secondExpression, sourceCode);
  48. const firstCall = firstExpression.expression;
  49. const firstCallArray = firstCall.callee.object;
  50. // Not same array
  51. if (!isSameReference(firstCallArray, secondCallArray)) {
  52. return;
  53. }
  54. const secondCallArguments = secondCall.arguments;
  55. const problem = {
  56. node: secondCall.callee.property,
  57. messageId: ERROR,
  58. };
  59. const fix = function * (fixer) {
  60. if (secondCallArguments.length > 0) {
  61. const text = getCallExpressionArgumentsText(secondCall, sourceCode);
  62. const [penultimateToken, lastToken] = sourceCode.getLastTokens(firstCall, 2);
  63. yield (
  64. isCommaToken(penultimateToken)
  65. ? fixer.insertTextAfter(penultimateToken, ` ${text}`)
  66. : fixer.insertTextBefore(lastToken, firstCall.arguments.length > 0 ? `, ${text}` : text)
  67. );
  68. }
  69. const shouldKeepSemicolon = !isSemicolonToken(sourceCode.getLastToken(firstExpression))
  70. && isSemicolonToken(sourceCode.getLastToken(secondExpression));
  71. yield fixer.replaceTextRange(
  72. [firstExpression.range[1], secondExpression.range[1]],
  73. shouldKeepSemicolon ? ';' : '',
  74. );
  75. };
  76. if (secondCallArguments.some(element => hasSideEffect(element, sourceCode))) {
  77. problem.suggest = [
  78. {
  79. messageId: SUGGESTION,
  80. fix,
  81. },
  82. ];
  83. } else {
  84. problem.fix = fix;
  85. }
  86. return problem;
  87. },
  88. };
  89. }
  90. const schema = [
  91. {
  92. type: 'object',
  93. additionalProperties: false,
  94. properties: {
  95. ignore: {
  96. type: 'array',
  97. uniqueItems: true,
  98. },
  99. },
  100. },
  101. ];
  102. /** @type {import('eslint').Rule.RuleModule} */
  103. module.exports = {
  104. create,
  105. meta: {
  106. type: 'suggestion',
  107. docs: {
  108. description: 'Enforce combining multiple `Array#push()` into one call.',
  109. },
  110. fixable: 'code',
  111. hasSuggestions: true,
  112. schema,
  113. messages,
  114. },
  115. };