-
-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix siblings and interleaving issues reported in #2342 #2348
base: master
Are you sure you want to change the base?
Changes from all commits
61e93d8
96f8a3e
f74541d
0923acc
e978c9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,33 @@ const casing = require('../utils/casing') | |
* @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one. | ||
*/ | ||
|
||
/** | ||
* Checks if a given node has sibling nodes of the same type that are also conditionally rendered. | ||
* This is used to determine if multiple instances of the same component are being conditionally | ||
* rendered within the same parent scope. | ||
* | ||
* @param {VElement} node - The Vue component node to check for conditional rendering siblings. | ||
* @param {string} componentName - The name of the component to check for sibling instances. | ||
* @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise. | ||
*/ | ||
const hasConditionalRenderedSiblings = (node, componentName) => { | ||
if (node.parent && node.parent.type === 'VElement') { | ||
const siblings = node.parent.children.filter( | ||
(child) => child.type === 'VElement' | ||
) | ||
|
||
return siblings.some( | ||
(sibling) => | ||
sibling !== node && | ||
sibling.type === 'VElement' && | ||
sibling.rawName === componentName && | ||
hasConditionalDirective(sibling) | ||
) | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing | ||
* and the node is part of a conditional family a report is generated. | ||
|
@@ -44,35 +71,39 @@ const checkForKey = ( | |
uniqueKey, | ||
conditionalFamilies | ||
) => { | ||
if (node.parent && node.parent.type === 'VElement') { | ||
if ( | ||
node.parent && | ||
node.parent.type === 'VElement' && | ||
hasConditionalRenderedSiblings(node, componentName) | ||
) { | ||
const conditionalFamily = conditionalFamilies.get(node.parent) | ||
|
||
if ( | ||
conditionalFamily && | ||
(utils.hasDirective(node, 'bind', 'key') || | ||
utils.hasAttribute(node, 'key') || | ||
!hasConditionalDirective(node) || | ||
!(conditionalFamily.else || conditionalFamily.elseIf.length > 0)) | ||
) { | ||
return | ||
} | ||
if (conditionalFamily && !utils.hasAttribute(node, 'key')) { | ||
let needsKey = false | ||
|
||
context.report({ | ||
node: node.startTag, | ||
loc: node.startTag.loc, | ||
messageId: 'requireKey', | ||
data: { | ||
componentName | ||
}, | ||
fix(fixer) { | ||
const afterComponentNamePosition = | ||
node.startTag.range[0] + componentName.length + 1 | ||
return fixer.insertTextBeforeRange( | ||
[afterComponentNamePosition, afterComponentNamePosition], | ||
` key="${uniqueKey}"` | ||
) | ||
if (node === conditionalFamily.if || node === conditionalFamily.else) { | ||
needsKey = true | ||
} else if (conditionalFamily.elseIf.includes(node)) { | ||
needsKey = true | ||
} | ||
Comment on lines
+82
to
88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please replace with a single const: const needsKey =
conditionalFamily.if === node ||
conditionalFamily.else === node ||
conditionalFamily.elseIf.includes(node) |
||
}) | ||
|
||
if (needsKey) { | ||
context.report({ | ||
node: node.startTag, | ||
loc: node.startTag.loc, | ||
messageId: 'requireKey', | ||
data: { componentName }, | ||
fix(fixer) { | ||
const afterComponentNamePosition = | ||
node.startTag.range[0] + componentName.length + 1 | ||
return fixer.insertTextBeforeRange( | ||
[afterComponentNamePosition, afterComponentNamePosition], | ||
` key="${uniqueKey}"` | ||
) | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -189,7 +220,7 @@ module.exports = { | |
if (node.parent && node.parent.type === 'VElement') { | ||
let conditionalFamily = conditionalFamilies.get(node.parent) | ||
|
||
if (conditionType === 'if' && !conditionalFamily) { | ||
if (conditionType === 'if') { | ||
conditionalFamily = createConditionalFamily(node) | ||
conditionalFamilies.set(node.parent, conditionalFamily) | ||
} | ||
|
@@ -222,44 +253,55 @@ module.exports = { | |
const currentScope = componentUsageStack[componentUsageStack.length - 1] | ||
const usageInfo = currentScope.get(componentName) || { | ||
count: 0, | ||
firstNode: null | ||
firstNode: null, | ||
hasIf: false | ||
} | ||
|
||
if (hasConditionalDirective(node)) { | ||
// Store the first node if this is the first occurrence | ||
if (usageInfo.count === 0) { | ||
usageInfo.firstNode = node | ||
} | ||
const isIfDirective = utils.getDirective(node, 'if') !== null | ||
const isConditional = | ||
isIfDirective || | ||
utils.getDirective(node, 'else-if') !== null || | ||
utils.getDirective(node, 'else') !== null | ||
|
||
if (usageInfo.count > 0) { | ||
const uniqueKey = `${casing.kebabCase(componentName)}-${ | ||
usageInfo.count + 1 | ||
}` | ||
checkForKey( | ||
node, | ||
context, | ||
componentName, | ||
uniqueKey, | ||
conditionalFamilies | ||
) | ||
|
||
// If this is the second occurrence, also apply a fix to the first occurrence | ||
if (usageInfo.count === 1) { | ||
const uniqueKeyForFirstInstance = `${casing.kebabCase( | ||
componentName | ||
)}-1` | ||
if (isConditional) { | ||
if (isIfDirective && usageInfo.hasIf) { | ||
// Reset if family already has an 'if' directive | ||
usageInfo.count = 1 | ||
usageInfo.firstNode = node | ||
Comment on lines
+268
to
+270
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this work for the following case? Please also add this as a test case: <ComponentA v-if="foo" />
<ComponentA v-else />
<ComponentA v-if="bar" />
<ComponentA v-else-if="baz" />
<ComponentA v-else /> An error should be reported for all of them. |
||
} else { | ||
usageInfo.hasIf = true | ||
if (usageInfo.count === 0) { | ||
usageInfo.firstNode = node | ||
} else if (usageInfo.count > 0 && usageInfo.firstNode !== node) { | ||
const uniqueKey = `${casing.kebabCase(componentName)}-${ | ||
usageInfo.count + 1 | ||
}` | ||
checkForKey( | ||
usageInfo.firstNode, | ||
node, | ||
context, | ||
componentName, | ||
uniqueKeyForFirstInstance, | ||
uniqueKey, | ||
conditionalFamilies | ||
) | ||
|
||
if (usageInfo.count === 1) { | ||
const uniqueKeyForFirstInstance = `${casing.kebabCase( | ||
componentName | ||
)}-1` | ||
checkForKey( | ||
usageInfo.firstNode, | ||
context, | ||
componentName, | ||
uniqueKeyForFirstInstance, | ||
conditionalFamilies | ||
) | ||
} | ||
} | ||
usageInfo.count++ | ||
} | ||
usageInfo.count += 1 | ||
currentScope.set(componentName, usageInfo) | ||
} | ||
|
||
componentUsageStack.push(new Map()) | ||
pushedNodes.add(node) | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we need to check the element's type anyway in the
some
predicate, then we can avoidfilter
ing the list before.