Skip to content

Commit 79e13d1

Browse files
Fix siblings and interleaving issues reported in #2342 (#2348)
1 parent 0c6f87a commit 79e13d1

File tree

2 files changed

+199
-15
lines changed

2 files changed

+199
-15
lines changed

Diff for: lib/rules/v-if-else-key.js

+47-15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ const casing = require('../utils/casing')
2525
* @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one.
2626
*/
2727

28+
/**
29+
* Checks if a given node has sibling nodes of the same type that are also conditionally rendered.
30+
* This is used to determine if multiple instances of the same component are being conditionally
31+
* rendered within the same parent scope.
32+
*
33+
* @param {VElement} node - The Vue component node to check for conditional rendering siblings.
34+
* @param {string} componentName - The name of the component to check for sibling instances.
35+
* @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise.
36+
*/
37+
const hasConditionalRenderedSiblings = (node, componentName) => {
38+
if (!node.parent || node.parent.type !== 'VElement') {
39+
return false
40+
}
41+
return node.parent.children.some(
42+
(sibling) =>
43+
sibling !== node &&
44+
sibling.type === 'VElement' &&
45+
sibling.rawName === componentName &&
46+
hasConditionalDirective(sibling)
47+
)
48+
}
49+
2850
/**
2951
* Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing
3052
* and the node is part of a conditional family a report is generated.
@@ -44,26 +66,31 @@ const checkForKey = (
4466
uniqueKey,
4567
conditionalFamilies
4668
) => {
47-
if (node.parent && node.parent.type === 'VElement') {
48-
const conditionalFamily = conditionalFamilies.get(node.parent)
69+
if (
70+
!node.parent ||
71+
node.parent.type !== 'VElement' ||
72+
!hasConditionalRenderedSiblings(node, componentName)
73+
) {
74+
return
75+
}
4976

50-
if (
51-
conditionalFamily &&
52-
(utils.hasDirective(node, 'bind', 'key') ||
53-
utils.hasAttribute(node, 'key') ||
54-
!hasConditionalDirective(node) ||
55-
!(conditionalFamily.else || conditionalFamily.elseIf.length > 0))
56-
) {
57-
return
58-
}
77+
const conditionalFamily = conditionalFamilies.get(node.parent)
78+
79+
if (!conditionalFamily || utils.hasAttribute(node, 'key')) {
80+
return
81+
}
82+
83+
const needsKey =
84+
conditionalFamily.if === node ||
85+
conditionalFamily.else === node ||
86+
conditionalFamily.elseIf.includes(node)
5987

88+
if (needsKey) {
6089
context.report({
6190
node: node.startTag,
6291
loc: node.startTag.loc,
6392
messageId: 'requireKey',
64-
data: {
65-
componentName
66-
},
93+
data: { componentName },
6794
fix(fixer) {
6895
const afterComponentNamePosition =
6996
node.startTag.range[0] + componentName.length + 1
@@ -190,13 +217,18 @@ module.exports = {
190217
if (node.parent && node.parent.type === 'VElement') {
191218
let conditionalFamily = conditionalFamilies.get(node.parent)
192219

193-
if (conditionType === 'if' && !conditionalFamily) {
220+
if (!conditionalFamily) {
194221
conditionalFamily = createConditionalFamily(node)
195222
conditionalFamilies.set(node.parent, conditionalFamily)
196223
}
197224

198225
if (conditionalFamily) {
199226
switch (conditionType) {
227+
case 'if': {
228+
conditionalFamily = createConditionalFamily(node)
229+
conditionalFamilies.set(node.parent, conditionalFamily)
230+
break
231+
}
200232
case 'else-if': {
201233
conditionalFamily.elseIf.push(node)
202234
break

Diff for: tests/lib/rules/v-if-else-key.js

+152
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,90 @@ tester.run('v-if-else-key', rule, {
127127
}
128128
</script>
129129
`
130+
},
131+
{
132+
filename: 'test.vue',
133+
code: `
134+
<template>
135+
<div>
136+
<div>
137+
<ComponentA v-if="foo" />
138+
<ComponentB v-else-if="bar" />
139+
</div>
140+
<div>
141+
<div>
142+
<ComponentA v-if="foo" />
143+
<ComponentB v-else-if="baz" />
144+
</div>
145+
<div>
146+
<ComponentA v-if="foo" />
147+
<ComponentB v-else-if="baz" />
148+
</div>
149+
</div>
150+
<div>
151+
<ComponentA v-if="foo" />
152+
<ComponentB v-else />
153+
</div>
154+
<div>
155+
<div v-if="foo" />
156+
<ComponentB v-else />
157+
</div>
158+
</div>
159+
</template>
160+
<script>
161+
export default {
162+
components: {
163+
ComponentA,
164+
ComponentB
165+
}
166+
}
167+
</script>
168+
`
169+
},
170+
{
171+
filename: 'test.vue',
172+
code: `
173+
<template>
174+
<div>
175+
<div>
176+
<CustomComponent v-if="foo" />
177+
<div v-else />
178+
</div>
179+
180+
<CustomComponent v-if="bar" />
181+
</div>
182+
</template>
183+
<script>
184+
export default {
185+
components: {
186+
CustomComponent
187+
}
188+
}
189+
</script>
190+
`
191+
},
192+
{
193+
filename: 'test.vue',
194+
code: `
195+
<template>
196+
<tile>
197+
<template v-if="state === 'foo'">
198+
<ComponentA>…</ComponentA>
199+
<ComponentB>…</ComponentB>
200+
</template>
201+
<ComponentA v-else-if="state === 'bar'" key="a">…</ComponentA>
202+
<ComponentA v-else-if="state === 'bar'" key="b">…</ComponentA>
203+
</tile>
204+
</template>
205+
<script>
206+
export default {
207+
components: {
208+
ComponentA,
209+
ComponentB
210+
}
211+
}
212+
</script>
213+
`
130214
}
131215
],
132216
invalid: [
@@ -424,6 +508,74 @@ tester.run('v-if-else-key', rule, {
424508
line: 6
425509
}
426510
]
511+
},
512+
{
513+
filename: 'test.vue',
514+
code: `
515+
<template>
516+
<div>
517+
<ComponentA v-if="foo" />
518+
<ComponentA v-else />
519+
520+
<ComponentA v-if="bar" />
521+
<ComponentA v-else-if="baz" />
522+
<ComponentA v-else />
523+
</div>
524+
</template>
525+
<script>
526+
export default {
527+
components: {
528+
ComponentA
529+
}
530+
}
531+
</script>
532+
`,
533+
output: `
534+
<template>
535+
<div>
536+
<ComponentA key="component-a-1" v-if="foo" />
537+
<ComponentA key="component-a-2" v-else />
538+
539+
<ComponentA key="component-a-3" v-if="bar" />
540+
<ComponentA key="component-a-4" v-else-if="baz" />
541+
<ComponentA key="component-a-5" v-else />
542+
</div>
543+
</template>
544+
<script>
545+
export default {
546+
components: {
547+
ComponentA
548+
}
549+
}
550+
</script>
551+
`,
552+
errors: [
553+
{
554+
message:
555+
"Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
556+
line: 4
557+
},
558+
{
559+
message:
560+
"Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
561+
line: 5
562+
},
563+
{
564+
message:
565+
"Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
566+
line: 7
567+
},
568+
{
569+
message:
570+
"Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
571+
line: 8
572+
},
573+
{
574+
message:
575+
"Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
576+
line: 9
577+
}
578+
]
427579
}
428580
]
429581
})

0 commit comments

Comments
 (0)