Skip to content

Commit 02890f3

Browse files
waynzhFloEdelmann
andauthored
Add vue/no-negated-v-if-condition rule (#2794)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent d2b201a commit 02890f3

File tree

7 files changed

+604
-0
lines changed

7 files changed

+604
-0
lines changed

.changeset/early-worlds-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-vue': minor
3+
---
4+
5+
Added new [`vue/no-negated-v-if-condition`](https://eslint.vuejs.org/rules/no-negated-v-if-condition.html) rule

docs/rules/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ For example:
237237
| [vue/no-empty-component-block] | disallow the `<template>` `<script>` `<style>` block to be empty | :wrench: | :hammer: |
238238
| [vue/no-import-compiler-macros] | disallow importing Vue compiler macros | :wrench: | :warning: |
239239
| [vue/no-multiple-objects-in-class] | disallow passing multiple objects in an array to class | | :hammer: |
240+
| [vue/no-negated-v-if-condition] | disallow negated conditions in v-if/v-else | :bulb: | :hammer: |
240241
| [vue/no-potential-component-option-typo] | disallow a potential typo in your component property | :bulb: | :hammer: |
241242
| [vue/no-ref-object-reactivity-loss] | disallow usages of ref objects that can lead to loss of reactivity | | :warning: |
242243
| [vue/no-restricted-block] | disallow specific block | | :hammer: |
@@ -484,6 +485,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
484485
[vue/no-multiple-template-root]: ./no-multiple-template-root.md
485486
[vue/no-mutating-props]: ./no-mutating-props.md
486487
[vue/no-negated-condition]: ./no-negated-condition.md
488+
[vue/no-negated-v-if-condition]: ./no-negated-v-if-condition.md
487489
[vue/no-parsing-error]: ./no-parsing-error.md
488490
[vue/no-potential-component-option-typo]: ./no-potential-component-option-typo.md
489491
[vue/no-ref-as-operand]: ./no-ref-as-operand.md

docs/rules/no-negated-condition.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This rule is the same rule as core [no-negated-condition] rule but it applies to
1717

1818
## :couple: Related Rules
1919

20+
- [`vue/no-negated-v-if-condition`](https://eslint.vuejs.org/rules/no-negated-v-if-condition.html)
2021
- [unicorn/no-negated-condition](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-negated-condition.md)
2122

2223
## :books: Further Reading
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-negated-v-if-condition
5+
description: disallow negated conditions in v-if/v-else
6+
---
7+
8+
# vue/no-negated-v-if-condition
9+
10+
> disallow negated conditions in v-if/v-else
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
13+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
14+
15+
## :book: Rule Details
16+
17+
This rule disallows negated conditions in `v-if` and `v-else-if` directives which have an `v-else` branch.
18+
19+
Negated conditions make the code less readable. When there's an `else` clause, it's better to use a positive condition and switch the branches.
20+
21+
<eslint-code-block :rules="{'vue/no-negated-v-if-condition': ['error']}">
22+
23+
```vue
24+
<template>
25+
<!-- ✓ GOOD -->
26+
<div v-if="foo">First</div>
27+
<div v-else>Second</div>
28+
29+
<div v-if="!foo">First</div>
30+
<div v-else-if="bar">Second</div>
31+
32+
<div v-if="!foo">Content</div>
33+
34+
<div v-if="a !== b">Not equal</div>
35+
36+
<!-- ✗ BAD -->
37+
<div v-if="!foo">First</div>
38+
<div v-else>Second</div>
39+
40+
<div v-if="a !== b">First</div>
41+
<div v-else>Second</div>
42+
43+
<div v-if="foo">First</div>
44+
<div v-else-if="!bar">Second</div>
45+
<div v-else>Third</div>
46+
</template>
47+
```
48+
49+
</eslint-code-block>
50+
51+
## :wrench: Options
52+
53+
Nothing.
54+
55+
## :couple: Related Rules
56+
57+
- [no-negated-condition](https://eslint.org/docs/latest/rules/no-negated-condition)
58+
- [vue/no-negated-condition](https://eslint.vuejs.org/rules/no-negated-condition.html)
59+
- [unicorn/no-negated-condition](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-negated-condition.md)
60+
61+
## :mag: Implementation
62+
63+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-negated-v-if-condition.js)
64+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-negated-v-if-condition.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const plugin = {
142142
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
143143
'no-mutating-props': require('./rules/no-mutating-props'),
144144
'no-negated-condition': require('./rules/no-negated-condition'),
145+
'no-negated-v-if-condition': require('./rules/no-negated-v-if-condition'),
145146
'no-parsing-error': require('./rules/no-parsing-error'),
146147
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
147148
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @author Wayne Zhang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/**
10+
* @typedef { VDirective & { value: (VExpressionContainer & { expression: Expression | null } ) | null } } VIfDirective
11+
*/
12+
13+
/**
14+
* @param {Expression} expression
15+
* @returns {boolean}
16+
*/
17+
function isNegatedExpression(expression) {
18+
return (
19+
(expression.type === 'UnaryExpression' && expression.operator === '!') ||
20+
(expression.type === 'BinaryExpression' &&
21+
(expression.operator === '!=' || expression.operator === '!=='))
22+
)
23+
}
24+
25+
/**
26+
* @param {VElement} node
27+
* @returns {VElement|null}
28+
*/
29+
function getNextSibling(node) {
30+
if (!node.parent?.children) {
31+
return null
32+
}
33+
34+
const siblings = node.parent.children
35+
const currentIndex = siblings.indexOf(node)
36+
37+
for (let i = currentIndex + 1; i < siblings.length; i++) {
38+
const sibling = siblings[i]
39+
if (sibling.type === 'VElement') {
40+
return sibling
41+
}
42+
}
43+
44+
return null
45+
}
46+
47+
/**
48+
* @param {VElement} element
49+
* @returns {boolean}
50+
*/
51+
function isDirectlyFollowedByElse(element) {
52+
const nextElement = getNextSibling(element)
53+
return nextElement ? utils.hasDirective(nextElement, 'else') : false
54+
}
55+
56+
module.exports = {
57+
meta: {
58+
type: 'suggestion',
59+
docs: {
60+
description: 'disallow negated conditions in v-if/v-else',
61+
categories: undefined,
62+
url: 'https://eslint.vuejs.org/rules/no-negated-v-if-condition.html'
63+
},
64+
fixable: null,
65+
hasSuggestions: true,
66+
schema: [],
67+
messages: {
68+
negatedCondition: 'Unexpected negated condition in v-if with v-else.',
69+
fixNegatedCondition:
70+
'Convert to positive condition and swap if/else blocks.'
71+
}
72+
},
73+
/** @param {RuleContext} context */
74+
create(context) {
75+
const sourceCode = context.getSourceCode()
76+
const templateTokens =
77+
sourceCode.parserServices.getTemplateBodyTokenStore &&
78+
sourceCode.parserServices.getTemplateBodyTokenStore()
79+
80+
/**
81+
* @param {VIfDirective} node
82+
*/
83+
function checkNegatedCondition(node) {
84+
if (!node.value?.expression) {
85+
return
86+
}
87+
88+
const expression = node.value.expression
89+
const element = node.parent.parent
90+
91+
if (
92+
!isNegatedExpression(expression) ||
93+
!isDirectlyFollowedByElse(element)
94+
) {
95+
return
96+
}
97+
98+
const elseElement = getNextSibling(element)
99+
if (!elseElement) {
100+
return
101+
}
102+
103+
context.report({
104+
node: expression,
105+
messageId: 'negatedCondition',
106+
suggest: [
107+
{
108+
messageId: 'fixNegatedCondition',
109+
*fix(fixer) {
110+
yield* convertNegatedCondition(fixer, expression)
111+
yield* swapElementContents(fixer, element, elseElement)
112+
}
113+
}
114+
]
115+
})
116+
}
117+
118+
/**
119+
* @param {RuleFixer} fixer
120+
* @param {Expression} expression
121+
*/
122+
function* convertNegatedCondition(fixer, expression) {
123+
if (
124+
expression.type === 'UnaryExpression' &&
125+
expression.operator === '!'
126+
) {
127+
const token = templateTokens.getFirstToken(expression)
128+
if (token?.type === 'Punctuator' && token.value === '!') {
129+
yield fixer.remove(token)
130+
}
131+
return
132+
}
133+
134+
if (expression.type === 'BinaryExpression') {
135+
const operatorToken = templateTokens.getTokenAfter(
136+
expression.left,
137+
(token) =>
138+
token?.type === 'Punctuator' && token.value === expression.operator
139+
)
140+
141+
if (!operatorToken) return
142+
143+
if (expression.operator === '!=') {
144+
yield fixer.replaceText(operatorToken, '==')
145+
} else if (expression.operator === '!==') {
146+
yield fixer.replaceText(operatorToken, '===')
147+
}
148+
}
149+
}
150+
151+
/**
152+
* @param {VElement} element
153+
* @returns {string}
154+
*/
155+
function getElementContent(element) {
156+
if (element.children.length === 0 || !element.endTag) {
157+
return ''
158+
}
159+
160+
const contentStart = element.startTag.range[1]
161+
const contentEnd = element.endTag.range[0]
162+
163+
return sourceCode.text.slice(contentStart, contentEnd)
164+
}
165+
166+
/**
167+
* @param {RuleFixer} fixer
168+
* @param {VElement} ifElement
169+
* @param {VElement} elseElement
170+
*/
171+
function* swapElementContents(fixer, ifElement, elseElement) {
172+
if (!ifElement.endTag || !elseElement.endTag) {
173+
return
174+
}
175+
176+
const ifContent = getElementContent(ifElement)
177+
const elseContent = getElementContent(elseElement)
178+
179+
if (ifContent === elseContent) {
180+
return
181+
}
182+
183+
yield fixer.replaceTextRange(
184+
[ifElement.startTag.range[1], ifElement.endTag.range[0]],
185+
elseContent
186+
)
187+
yield fixer.replaceTextRange(
188+
[elseElement.startTag.range[1], elseElement.endTag.range[0]],
189+
ifContent
190+
)
191+
}
192+
193+
return utils.defineTemplateBodyVisitor(context, {
194+
/** @param {VIfDirective} node */
195+
"VAttribute[directive=true][key.name.name='if']"(node) {
196+
checkNegatedCondition(node)
197+
},
198+
/** @param {VIfDirective} node */
199+
"VAttribute[directive=true][key.name.name='else-if']"(node) {
200+
checkNegatedCondition(node)
201+
}
202+
})
203+
}
204+
}

0 commit comments

Comments
 (0)