Skip to content
Open
Show file tree
Hide file tree
Changes from all 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