Skip to content

Commit fec4e41

Browse files
authored
Add vue/no-dupe-v-else-if rule (#1239)
1 parent cc8450d commit fec4e41

File tree

9 files changed

+1072
-16
lines changed

9 files changed

+1072
-16
lines changed

docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
5858
| [vue/no-deprecated-v-on-number-modifiers](./no-deprecated-v-on-number-modifiers.md) | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: |
5959
| [vue/no-deprecated-vue-config-keycodes](./no-deprecated-vue-config-keycodes.md) | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) | |
6060
| [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | |
61+
| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | |
6162
| [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | |
6263
| [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | |
6364
| [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | |
@@ -171,6 +172,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
171172
| [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | |
172173
| [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | |
173174
| [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | |
175+
| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | |
174176
| [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | |
175177
| [vue/no-multiple-template-root](./no-multiple-template-root.md) | disallow adding multiple root nodes to the template | |
176178
| [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | |

docs/rules/no-dupe-v-else-if.md

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-dupe-v-else-if
5+
description: disallow duplicate conditions in `v-if` / `v-else-if` chains
6+
---
7+
# vue/no-dupe-v-else-if
8+
> disallow duplicate conditions in `v-if` / `v-else-if` chains
9+
10+
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
11+
12+
## :book: Rule Details
13+
14+
This rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain.
15+
16+
<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}">
17+
18+
```vue
19+
<template>
20+
<!-- ✗ BAD -->
21+
<div v-if="isSomething(x)" />
22+
<div v-else-if="isSomething(x)" />
23+
24+
<div v-if="a" />
25+
<div v-else-if="b" />
26+
<div v-else-if="c && d" />
27+
<div v-else-if="c && d" />
28+
29+
<div v-if="n === 1" />
30+
<div v-else-if="n === 2" />
31+
<div v-else-if="n === 3" />
32+
<div v-else-if="n === 2" />
33+
<div v-else-if="n === 5" />
34+
35+
<!-- ✓ GOOD -->
36+
<div v-if="isSomething(x)" />
37+
<div v-else-if="isSomethingElse(x)" />
38+
39+
<div v-if="a" />
40+
<div v-else-if="b" />
41+
<div v-else-if="c && d" />
42+
<div v-else-if="c && e" />
43+
44+
<div v-if="n === 1" />
45+
<div v-else-if="n === 2" />
46+
<div v-else-if="n === 3" />
47+
<div v-else-if="n === 4" />
48+
<div v-else-if="n === 5" />
49+
</template>
50+
```
51+
52+
</eslint-code-block>
53+
54+
This rule can also detect some cases where the conditions are not identical, but the branch can never execute due to the logic of `||` and `&&` operators.
55+
56+
<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}">
57+
58+
```vue
59+
<template>
60+
<!-- ✗ BAD -->
61+
<div v-if="a || b" />
62+
<div v-else-if="a" />
63+
64+
<div v-if="a" />
65+
<div v-else-if="b" />
66+
<div v-else-if="a || b" />
67+
68+
<div v-if="a" />
69+
<div v-else-if="a && b" />
70+
71+
<div v-if="a && b" />
72+
<div v-else-if="a && b && c" />
73+
74+
<div v-if="a || b" />
75+
<div v-else-if="b && c" />
76+
77+
<div v-if="a" />
78+
<div v-else-if="b && c" />
79+
<div v-else-if="d && (c && e && b || a)" />
80+
</template>
81+
```
82+
83+
</eslint-code-block>
84+
85+
## :wrench: Options
86+
87+
Nothing.
88+
89+
## :couple: Related rules
90+
91+
- [no-dupe-else-if]
92+
93+
[no-dupe-else-if]: https://eslint.org/docs/rules/no-dupe-else-if
94+
95+
## :mag: Implementation
96+
97+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-v-else-if.js)
98+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-dupe-v-else-if.js)

lib/configs/essential.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
'vue/no-async-in-computed-properties': 'error',
1212
'vue/no-custom-modifiers-on-v-model': 'error',
1313
'vue/no-dupe-keys': 'error',
14+
'vue/no-dupe-v-else-if': 'error',
1415
'vue/no-duplicate-attributes': 'error',
1516
'vue/no-multiple-template-root': 'error',
1617
'vue/no-mutating-props': 'error',

lib/configs/vue3-essential.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
'vue/no-deprecated-v-on-number-modifiers': 'error',
2727
'vue/no-deprecated-vue-config-keycodes': 'error',
2828
'vue/no-dupe-keys': 'error',
29+
'vue/no-dupe-v-else-if': 'error',
2930
'vue/no-duplicate-attributes': 'error',
3031
'vue/no-lifecycle-after-await': 'error',
3132
'vue/no-mutating-props': 'error',

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module.exports = {
6767
'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
6868
'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'),
6969
'no-dupe-keys': require('./rules/no-dupe-keys'),
70+
'no-dupe-v-else-if': require('./rules/no-dupe-v-else-if'),
7071
'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
7172
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
7273
'no-empty-component-block': require('./rules/no-empty-component-block'),

lib/rules/no-dupe-v-else-if.js

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
/**
18+
* @typedef {NonNullable<VExpressionContainer['expression']>} VExpression
19+
*/
20+
/**
21+
* @typedef {object} OrOperands
22+
* @property {VExpression} OrOperands.node
23+
* @property {AndOperands[]} OrOperands.operands
24+
*
25+
* @typedef {object} AndOperands
26+
* @property {VExpression} AndOperands.node
27+
* @property {VExpression[]} AndOperands.operands
28+
*/
29+
/**
30+
* Splits the given node by the given logical operator.
31+
* @param {string} operator Logical operator `||` or `&&`.
32+
* @param {VExpression} node The node to split.
33+
* @returns {VExpression[]} 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 [
38+
...splitByLogicalOperator(operator, node.left),
39+
...splitByLogicalOperator(operator, node.right)
40+
]
41+
}
42+
return [node]
43+
}
44+
45+
/**
46+
* @param {VExpression} node
47+
*/
48+
function splitByOr(node) {
49+
return splitByLogicalOperator('||', node)
50+
}
51+
/**
52+
* @param {VExpression} node
53+
*/
54+
function splitByAnd(node) {
55+
return splitByLogicalOperator('&&', node)
56+
}
57+
58+
/**
59+
* @param {VExpression} node
60+
* @returns {OrOperands}
61+
*/
62+
function buildOrOperands(node) {
63+
const orOperands = splitByOr(node)
64+
return {
65+
node,
66+
operands: orOperands.map((orOperand) => {
67+
const andOperands = splitByAnd(orOperand)
68+
return {
69+
node: orOperand,
70+
operands: andOperands
71+
}
72+
})
73+
}
74+
}
75+
76+
// ------------------------------------------------------------------------------
77+
// Rule Definition
78+
// ------------------------------------------------------------------------------
79+
80+
module.exports = {
81+
meta: {
82+
type: 'problem',
83+
docs: {
84+
description:
85+
'disallow duplicate conditions in `v-if` / `v-else-if` chains',
86+
categories: ['vue3-essential', 'essential'],
87+
url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html'
88+
},
89+
fixable: null,
90+
schema: [],
91+
messages: {
92+
unexpected:
93+
'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.'
94+
}
95+
},
96+
/** @param {RuleContext} context */
97+
create(context) {
98+
const tokenStore =
99+
context.parserServices.getTemplateBodyTokenStore &&
100+
context.parserServices.getTemplateBodyTokenStore()
101+
/**
102+
* Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
103+
* represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
104+
* @param {VExpression} a First node.
105+
* @param {VExpression} b Second node.
106+
* @returns {boolean} `true` if the nodes are considered to be equal.
107+
*/
108+
function equal(a, b) {
109+
if (a.type !== b.type) {
110+
return false
111+
}
112+
113+
if (
114+
a.type === 'LogicalExpression' &&
115+
b.type === 'LogicalExpression' &&
116+
(a.operator === '||' || a.operator === '&&') &&
117+
a.operator === b.operator
118+
) {
119+
return (
120+
(equal(a.left, b.left) && equal(a.right, b.right)) ||
121+
(equal(a.left, b.right) && equal(a.right, b.left))
122+
)
123+
}
124+
125+
return utils.equalTokens(a, b, tokenStore)
126+
}
127+
128+
/**
129+
* Determines whether the first given AndOperands is a subset of the second given AndOperands.
130+
*
131+
* e.g. A: (a && b), B: (a && b && c): B is a subset of A.
132+
*
133+
* @param {AndOperands} operandsA The AndOperands to compare from.
134+
* @param {AndOperands} operandsB The AndOperands to compare against.
135+
* @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
136+
*/
137+
function isSubset(operandsA, operandsB) {
138+
return operandsA.operands.every((operandA) =>
139+
operandsB.operands.some((operandB) => equal(operandA, operandB))
140+
)
141+
}
142+
143+
return utils.defineTemplateBodyVisitor(context, {
144+
"VAttribute[directive=true][key.name.name='else-if']"(node) {
145+
if (!node.value || !node.value.expression) {
146+
return
147+
}
148+
const test = node.value.expression
149+
const conditionsToCheck =
150+
test.type === 'LogicalExpression' && test.operator === '&&'
151+
? [...splitByAnd(test), test]
152+
: [test]
153+
const listToCheck = conditionsToCheck.map(buildOrOperands)
154+
155+
/** @type {VElement | null} */
156+
let current = node.parent.parent
157+
while (current && (current = utils.prevSibling(current))) {
158+
const vIf = utils.getDirective(current, 'if')
159+
const currentTestDir = vIf || utils.getDirective(current, 'else-if')
160+
if (!currentTestDir) {
161+
return
162+
}
163+
if (currentTestDir.value && currentTestDir.value.expression) {
164+
const currentOrOperands = buildOrOperands(
165+
currentTestDir.value.expression
166+
)
167+
168+
for (const condition of listToCheck) {
169+
const operands = (condition.operands = condition.operands.filter(
170+
(orOperand) => {
171+
return !currentOrOperands.operands.some((currentOrOperand) =>
172+
isSubset(currentOrOperand, orOperand)
173+
)
174+
}
175+
))
176+
if (!operands.length) {
177+
context.report({
178+
node: condition.node,
179+
messageId: 'unexpected'
180+
})
181+
return
182+
}
183+
}
184+
}
185+
186+
if (vIf) {
187+
return
188+
}
189+
}
190+
}
191+
})
192+
}
193+
}

lib/utils/index.js

+26
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,32 @@ module.exports = {
15301530

15311531
return null
15321532
}
1533+
},
1534+
1535+
/**
1536+
* Checks whether or not the tokens of two given nodes are same.
1537+
* @param {ASTNode} left A node 1 to compare.
1538+
* @param {ASTNode} right A node 2 to compare.
1539+
* @param {ParserServices.TokenStore | SourceCode} sourceCode The ESLint source code object.
1540+
* @returns {boolean} the source code for the given node.
1541+
*/
1542+
equalTokens(left, right, sourceCode) {
1543+
const tokensL = sourceCode.getTokens(left)
1544+
const tokensR = sourceCode.getTokens(right)
1545+
1546+
if (tokensL.length !== tokensR.length) {
1547+
return false
1548+
}
1549+
for (let i = 0; i < tokensL.length; ++i) {
1550+
if (
1551+
tokensL[i].type !== tokensR[i].type ||
1552+
tokensL[i].value !== tokensR[i].value
1553+
) {
1554+
return false
1555+
}
1556+
}
1557+
1558+
return true
15331559
}
15341560
}
15351561

0 commit comments

Comments
 (0)