prefer-array-some.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. 'use strict';
  2. const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js');
  3. const {checkVueTemplate} = require('./utils/rule.js');
  4. const {isBooleanNode} = require('./utils/boolean.js');
  5. const {getParenthesizedRange} = require('./utils/parentheses.js');
  6. const {removeMemberExpressionProperty} = require('./fix/index.js');
  7. const {isLiteral, isUndefined} = require('./ast/index.js');
  8. const ERROR_ID_ARRAY_SOME = 'some';
  9. const SUGGESTION_ID_ARRAY_SOME = 'some-suggestion';
  10. const ERROR_ID_ARRAY_FILTER = 'filter';
  11. const messages = {
  12. [ERROR_ID_ARRAY_SOME]: 'Prefer `.some(…)` over `.{{method}}(…)`.',
  13. [SUGGESTION_ID_ARRAY_SOME]: 'Replace `.{{method}}(…)` with `.some(…)`.',
  14. [ERROR_ID_ARRAY_FILTER]: 'Prefer `.some(…)` over non-zero length check from `.filter(…)`.',
  15. };
  16. const arrayFindOrFindLastCallSelector = methodCallSelector({
  17. methods: ['find', 'findLast'],
  18. minimumArguments: 1,
  19. maximumArguments: 2,
  20. });
  21. const isCheckingUndefined = node =>
  22. node.parent.type === 'BinaryExpression'
  23. // Not checking yoda expression `null != foo.find()` and `undefined !== foo.find()
  24. && node.parent.left === node
  25. && (
  26. (
  27. (
  28. node.parent.operator === '!='
  29. || node.parent.operator === '=='
  30. || node.parent.operator === '==='
  31. || node.parent.operator === '!=='
  32. )
  33. && isUndefined(node.parent.right)
  34. )
  35. || (
  36. (
  37. node.parent.operator === '!='
  38. || node.parent.operator === '=='
  39. )
  40. // eslint-disable-next-line unicorn/no-null
  41. && isLiteral(node.parent.right, null)
  42. )
  43. );
  44. const arrayFilterCallSelector = [
  45. 'BinaryExpression',
  46. '[right.type="Literal"]',
  47. '[right.raw="0"]',
  48. // We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
  49. matches(['[operator=">"]', '[operator="!=="]']),
  50. ' > ',
  51. `${memberExpressionSelector('length')}.left`,
  52. ' > ',
  53. `${methodCallSelector('filter')}.object`,
  54. ].join('');
  55. /** @param {import('eslint').Rule.RuleContext} context */
  56. const create = context => ({
  57. [arrayFindOrFindLastCallSelector](callExpression) {
  58. const isCompare = isCheckingUndefined(callExpression);
  59. if (!isCompare && !isBooleanNode(callExpression)) {
  60. return;
  61. }
  62. const methodNode = callExpression.callee.property;
  63. return {
  64. node: methodNode,
  65. messageId: ERROR_ID_ARRAY_SOME,
  66. data: {method: methodNode.name},
  67. suggest: [
  68. {
  69. messageId: SUGGESTION_ID_ARRAY_SOME,
  70. * fix(fixer) {
  71. yield fixer.replaceText(methodNode, 'some');
  72. if (!isCompare) {
  73. return;
  74. }
  75. const parenthesizedRange = getParenthesizedRange(callExpression, context.getSourceCode());
  76. yield fixer.replaceTextRange([parenthesizedRange[1], callExpression.parent.range[1]], '');
  77. if (callExpression.parent.operator === '!=' || callExpression.parent.operator === '!==') {
  78. return;
  79. }
  80. yield fixer.insertTextBeforeRange(parenthesizedRange, '!');
  81. },
  82. },
  83. ],
  84. };
  85. },
  86. [arrayFilterCallSelector](filterCall) {
  87. const filterProperty = filterCall.callee.property;
  88. return {
  89. node: filterProperty,
  90. messageId: ERROR_ID_ARRAY_FILTER,
  91. * fix(fixer) {
  92. // `.filter` to `.some`
  93. yield fixer.replaceText(filterProperty, 'some');
  94. const sourceCode = context.getSourceCode();
  95. const lengthNode = filterCall.parent;
  96. /*
  97. Remove `.length`
  98. `(( (( array.filter() )).length )) > (( 0 ))`
  99. ------------------------^^^^^^^
  100. */
  101. yield removeMemberExpressionProperty(fixer, lengthNode, sourceCode);
  102. const compareNode = lengthNode.parent;
  103. /*
  104. Remove `> 0`
  105. `(( (( array.filter() )).length )) > (( 0 ))`
  106. ----------------------------------^^^^^^^^^^
  107. */
  108. yield fixer.removeRange([
  109. getParenthesizedRange(lengthNode, sourceCode)[1],
  110. compareNode.range[1],
  111. ]);
  112. // The `BinaryExpression` always ends with a number or `)`, no need check for ASI
  113. },
  114. };
  115. },
  116. });
  117. /** @type {import('eslint').Rule.RuleModule} */
  118. module.exports = {
  119. create: checkVueTemplate(create),
  120. meta: {
  121. type: 'suggestion',
  122. docs: {
  123. description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`.',
  124. },
  125. fixable: 'code',
  126. messages,
  127. hasSuggestions: true,
  128. },
  129. };