no-array-push-push.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. 'use strict';
  2. const {hasSideEffect, isCommaToken, isSemicolonToken} = require('@eslint-community/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. /* c8 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 = [
  39. 'stream',
  40. 'this',
  41. 'this.stream',
  42. 'process.stdin',
  43. 'process.stdout',
  44. 'process.stderr',
  45. ...ignore,
  46. ];
  47. const sourceCode = context.getSourceCode();
  48. return {
  49. [selector](secondExpression) {
  50. const secondCall = secondExpression.expression;
  51. const secondCallArray = secondCall.callee.object;
  52. if (isNodeMatches(secondCallArray, ignoredObjects)) {
  53. return;
  54. }
  55. const firstExpression = getFirstExpression(secondExpression, sourceCode);
  56. const firstCall = firstExpression.expression;
  57. const firstCallArray = firstCall.callee.object;
  58. // Not same array
  59. if (!isSameReference(firstCallArray, secondCallArray)) {
  60. return;
  61. }
  62. const secondCallArguments = secondCall.arguments;
  63. const problem = {
  64. node: secondCall.callee.property,
  65. messageId: ERROR,
  66. };
  67. const fix = function * (fixer) {
  68. if (secondCallArguments.length > 0) {
  69. const text = getCallExpressionArgumentsText(secondCall, sourceCode);
  70. const [penultimateToken, lastToken] = sourceCode.getLastTokens(firstCall, 2);
  71. yield (
  72. isCommaToken(penultimateToken)
  73. ? fixer.insertTextAfter(penultimateToken, ` ${text}`)
  74. : fixer.insertTextBefore(lastToken, firstCall.arguments.length > 0 ? `, ${text}` : text)
  75. );
  76. }
  77. const shouldKeepSemicolon = !isSemicolonToken(sourceCode.getLastToken(firstExpression))
  78. && isSemicolonToken(sourceCode.getLastToken(secondExpression));
  79. yield fixer.replaceTextRange(
  80. [firstExpression.range[1], secondExpression.range[1]],
  81. shouldKeepSemicolon ? ';' : '',
  82. );
  83. };
  84. if (secondCallArguments.some(element => hasSideEffect(element, sourceCode))) {
  85. problem.suggest = [
  86. {
  87. messageId: SUGGESTION,
  88. fix,
  89. },
  90. ];
  91. } else {
  92. problem.fix = fix;
  93. }
  94. return problem;
  95. },
  96. };
  97. }
  98. const schema = [
  99. {
  100. type: 'object',
  101. additionalProperties: false,
  102. properties: {
  103. ignore: {
  104. type: 'array',
  105. uniqueItems: true,
  106. },
  107. },
  108. },
  109. ];
  110. /** @type {import('eslint').Rule.RuleModule} */
  111. module.exports = {
  112. create,
  113. meta: {
  114. type: 'suggestion',
  115. docs: {
  116. description: 'Enforce combining multiple `Array#push()` into one call.',
  117. },
  118. fixable: 'code',
  119. hasSuggestions: true,
  120. schema,
  121. messages,
  122. },
  123. };