no-useless-length-check.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. 'use strict';
  2. const {methodCallSelector, matches, memberExpressionSelector} = require('./selectors/index.js');
  3. const isSameReference = require('./utils/is-same-reference.js');
  4. const {getParenthesizedRange} = require('./utils/parentheses.js');
  5. const messages = {
  6. 'non-zero': 'The non-empty check is useless as `Array#some()` returns `false` for an empty array.',
  7. zero: 'The empty check is useless as `Array#every()` returns `true` for an empty array.',
  8. };
  9. const logicalExpressionSelector = [
  10. 'LogicalExpression',
  11. matches(['[operator="||"]', '[operator="&&"]']),
  12. ].join('');
  13. // We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
  14. const lengthCompareZeroSelector = [
  15. logicalExpressionSelector,
  16. ' > ',
  17. 'BinaryExpression',
  18. memberExpressionSelector({path: 'left', property: 'length'}),
  19. '[right.type="Literal"]',
  20. '[right.raw="0"]',
  21. ].join('');
  22. const zeroLengthCheckSelector = [
  23. lengthCompareZeroSelector,
  24. '[operator="==="]',
  25. ].join('');
  26. const nonZeroLengthCheckSelector = [
  27. lengthCompareZeroSelector,
  28. matches(['[operator=">"]', '[operator="!=="]']),
  29. ].join('');
  30. const arraySomeCallSelector = methodCallSelector('some');
  31. const arrayEveryCallSelector = methodCallSelector('every');
  32. function flatLogicalExpression(node) {
  33. return [node.left, node.right].flatMap(child =>
  34. child.type === 'LogicalExpression' && child.operator === node.operator
  35. ? flatLogicalExpression(child)
  36. : [child],
  37. );
  38. }
  39. /** @param {import('eslint').Rule.RuleContext} context */
  40. const create = context => {
  41. const logicalExpressions = [];
  42. const zeroLengthChecks = new Set();
  43. const nonZeroLengthChecks = new Set();
  44. const arraySomeCalls = new Set();
  45. const arrayEveryCalls = new Set();
  46. function isUselessLengthCheckNode({node, operator, siblings}) {
  47. return (
  48. (
  49. operator === '||'
  50. && zeroLengthChecks.has(node)
  51. && siblings.some(condition =>
  52. arrayEveryCalls.has(condition)
  53. && isSameReference(node.left.object, condition.callee.object),
  54. )
  55. )
  56. || (
  57. operator === '&&'
  58. && nonZeroLengthChecks.has(node)
  59. && siblings.some(condition =>
  60. arraySomeCalls.has(condition)
  61. && isSameReference(node.left.object, condition.callee.object),
  62. )
  63. )
  64. );
  65. }
  66. function getUselessLengthCheckNode(logicalExpression) {
  67. const {operator} = logicalExpression;
  68. return flatLogicalExpression(logicalExpression)
  69. .filter((node, index, conditions) => isUselessLengthCheckNode({
  70. node,
  71. operator,
  72. siblings: [
  73. conditions[index - 1],
  74. conditions[index + 1],
  75. ].filter(Boolean),
  76. }));
  77. }
  78. return {
  79. [zeroLengthCheckSelector](node) {
  80. zeroLengthChecks.add(node);
  81. },
  82. [nonZeroLengthCheckSelector](node) {
  83. nonZeroLengthChecks.add(node);
  84. },
  85. [arraySomeCallSelector](node) {
  86. arraySomeCalls.add(node);
  87. },
  88. [arrayEveryCallSelector](node) {
  89. arrayEveryCalls.add(node);
  90. },
  91. [logicalExpressionSelector](node) {
  92. logicalExpressions.push(node);
  93. },
  94. * 'Program:exit'() {
  95. const nodes = new Set(
  96. logicalExpressions.flatMap(logicalExpression =>
  97. getUselessLengthCheckNode(logicalExpression),
  98. ),
  99. );
  100. for (const node of nodes) {
  101. yield {
  102. loc: {
  103. start: node.left.property.loc.start,
  104. end: node.loc.end,
  105. },
  106. messageId: zeroLengthChecks.has(node) ? 'zero' : 'non-zero',
  107. /** @param {import('eslint').Rule.RuleFixer} fixer */
  108. fix(fixer) {
  109. const sourceCode = context.getSourceCode();
  110. const {left, right} = node.parent;
  111. const leftRange = getParenthesizedRange(left, sourceCode);
  112. const rightRange = getParenthesizedRange(right, sourceCode);
  113. const range = [];
  114. if (left === node) {
  115. range[0] = leftRange[0];
  116. range[1] = rightRange[0];
  117. } else {
  118. range[0] = leftRange[1];
  119. range[1] = rightRange[1];
  120. }
  121. return fixer.removeRange(range);
  122. },
  123. };
  124. }
  125. },
  126. };
  127. };
  128. /** @type {import('eslint').Rule.RuleModule} */
  129. module.exports = {
  130. create,
  131. meta: {
  132. type: 'suggestion',
  133. docs: {
  134. description: 'Disallow useless array length check.',
  135. },
  136. fixable: 'code',
  137. messages,
  138. },
  139. };