Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tired-emus-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-vue': minor
---

Fixed `no-negated-v-if-condition` rule to swap entire elements
148 changes: 82 additions & 66 deletions lib/rules/no-negated-v-if-condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand All @@ -100,94 +143,67 @@ 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',
suggest: [
{
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, {
Expand Down
Loading