Skip to content

Commit f215f13

Browse files
committed
feat: support suggestions
1 parent 7919d9e commit f215f13

File tree

4 files changed

+235
-50
lines changed

4 files changed

+235
-50
lines changed

docs/rules/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +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 | | :hammer: |
240+
| [vue/no-negated-v-if-condition] | disallow negated conditions in v-if/v-else | :bulb: | :hammer: |
241241
| [vue/no-potential-component-option-typo] | disallow a potential typo in your component property | :bulb: | :hammer: |
242242
| [vue/no-ref-object-reactivity-loss] | disallow usages of ref objects that can lead to loss of reactivity | | :warning: |
243243
| [vue/no-restricted-block] | disallow specific block | | :hammer: |

docs/rules/no-negated-v-if-condition.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ description: disallow negated conditions in v-if/v-else
1010
> disallow negated conditions in v-if/v-else
1111
1212
- :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).
1314

1415
## :book: Rule Details
1516

@@ -22,26 +23,26 @@ Negated conditions make the code less readable. When there's an `else` clause, i
2223
```vue
2324
<template>
2425
<!-- ✓ GOOD -->
25-
<div v-if="foo" />
26-
<div v-else />
26+
<div v-if="foo">First</div>
27+
<div v-else>Second</div>
2728
28-
<div v-if="!foo" />
29-
<div v-else-if="bar" />
29+
<div v-if="!foo">First</div>
30+
<div v-else-if="bar">Second</div>
3031
31-
<div v-if="!foo" />
32+
<div v-if="!foo">Content</div>
3233
33-
<div v-if="a !== b" />
34+
<div v-if="a !== b">Not equal</div>
3435
3536
<!-- ✗ BAD -->
36-
<div v-if="!foo" />
37-
<div v-else />
37+
<div v-if="!foo">First</div>
38+
<div v-else>Second</div>
3839
39-
<div v-if="a !== b" />
40-
<div v-else />
40+
<div v-if="a !== b">First</div>
41+
<div v-else>Second</div>
4142
42-
<div v-if="foo" />
43-
<div v-else-if="!bar" />
44-
<div v-else />
43+
<div v-if="foo">First</div>
44+
<div v-else-if="!bar">Second</div>
45+
<div v-else>Third</div>
4546
</template>
4647
```
4748

lib/rules/no-negated-v-if-condition.js

Lines changed: 134 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
const utils = require('../utils')
88

