no-dupe-else-if.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. /**
  2. * @fileoverview Rule to disallow duplicate conditions in if-else-if chains
  3. * @author Milos Djermanovic
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Determines whether the first given array is a subset of the second given array.
  15. * @param {Function} comparator A function to compare two elements, should return `true` if they are equal.
  16. * @param {Array} arrA The array to compare from.
  17. * @param {Array} arrB The array to compare against.
  18. * @returns {boolean} `true` if the array `arrA` is a subset of the array `arrB`.
  19. */
  20. function isSubsetByComparator(comparator, arrA, arrB) {
  21. return arrA.every(a => arrB.some(b => comparator(a, b)));
  22. }
  23. /**
  24. * Splits the given node by the given logical operator.
  25. * @param {string} operator Logical operator `||` or `&&`.
  26. * @param {ASTNode} node The node to split.
  27. * @returns {ASTNode[]} Array of conditions that makes the node when joined by the operator.
  28. */
  29. function splitByLogicalOperator(operator, node) {
  30. if (node.type === "LogicalExpression" && node.operator === operator) {
  31. return [
  32. ...splitByLogicalOperator(operator, node.left),
  33. ...splitByLogicalOperator(operator, node.right),
  34. ];
  35. }
  36. return [node];
  37. }
  38. const splitByOr = splitByLogicalOperator.bind(null, "||");
  39. const splitByAnd = splitByLogicalOperator.bind(null, "&&");
  40. //------------------------------------------------------------------------------
  41. // Rule Definition
  42. //------------------------------------------------------------------------------
  43. /** @type {import('../types').Rule.RuleModule} */
  44. module.exports = {
  45. meta: {
  46. type: "problem",
  47. docs: {
  48. description: "Disallow duplicate conditions in if-else-if chains",
  49. recommended: true,
  50. url: "https://eslint.org/docs/latest/rules/no-dupe-else-if",
  51. },
  52. schema: [],
  53. messages: {
  54. unexpected:
  55. "This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.",
  56. },
  57. },
  58. create(context) {
  59. const sourceCode = context.sourceCode;
  60. /**
  61. * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
  62. * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
  63. * @param {ASTNode} a First node.
  64. * @param {ASTNode} b Second node.
  65. * @returns {boolean} `true` if the nodes are considered to be equal.
  66. */
  67. function equal(a, b) {
  68. if (a.type !== b.type) {
  69. return false;
  70. }
  71. if (
  72. a.type === "LogicalExpression" &&
  73. (a.operator === "||" || a.operator === "&&") &&
  74. a.operator === b.operator
  75. ) {
  76. return (
  77. (equal(a.left, b.left) && equal(a.right, b.right)) ||
  78. (equal(a.left, b.right) && equal(a.right, b.left))
  79. );
  80. }
  81. return astUtils.equalTokens(a, b, sourceCode);
  82. }
  83. const isSubset = isSubsetByComparator.bind(null, equal);
  84. return {
  85. IfStatement(node) {
  86. const test = node.test,
  87. conditionsToCheck =
  88. test.type === "LogicalExpression" &&
  89. test.operator === "&&"
  90. ? [test, ...splitByAnd(test)]
  91. : [test];
  92. let current = node,
  93. listToCheck = conditionsToCheck.map(c =>
  94. splitByOr(c).map(splitByAnd),
  95. );
  96. while (
  97. current.parent &&
  98. current.parent.type === "IfStatement" &&
  99. current.parent.alternate === current
  100. ) {
  101. current = current.parent;
  102. const currentOrOperands = splitByOr(current.test).map(
  103. splitByAnd,
  104. );
  105. listToCheck = listToCheck.map(orOperands =>
  106. orOperands.filter(
  107. orOperand =>
  108. !currentOrOperands.some(currentOrOperand =>
  109. isSubset(currentOrOperand, orOperand),
  110. ),
  111. ),
  112. );
  113. if (
  114. listToCheck.some(orOperands => orOperands.length === 0)
  115. ) {
  116. context.report({ node: test, messageId: "unexpected" });
  117. break;
  118. }
  119. }
  120. },
  121. };
  122. },
  123. };