Skip to content

Commit

Permalink
Add vue/no-dupe-v-else-if rule (#1239)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jul 14, 2020
1 parent cc8450d commit fec4e41
Show file tree
Hide file tree
Showing 9 changed files with 1,072 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/rules/README.md
Expand Up @@ -58,6 +58,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [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: |
| [vue/no-deprecated-vue-config-keycodes](./no-deprecated-vue-config-keycodes.md) | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) | |
| [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | |
| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | |
| [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | |
| [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | |
| [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | |
Expand Down Expand Up @@ -171,6 +172,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | |
| [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | |
| [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | |
| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | |
| [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | |
| [vue/no-multiple-template-root](./no-multiple-template-root.md) | disallow adding multiple root nodes to the template | |
| [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | |
Expand Down
98 changes: 98 additions & 0 deletions docs/rules/no-dupe-v-else-if.md
@@ -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)
1 change: 1 addition & 0 deletions lib/configs/essential.js
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'vue/no-async-in-computed-properties': 'error',
'vue/no-custom-modifiers-on-v-model': 'error',
'vue/no-dupe-keys': 'error',
'vue/no-dupe-v-else-if': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-multiple-template-root': 'error',
'vue/no-mutating-props': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue3-essential.js
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'vue/no-deprecated-v-on-number-modifiers': 'error',
'vue/no-deprecated-vue-config-keycodes': 'error',
'vue/no-dupe-keys': 'error',
'vue/no-dupe-v-else-if': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-lifecycle-after-await': 'error',
'vue/no-mutating-props': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -67,6 +67,7 @@ module.exports = {
'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'),
'no-dupe-keys': require('./rules/no-dupe-keys'),
'no-dupe-v-else-if': require('./rules/no-dupe-v-else-if'),
'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-empty-component-block': require('./rules/no-empty-component-block'),
Expand Down
193 changes: 193 additions & 0 deletions lib/rules/no-dupe-v-else-if.js
@@ -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
}
}
}
})
}
}
26 changes: 26 additions & 0 deletions lib/utils/index.js
Expand Up @@ -1530,6 +1530,32 @@ module.exports = {

return null
}
},

/**
* Checks whether or not the tokens of two given nodes are same.
* @param {ASTNode} left A node 1 to compare.
* @param {ASTNode} right A node 2 to compare.
* @param {ParserServices.TokenStore | SourceCode} sourceCode The ESLint source code object.
* @returns {boolean} the source code for the given node.
*/
equalTokens(left, right, sourceCode) {
const tokensL = sourceCode.getTokens(left)
const tokensR = sourceCode.getTokens(right)

if (tokensL.length !== tokensR.length) {
return false
}
for (let i = 0; i < tokensL.length; ++i) {
if (
tokensL[i].type !== tokensR[i].type ||
tokensL[i].value !== tokensR[i].value
) {
return false
}
}

return true
}
}

Expand Down

0 comments on commit fec4e41

Please sign in to comment.