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