Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
vue/no-dupe-v-else-if
rule (#1239)
- Loading branch information
Showing
9 changed files
with
1,072 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
--- | ||
pageClass: rule-details | ||
sidebarDepth: 0 | ||
title: vue/no-dupe-v-else-if | ||
description: disallow duplicate conditions in `v-if` / `v-else-if` chains | ||
--- | ||
# vue/no-dupe-v-else-if | ||
> disallow duplicate conditions in `v-if` / `v-else-if` chains | ||
- :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"`. | ||
|
||
## :book: Rule Details | ||
|
||
This rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain. | ||
|
||
<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}"> | ||
|
||
```vue | ||
<template> | ||
<!-- ✗ BAD --> | ||
<div v-if="isSomething(x)" /> | ||
<div v-else-if="isSomething(x)" /> | ||
<div v-if="a" /> | ||
<div v-else-if="b" /> | ||
<div v-else-if="c && d" /> | ||
<div v-else-if="c && d" /> | ||
<div v-if="n === 1" /> | ||
<div v-else-if="n === 2" /> | ||
<div v-else-if="n === 3" /> | ||
<div v-else-if="n === 2" /> | ||
<div v-else-if="n === 5" /> | ||
<!-- ✓ GOOD --> | ||
<div v-if="isSomething(x)" /> | ||
<div v-else-if="isSomethingElse(x)" /> | ||
<div v-if="a" /> | ||
<div v-else-if="b" /> | ||
<div v-else-if="c && d" /> | ||
<div v-else-if="c && e" /> | ||
<div v-if="n === 1" /> | ||
<div v-else-if="n === 2" /> | ||
<div v-else-if="n === 3" /> | ||
<div v-else-if="n === 4" /> | ||
<div v-else-if="n === 5" /> | ||
</template> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
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. | ||
|
||
<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}"> | ||
|
||
```vue | ||
<template> | ||
<!-- ✗ BAD --> | ||
<div v-if="a || b" /> | ||
<div v-else-if="a" /> | ||
<div v-if="a" /> | ||
<div v-else-if="b" /> | ||
<div v-else-if="a || b" /> | ||
<div v-if="a" /> | ||
<div v-else-if="a && b" /> | ||
<div v-if="a && b" /> | ||
<div v-else-if="a && b && c" /> | ||
<div v-if="a || b" /> | ||
<div v-else-if="b && c" /> | ||
<div v-if="a" /> | ||
<div v-else-if="b && c" /> | ||
<div v-else-if="d && (c && e && b || a)" /> | ||
</template> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
## :wrench: Options | ||
|
||
Nothing. | ||
|
||
## :couple: Related rules | ||
|
||
- [no-dupe-else-if] | ||
|
||
[no-dupe-else-if]: https://eslint.org/docs/rules/no-dupe-else-if | ||
|
||
## :mag: Implementation | ||
|
||
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-v-else-if.js) | ||
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-dupe-v-else-if.js) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/** | ||
* @author Yosuke Ota | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
'use strict' | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Requirements | ||
// ------------------------------------------------------------------------------ | ||
|
||
const utils = require('../utils') | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Helpers | ||
// ------------------------------------------------------------------------------ | ||
|
||
/** | ||
* @typedef {NonNullable<VExpressionContainer['expression']>} VExpression | ||
*/ | ||
/** | ||
* @typedef {object} OrOperands | ||
* @property {VExpression} OrOperands.node | ||
* @property {AndOperands[]} OrOperands.operands | ||
* | ||
* @typedef {object} AndOperands | ||
* @property {VExpression} AndOperands.node | ||
* @property {VExpression[]} AndOperands.operands | ||
*/ | ||
/** | ||
* Splits the given node by the given logical operator. | ||
* @param {string} operator Logical operator `||` or `&&`. | ||
* @param {VExpression} node The node to split. | ||
* @returns {VExpression[]} Array of conditions that makes the node when joined by the operator. | ||
*/ | ||
function splitByLogicalOperator(operator, node) { | ||
if (node.type === 'LogicalExpression' && node.operator === operator) { | ||
return [ | ||
...splitByLogicalOperator(operator, node.left), | ||
...splitByLogicalOperator(operator, node.right) | ||
] | ||
} | ||
return [node] | ||
} | ||
|
||
/** | ||
* @param {VExpression} node | ||
*/ | ||
function splitByOr(node) { | ||
return splitByLogicalOperator('||', node) | ||
} | ||
/** | ||
* @param {VExpression} node | ||
*/ | ||
function splitByAnd(node) { | ||
return splitByLogicalOperator('&&', node) | ||
} | ||
|
||
/** | ||
* @param {VExpression} node | ||
* @returns {OrOperands} | ||
*/ | ||
function buildOrOperands(node) { | ||
const orOperands = splitByOr(node) | ||
return { | ||
node, | ||
operands: orOperands.map((orOperand) => { | ||
const andOperands = splitByAnd(orOperand) | ||
return { | ||
node: orOperand, | ||
operands: andOperands | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'disallow duplicate conditions in `v-if` / `v-else-if` chains', | ||
categories: ['vue3-essential', 'essential'], | ||
url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html' | ||
}, | ||
fixable: null, | ||
schema: [], | ||
messages: { | ||
unexpected: | ||
'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.' | ||
} | ||
}, | ||
/** @param {RuleContext} context */ | ||
create(context) { | ||
const tokenStore = | ||
context.parserServices.getTemplateBodyTokenStore && | ||
context.parserServices.getTemplateBodyTokenStore() | ||
/** | ||
* Determines whether the two given nodes are considered to be equal. In particular, given that the nodes | ||
* represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators. | ||
* @param {VExpression} a First node. | ||
* @param {VExpression} b Second node. | ||
* @returns {boolean} `true` if the nodes are considered to be equal. | ||
*/ | ||
function equal(a, b) { | ||
if (a.type !== b.type) { | ||
return false | ||
} | ||
|
||
if ( | ||
a.type === 'LogicalExpression' && | ||
b.type === 'LogicalExpression' && | ||
(a.operator === '||' || a.operator === '&&') && | ||
a.operator === b.operator | ||
) { | ||
return ( | ||
(equal(a.left, b.left) && equal(a.right, b.right)) || | ||
(equal(a.left, b.right) && equal(a.right, b.left)) | ||
) | ||
} | ||
|
||
return utils.equalTokens(a, b, tokenStore) | ||
} | ||
|
||
/** | ||
* Determines whether the first given AndOperands is a subset of the second given AndOperands. | ||
* | ||
* e.g. A: (a && b), B: (a && b && c): B is a subset of A. | ||
* | ||
* @param {AndOperands} operandsA The AndOperands to compare from. | ||
* @param {AndOperands} operandsB The AndOperands to compare against. | ||
* @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`. | ||
*/ | ||
function isSubset(operandsA, operandsB) { | ||
return operandsA.operands.every((operandA) => | ||
operandsB.operands.some((operandB) => equal(operandA, operandB)) | ||
) | ||
} | ||
|
||
return utils.defineTemplateBodyVisitor(context, { | ||
"VAttribute[directive=true][key.name.name='else-if']"(node) { | ||
if (!node.value || !node.value.expression) { | ||
return | ||
} | ||
const test = node.value.expression | ||
const conditionsToCheck = | ||
test.type === 'LogicalExpression' && test.operator === '&&' | ||
? [...splitByAnd(test), test] | ||
: [test] | ||
const listToCheck = conditionsToCheck.map(buildOrOperands) | ||
|
||
/** @type {VElement | null} */ | ||
let current = node.parent.parent | ||
while (current && (current = utils.prevSibling(current))) { | ||
const vIf = utils.getDirective(current, 'if') | ||
const currentTestDir = vIf || utils.getDirective(current, 'else-if') | ||
if (!currentTestDir) { | ||
return | ||
} | ||
if (currentTestDir.value && currentTestDir.value.expression) { | ||
const currentOrOperands = buildOrOperands( | ||
currentTestDir.value.expression | ||
) | ||
|
||
for (const condition of listToCheck) { | ||
const operands = (condition.operands = condition.operands.filter( | ||
(orOperand) => { | ||
return !currentOrOperands.operands.some((currentOrOperand) => | ||
isSubset(currentOrOperand, orOperand) | ||
) | ||
} | ||
)) | ||
if (!operands.length) { | ||
context.report({ | ||
node: condition.node, | ||
messageId: 'unexpected' | ||
}) | ||
return | ||
} | ||
} | ||
} | ||
|
||
if (vIf) { | ||
return | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.