From b30c051cc3646b083ea0cea072d1306281fe4e72 Mon Sep 17 00:00:00 2001 From: waynzh Date: Thu, 10 Jul 2025 11:19:51 +0800 Subject: [PATCH 01/10] Add `vue/no-negated-condition` rule --- lib/rules/no-negated-condition.js | 99 ++++++++++ tests/lib/rules/no-negated-condition.js | 247 ++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 lib/rules/no-negated-condition.js create mode 100644 tests/lib/rules/no-negated-condition.js diff --git a/lib/rules/no-negated-condition.js b/lib/rules/no-negated-condition.js new file mode 100644 index 000000000..a300dbd95 --- /dev/null +++ b/lib/rules/no-negated-condition.js @@ -0,0 +1,99 @@ +/** + * @author Wayne + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +/** + * @param {*} expression + * @returns {boolean} + */ +function isNegatedExpression(expression) { + return ( + (expression.type === 'UnaryExpression' && expression.operator === '!') || + (expression.type === 'BinaryExpression' && + (expression.operator === '!=' || expression.operator === '!==')) + ) +} + +/** + * @param {VElement} node The element node to get the next sibling element + * @returns {VElement|null} The next sibling element + */ +function getNextSibling(node) { + if (!node.parent || !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')) +} + +/** + * @param {VDirective} node The directive node + * @param {RuleContext} context The rule context + */ +function checkNegatedCondition(node, context) { + if (!node.value || !node.value.expression) { + return + } + + const expression = node.value.expression + const element = node.parent.parent + + if (isNegatedExpression(expression) && isDirectlyFollowedByElse(element)) { + context.report({ + node: expression, + messageId: 'negatedCondition' + }) + } +} + +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-condition.html' + }, + fixable: null, + schema: [], + messages: { + negatedCondition: 'Unexpected negated condition in v-if with v-else.' + } + }, + /** @param {RuleContext} context */ + create(context) { + return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective} node */ + "VAttribute[directive=true][key.name.name='if']"(node) { + checkNegatedCondition(node, context) + }, + /** @param {VDirective} node */ + "VAttribute[directive=true][key.name.name='else-if']"(node) { + checkNegatedCondition(node, context) + } + }) + } +} diff --git a/tests/lib/rules/no-negated-condition.js b/tests/lib/rules/no-negated-condition.js new file mode 100644 index 000000000..b96a38f56 --- /dev/null +++ b/tests/lib/rules/no-negated-condition.js @@ -0,0 +1,247 @@ +/** + * @author Wayne + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/no-negated-condition') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('no-negated-condition', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 4, + column: 25 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 4, + column: 25 + } + ] + } + ] +}) From c7ded94a913c9fc6116be0e026c3dcb9b1523285 Mon Sep 17 00:00:00 2001 From: waynzh Date: Thu, 10 Jul 2025 11:28:59 +0800 Subject: [PATCH 02/10] docs: update --- docs/rules/index.md | 2 + docs/rules/no-negated-condition.md | 61 ++++++++++++++++++++++++++++++ lib/index.js | 1 + 3 files changed, 64 insertions(+) create mode 100644 docs/rules/no-negated-condition.md diff --git a/docs/rules/index.md b/docs/rules/index.md index 149f877ad..2675b13e5 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -237,6 +237,7 @@ For example: | [vue/no-empty-component-block] | disallow the `