99
/**
10-
* @param {*} expression
10+
* @typedef { VDirective & { value: (VExpressionContainer & { expression: Expression | null } ) | null } } VIfDirective
11+
*/
12+
13+
/**
14+
* @param {Expression} expression
1115
* @returns {boolean}
1216
*/
1317
function isNegatedExpression(expression) {
@@ -19,11 +23,11 @@ function isNegatedExpression(expression) {
1923
}
2024

2125
/**
22-
* @param {VElement} node The element node to get the next sibling element
23-
* @returns {VElement|null} The next sibling element
26+
* @param {VElement} node
27+
* @returns {VElement|null}
2428
*/
2529
function getNextSibling(node) {
26-
if (!node.parent || !node.parent.children) {
30+
if (!node.parent?.children) {
2731
return null
2832
}
2933

@@ -46,27 +50,7 @@ function getNextSibling(node) {
4650
*/
4751
function isDirectlyFollowedByElse(element) {
4852
const nextElement = getNextSibling(element)
49-
return !!(nextElement && utils.hasDirective(nextElement, 'else'))
50-
}
51-
52-
/**
53-
* @param {VDirective} node The directive node
54-
* @param {RuleContext} context The rule context
55-
*/
56-
function checkNegatedCondition(node, context) {
57-
if (!node.value || !node.value.expression) {
58-
return
59-
}
60-
61-
const expression = node.value.expression
62-
const element = node.parent.parent
63-
64-
if (isNegatedExpression(expression) && isDirectlyFollowedByElse(element)) {
65-
context.report({
66-
node: expression,
67-
messageId: 'negatedCondition'
68-
})
69-
}
53+
return nextElement ? utils.hasDirective(nextElement, 'else') : false
7054
}
7155

7256
module.exports = {
@@ -78,21 +62,141 @@ module.exports = {
7862
url: 'https://eslint.vuejs.org/rules/no-negated-v-if-condition.html'
7963
},
8064
fixable: null,
65+
hasSuggestions: true,
8166
schema: [],
8267
messages: {
83-
negatedCondition: 'Unexpected negated condition in v-if with v-else.'
68+
negatedCondition: 'Unexpected negated condition in v-if with v-else.',
69+
fixNegatedCondition:
70+
'Convert to positive condition and swap if/else blocks.'
8471
}
8572
},
8673
/** @param {RuleContext} context */
8774
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) return ''
157+
if (!element.endTag) return ''
158+
159+
const contentStart = element.startTag.range[1]
160+
const contentEnd = element.endTag.range[0]
161+
162+
return sourceCode.text.slice(contentStart, contentEnd)
163+
}
164+
165+
/**
166+
* @param {RuleFixer} fixer
167+
* @param {VElement} ifElement
168+
* @param {VElement} elseElement
169+
*/
170+
function* swapElementContents(fixer, ifElement, elseElement) {
171+
if (!ifElement.endTag || !elseElement.endTag) {
172+
return
173+
}
174+
175+
const ifContent = getElementContent(ifElement)
176+
const elseContent = getElementContent(elseElement)
177+
178+
if (ifContent === elseContent) {
179+
return
180+
}
181+
182+
yield fixer.replaceTextRange(
183+
[ifElement.startTag.range[1], ifElement.endTag.range[0]],
184+
elseContent
185+
)
186+
yield fixer.replaceTextRange(
187+
[elseElement.startTag.range[1], elseElement.endTag.range[0]],
188+
ifContent
189+
)
190+
}
191+
88192
return utils.defineTemplateBodyVisitor(context, {
89-
/** @param {VDirective} node */
193+
/** @param {VIfDirective} node */
90194
"VAttribute[directive=true][key.name.name='if']"(node) {
91-
checkNegatedCondition(node, context)
195+
checkNegatedCondition(node)
92196
},
93-
/** @param {VDirective} node */
197+
/** @param {VIfDirective} node */
94198
"VAttribute[directive=true][key.name.name='else-if']"(node) {
95-
checkNegatedCondition(node, context)
199+
checkNegatedCondition(node)
96200
}
97201
})
98202
}

tests/lib/rules/no-negated-v-if-condition.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,20 @@ tester.run('no-negated-v-if-condition', rule, {
157157
{
158158
messageId: 'negatedCondition',
159159
line: 3,
160-
column: 20
160+
column: 20,
161+
endLine: 3,
162+
endColumn: 24,
163+
suggestions: [
164+
{
165+
messageId: 'fixNegatedCondition',
166+
output: `
167+
<template>
168+
<div v-if="foo">Alternative</div>
169+
<div v-else>Content</div>
170+
</template>
171+
`
172+
}
173+
]
161174
}
162175
]
163176
},
@@ -173,7 +186,20 @@ tester.run('no-negated-v-if-condition', rule, {
173186
{
174187
messageId: 'negatedCondition',
175188
line: 3,
176-
column: 20
189+
column: 20,
190+
endLine: 3,
191+
endColumn: 33,
192+
suggestions: [
193+
{
194+
messageId: 'fixNegatedCondition',
195+
output: `
196+
<template>
197+
<div v-if="(foo && bar)">Otherwise</div>
198+
<div v-else>Negated condition</div>
199+
</template>
200+
`
201+
}
202+
]
177203
}
178204
]
179205
},
@@ -189,7 +215,20 @@ tester.run('no-negated-v-if-condition', rule, {
189215
{
190216
messageId: 'negatedCondition',
191217
line: 3,
192-
column: 20
218+
column: 20,
219+
endLine: 3,
220+
endColumn: 26,
221+
suggestions: [
222+
{
223+
messageId: 'fixNegatedCondition',
224+
output: `
225+
<template>
226+
<div v-if="a == b">Equal</div>
227+
<div v-else>Not equal</div>
228+
</template>
229+
`
230+
}
231+
]
193232
}
194233
]
195234
},
@@ -205,7 +244,20 @@ tester.run('no-negated-v-if-condition', rule, {
205244
{
206245
messageId: 'negatedCondition',
207246
line: 3,
208-
column: 20
247+
column: 20,
248+
endLine: 3,
249+
endColumn: 27,
250+
suggestions: [
251+
{
252+
messageId: 'fixNegatedCondition',
253+
output: `
254+
<template>
255+
<div v-if="a === b">Strictly equal</div>
256+
<div v-else>Strictly not equal</div>
257+
</template>
258+
`
259+
}
260+
]
209261
}
210262
]
211263
},
@@ -222,7 +274,21 @@ tester.run('no-negated-v-if-condition', rule, {
222274
{
223275
messageId: 'negatedCondition',
224276
line: 4,
225-
column: 25
277+
column: 25,
278+
endLine: 4,
279+
endColumn: 29,
280+
suggestions: [
281+
{
282+
messageId: 'fixNegatedCondition',
283+
output: `
284+
<template>
285+
<div v-if="foo">First</div>
286+
<div v-else-if="bar">Default</div>
287+
<div v-else>Second</div>
288+
</template>
289+
`
290+
}
291+
]
226292
}
227293
]
228294
},
@@ -239,7 +305,21 @@ tester.run('no-negated-v-if-condition', rule, {
239305
{
240306
messageId: 'negatedCondition',
241307
line: 4,
242-
column: 25
308+
column: 25,
309+
endLine: 4,
310+
endColumn: 27,
311+
suggestions: [
312+
{
313+
messageId: 'fixNegatedCondition',
314+
output: `
315+
<template>
316+
<div v-if="!a">First</div>
317+
<div v-else-if="b">Default</div>
318+
<div v-else>Second</div>
319+
</template>
320+
`
321+
}
322+
]
243323
}
244324
]
245325
}

0 commit comments

Comments
 (0)