Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit fec4e41

Browse files
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‎

Lines changed: 2 additions & 0 deletions
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‎

Lines changed: 98 additions & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 193 additions & 0 deletions
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‎

Lines changed: 26 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /