no-dupe-else-if.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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 [...splitByLogicalOperator(operator, node.left), ...splitByLogicalOperator(operator, node.right)];
  32. }
  33. return [node];
  34. }
  35. const splitByOr = splitByLogicalOperator.bind(null, "||");
  36. const splitByAnd = splitByLogicalOperator.bind(null, "&&");
  37. //------------------------------------------------------------------------------
  38. // Rule Definition
  39. //------------------------------------------------------------------------------
  40. /** @type {import('../shared/types').Rule} */
  41. module.exports = {
  42. meta: {
  43. type: "problem",
  44. docs: {
  45. description: "Disallow duplicate conditions in if-else-if chains",
  46. recommended: true,
  47. url: "https://eslint.org/docs/rules/no-dupe-else-if"
  48. },
  49. schema: [],
  50. messages: {
  51. unexpected: "This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain."
  52. }
  53. },
  54. create(context) {
  55. const sourceCode = context.getSourceCode();
  56. /**
  57. * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
  58. * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
  59. * @param {ASTNode} a First node.
  60. * @param {ASTNode} b Second node.
  61. * @returns {boolean} `true` if the nodes are considered to be equal.
  62. */
  63. function equal(a, b) {
  64. if (a.type !== b.type) {
  65. return false;
  66. }
  67. if (
  68. a.type === "LogicalExpression" &&
  69. (a.operator === "||" || a.operator === "&&") &&
  70. a.operator === b.operator
  71. ) {
  72. return equal(a.left, b.left) && equal(a.right, b.right) ||
  73. equal(a.left, b.right) && equal(a.right, b.left);
  74. }
  75. return astUtils.equalTokens(a, b, sourceCode);
  76. }
  77. const isSubset = isSubsetByComparator.bind(null, equal);
  78. return {
  79. IfStatement(node) {
  80. const test = node.test,
  81. conditionsToCheck = test.type === "LogicalExpression" && test.operator === "&&"
  82. ? [test, ...splitByAnd(test)]
  83. : [test];
  84. let current = node,
  85. listToCheck = conditionsToCheck.map(c => splitByOr(c).map(splitByAnd));
  86. while (current.parent && current.parent.type === "IfStatement" && current.parent.alternate === current) {
  87. current = current.parent;
  88. const currentOrOperands = splitByOr(current.test).map(splitByAnd);
  89. listToCheck = listToCheck.map(orOperands => orOperands.filter(
  90. orOperand => !currentOrOperands.some(currentOrOperand => isSubset(currentOrOperand, orOperand))
  91. ));
  92. if (listToCheck.some(orOperands => orOperands.length === 0)) {
  93. context.report({ node: test, messageId: "unexpected" });
  94. break;
  95. }
  96. }
  97. }
  98. };
  99. }
  100. };