Skip to content
2 changes: 2 additions & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ For example:
| [vue/no-empty-component-block] | disallow the `<template>` `<script>` `<style>` block to be empty | :wrench: | :hammer: |
| [vue/no-import-compiler-macros] | disallow importing Vue compiler macros | :wrench: | :warning: |
| [vue/no-multiple-objects-in-class] | disallow passing multiple objects in an array to class | | :hammer: |
| [vue/no-negated-v-if-condition] | disallow negated conditions in v-if/v-else | :bulb: | :hammer: |
| [vue/no-potential-component-option-typo] | disallow a potential typo in your component property | :bulb: | :hammer: |
| [vue/no-ref-object-reactivity-loss] | disallow usages of ref objects that can lead to loss of reactivity | | :warning: |
| [vue/no-restricted-block] | disallow specific block | | :hammer: |
Expand Down Expand Up @@ -482,6 +483,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
[vue/no-multiple-slot-args]: ./no-multiple-slot-args.md
[vue/no-multiple-template-root]: ./no-multiple-template-root.md
[vue/no-mutating-props]: ./no-mutating-props.md
[vue/no-negated-v-if-condition]: ./no-negated-v-if-condition.md
[vue/no-parsing-error]: ./no-parsing-error.md
[vue/no-potential-component-option-typo]: ./no-potential-component-option-typo.md
[vue/no-ref-as-operand]: ./no-ref-as-operand.md
Expand Down
62 changes: 62 additions & 0 deletions docs/rules/no-negated-v-if-condition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-negated-v-if-condition
description: disallow negated conditions in v-if/v-else
---

# vue/no-negated-v-if-condition

> disallow negated conditions in v-if/v-else

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :book: Rule Details

This rule disallows negated conditions in `v-if` and `v-else-if` directives which have an `v-else` branch.

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.

<eslint-code-block :rules="{'vue/no-negated-v-if-condition': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<div v-if="foo">First</div>
<div v-else>Second</div>

<div v-if="!foo">First</div>
<div v-else-if="bar">Second</div>

<div v-if="!foo">Content</div>

<div v-if="a !== b">Not equal</div>

<!-- ✗ BAD -->
<div v-if="!foo">First</div>
<div v-else>Second</div>

<div v-if="a !== b">First</div>
<div v-else>Second</div>

<div v-if="foo">First</div>
<div v-else-if="!bar">Second</div>
<div v-else>Third</div>
</template>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :couple: Related Rules

- [no-negated-v-if-condition](https://eslint.org/docs/rules/no-negated-v-if-condition)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-negated-v-if-condition.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-negated-v-if-condition.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const plugin = {
'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
'no-mutating-props': require('./rules/no-mutating-props'),
'no-negated-v-if-condition': require('./rules/no-negated-v-if-condition'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
Expand Down
203 changes: 203 additions & 0 deletions lib/rules/no-negated-v-if-condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* @author Wayne Zhang
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../utils')

/**
* @typedef { VDirective & { value: (VExpressionContainer & { expression: Expression | null } ) | null } } VIfDirective
*/

/**
* @param {Expression} expression
* @returns {boolean}
*/
function isNegatedExpression(expression) {
return (
(expression.type === 'UnaryExpression' && expression.operator === '!') ||
(expression.type === 'BinaryExpression' &&
(expression.operator === '!=' || expression.operator === '!=='))
)
}

/**
* @param {VElement} node
* @returns {VElement|null}
*/
function getNextSibling(node) {
if (!node.parent?.children) {
return null
}

const siblings = node.parent.children
const currentIndex = siblings.indexOf(node)

for (let i = currentIndex + 1; i < siblings.length; i++) {
const sibling = siblings[i]
if (sibling.type === 'VElement') {
return sibling
}
}

return null
}

/**
* @param {VElement} element
* @returns {boolean}
*/
function isDirectlyFollowedByElse(element) {
const nextElement = getNextSibling(element)
return nextElement ? utils.hasDirective(nextElement, 'else') : false
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow negated conditions in v-if/v-else',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-negated-v-if-condition.html'
},
fixable: null,
hasSuggestions: true,
schema: [],
messages: {
negatedCondition: 'Unexpected negated condition in v-if with v-else.',
fixNegatedCondition:
'Convert to positive condition and swap if/else blocks.'
}
},
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const templateTokens =
sourceCode.parserServices.getTemplateBodyTokenStore &&
sourceCode.parserServices.getTemplateBodyTokenStore()

/**
* @param {VIfDirective} node
*/
function checkNegatedCondition(node) {
if (!node.value?.expression) {
return
}

const expression = node.value.expression
const element = node.parent.parent

if (
!isNegatedExpression(expression) ||
!isDirectlyFollowedByElse(element)
) {
return
}

const elseElement = getNextSibling(element)
if (!elseElement) {
return
}

context.report({
node: expression,
messageId: 'negatedCondition',
suggest: [
{
messageId: 'fixNegatedCondition',
*fix(fixer) {
yield* convertNegatedCondition(fixer, expression)
yield* swapElementContents(fixer, element, elseElement)
}
}
]
})
}

/**
* @param {RuleFixer} fixer
* @param {Expression} expression
*/
function* convertNegatedCondition(fixer, expression) {
if (
expression.type === 'UnaryExpression' &&
expression.operator === '!'
) {
const token = templateTokens.getFirstToken(expression)
if (token?.type === 'Punctuator' && token.value === '!') {
yield fixer.remove(token)
}
return
}

if (expression.type === 'BinaryExpression') {
const operatorToken = templateTokens.getTokenAfter(
expression.left,
(token) =>
token?.type === 'Punctuator' && token.value === expression.operator
)

if (!operatorToken) return

if (expression.operator === '!=') {
yield fixer.replaceText(operatorToken, '==')
} else if (expression.operator === '!==') {
yield fixer.replaceText(operatorToken, '===')
}
}
}

/**
* @param {VElement} element
* @returns {string}
*/
function getElementContent(element) {
if (element.children.length === 0) return ''
if (!element.endTag) return ''

const contentStart = element.startTag.range[1]
const contentEnd = element.endTag.range[0]

return sourceCode.text.slice(contentStart, contentEnd)
}

/**
* @param {RuleFixer} fixer
* @param {VElement} ifElement
* @param {VElement} elseElement
*/
function* swapElementContents(fixer, ifElement, elseElement) {
if (!ifElement.endTag || !elseElement.endTag) {
return
}

const ifContent = getElementContent(ifElement)
const elseContent = getElementContent(elseElement)

if (ifContent === elseContent) {
return
}

yield fixer.replaceTextRange(
[ifElement.startTag.range[1], ifElement.endTag.range[0]],
elseContent
)
yield fixer.replaceTextRange(
[elseElement.startTag.range[1], elseElement.endTag.range[0]],
ifContent
)
}

return utils.defineTemplateBodyVisitor(context, {
/** @param {VIfDirective} node */
"VAttribute[directive=true][key.name.name='if']"(node) {
checkNegatedCondition(node)
},
/** @param {VIfDirective} node */
"VAttribute[directive=true][key.name.name='else-if']"(node) {
checkNegatedCondition(node)
}
})
}
}
Loading