diff --git a/.changeset/tired-emus-matter.md b/.changeset/tired-emus-matter.md new file mode 100644 index 000000000..6ca7123cb --- /dev/null +++ b/.changeset/tired-emus-matter.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-vue': minor +--- + +Fixed `no-negated-v-if-condition` rule to swap entire elements diff --git a/lib/rules/no-negated-v-if-condition.js b/lib/rules/no-negated-v-if-condition.js index 73ee877c0..e0fec2546 100644 --- a/lib/rules/no-negated-v-if-condition.js +++ b/lib/rules/no-negated-v-if-condition.js @@ -53,6 +53,21 @@ function isDirectlyFollowedByElse(element) { return nextElement ? utils.hasDirective(nextElement, 'else') : false } +/** + * @param {VElement} element + */ +function getDirective(element) { + return /** @type {VIfDirective|undefined} */ ( + element.startTag.attributes.find( + (attr) => + attr.directive && + attr.key.name && + attr.key.name.name && + ['if', 'else-if', 'else'].includes(attr.key.name.name) + ) + ) +} + module.exports = { meta: { type: 'suggestion', @@ -73,9 +88,37 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() - const templateTokens = - sourceCode.parserServices.getTemplateBodyTokenStore && - sourceCode.parserServices.getTemplateBodyTokenStore() + + const processedPairs = new Set() + + /** + * @param {Expression} expression + * @returns {string} + */ + function getConvertedCondition(expression) { + if ( + expression.type === 'UnaryExpression' && + expression.operator === '!' + ) { + return sourceCode.text.slice( + expression.range[0] + 1, + expression.range[1] + ) + } + + if (expression.type === 'BinaryExpression') { + const left = sourceCode.getText(expression.left) + const right = sourceCode.getText(expression.right) + + if (expression.operator === '!=') { + return `${left} == ${right}` + } else if (expression.operator === '!==') { + return `${left} === ${right}` + } + } + + return sourceCode.getText(expression) + } /** * @param {VIfDirective} node @@ -100,6 +143,12 @@ module.exports = { return } + const pairKey = `${element.range[0]}-${elseElement.range[0]}` + if (processedPairs.has(pairKey)) { + return + } + processedPairs.add(pairKey) + context.report({ node: expression, messageId: 'negatedCondition', @@ -107,87 +156,54 @@ module.exports = { { messageId: 'fixNegatedCondition', *fix(fixer) { - yield* convertNegatedCondition(fixer, expression) - yield* swapElementContents(fixer, element, elseElement) + yield* swapElements(fixer, element, elseElement, expression) } } ] }) } - /** - * @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 || !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 + * @param {Expression} expression */ - function* swapElementContents(fixer, ifElement, elseElement) { - if (!ifElement.endTag || !elseElement.endTag) { - return - } + function* swapElements(fixer, ifElement, elseElement, expression) { + const convertedCondition = getConvertedCondition(expression) - const ifContent = getElementContent(ifElement) - const elseContent = getElementContent(elseElement) + const ifDir = getDirective(ifElement) + const elseDir = getDirective(elseElement) - if (ifContent === elseContent) { + if (!ifDir || !elseDir) { return } - yield fixer.replaceTextRange( - [ifElement.startTag.range[1], ifElement.endTag.range[0]], - elseContent + const ifDirectiveName = ifDir.key.name.name + + const ifText = sourceCode.text.slice( + ifElement.range[0], + ifElement.range[1] ) - yield fixer.replaceTextRange( - [elseElement.startTag.range[1], elseElement.endTag.range[0]], - ifContent + const elseText = sourceCode.text.slice( + elseElement.range[0], + elseElement.range[1] ) + + const newIfDirective = `v-${ifDirectiveName}="${convertedCondition}"` + const newIfText = + elseText.slice(0, elseDir.range[0] - elseElement.range[0]) + + newIfDirective + + elseText.slice(elseDir.range[1] - elseElement.range[0]) + + const newElseDirective = 'v-else' + const newElseText = + ifText.slice(0, ifDir.range[0] - ifElement.range[0]) + + newElseDirective + + ifText.slice(ifDir.range[1] - ifElement.range[0]) + + yield fixer.replaceTextRange(ifElement.range, newIfText) + yield fixer.replaceTextRange(elseElement.range, newElseText) } return utils.defineTemplateBodyVisitor(context, { diff --git a/tests/lib/rules/no-negated-v-if-condition.js b/tests/lib/rules/no-negated-v-if-condition.js index ee60562ea..fc0e02d0d 100644 --- a/tests/lib/rules/no-negated-v-if-condition.js +++ b/tests/lib/rules/no-negated-v-if-condition.js @@ -149,8 +149,8 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ @@ -165,8 +165,8 @@ tester.run('no-negated-v-if-condition', rule, { messageId: 'fixNegatedCondition', output: ` ` } @@ -178,8 +178,8 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ @@ -194,8 +194,8 @@ tester.run('no-negated-v-if-condition', rule, { messageId: 'fixNegatedCondition', output: ` ` } @@ -265,26 +265,26 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ { messageId: 'negatedCondition', line: 4, - column: 25, + column: 26, endLine: 4, - endColumn: 29, + endColumn: 30, suggestions: [ { messageId: 'fixNegatedCondition', output: ` ` } @@ -296,26 +296,144 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ { messageId: 'negatedCondition', line: 4, - column: 25, + column: 26, endLine: 4, - endColumn: 27, + endColumn: 28, suggestions: [ { messageId: 'fixNegatedCondition', output: ` + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20, + endLine: 3, + endColumn: 30, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20, + endLine: 3, + endColumn: 26, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + }, + { + messageId: 'negatedCondition', + line: 4, + column: 23, + endLine: 4, + endColumn: 29, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + }, + { + messageId: 'negatedCondition', + line: 8, + column: 23, + endLine: 8, + endColumn: 30, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + ` }