Skip to content

Commit 391e88a

Browse files
committed
feat(analyze/html/vue): add useVueVForKey
1 parent d45faf3 commit 391e88a

File tree

13 files changed

+346
-24
lines changed

13 files changed

+346
-24
lines changed

.changeset/add-v-for-key-rule.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the rule [`useVueVForKey`](https://biomejs.dev/linter/rules/use-vue-v-for-key/), which enforces that any element using `v-for` also specifies a `key`.
6+
7+
**Invalid**
8+
9+
```vue
10+
<li v-for="item in items">{{ item }}</li>
11+
```
12+
13+
**Valid**
14+
15+
```vue
16+
<li v-for="item in items" :key="item.id">{{ item }}</li>
17+
```

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 44 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod no_vue_v_if_with_v_for;
1010
pub mod use_vue_consistent_v_bind_style;
1111
pub mod use_vue_consistent_v_on_style;
1212
pub mod use_vue_hyphenated_attributes;
13+
pub mod use_vue_v_for_key;
1314
pub mod use_vue_valid_template_root;
1415
pub mod use_vue_valid_v_bind;
1516
pub mod use_vue_valid_v_cloak;
@@ -21,4 +22,4 @@ pub mod use_vue_valid_v_on;
2122
pub mod use_vue_valid_v_once;
2223
pub mod use_vue_valid_v_pre;
2324
pub mod use_vue_valid_v_text;
24-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_consistent_v_bind_style :: UseVueConsistentVBindStyle , self :: use_vue_consistent_v_on_style :: UseVueConsistentVOnStyle , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_template_root :: UseVueValidTemplateRoot , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_cloak :: UseVueValidVCloak , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_once :: UseVueValidVOnce , self :: use_vue_valid_v_pre :: UseVueValidVPre , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
25+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_consistent_v_bind_style :: UseVueConsistentVBindStyle , self :: use_vue_consistent_v_on_style :: UseVueConsistentVOnStyle , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_v_for_key :: UseVueVForKey , self :: use_vue_valid_template_root :: UseVueValidTemplateRoot , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_cloak :: UseVueValidVCloak , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_once :: UseVueValidVOnce , self :: use_vue_valid_v_pre :: UseVueValidVPre , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_html_syntax::{AnyVueDirective, HtmlAttributeList, VueDirectiveArgument};
6+
use biome_rowan::{AstNode, AstNodeList, TextRange};
7+
use biome_rule_options::use_vue_v_for_key::UseVueVForKeyOptions;
8+
9+
declare_lint_rule! {
10+
/// Enforce that elements using `v-for` also specify a unique `key`.
11+
///
12+
/// When rendering lists with `v-for`, Vue relies on a `key` to track elements efficiently.
13+
/// The `key` can be provided via longhand `v-bind:key` or shorthand `:key`.
14+
///
15+
/// ## Examples
16+
///
17+
/// ### Invalid
18+
///
19+
/// ```vue,expect_diagnostic
20+
/// <li v-for="item in items">{{ item }}</li>
21+
/// ```
22+
///
23+
/// ### Valid
24+
///
25+
/// ```vue
26+
/// <li v-for="item in items" :key="item.id">{{ item }}</li>
27+
/// ```
28+
///
29+
/// ```vue
30+
/// <li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
31+
/// ```
32+
///
33+
pub UseVueVForKey {
34+
version: "next",
35+
name: "useVueVForKey",
36+
language: "html",
37+
recommended: true,
38+
domains: &[RuleDomain::Vue],
39+
sources: &[RuleSource::EslintVueJs("require-v-for-key").same()],
40+
}
41+
}
42+
43+
impl Rule for UseVueVForKey {
44+
type Query = Ast<HtmlAttributeList>;
45+
type State = TextRange;
46+
type Signals = Option<Self::State>;
47+
type Options = UseVueVForKeyOptions;
48+
49+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
50+
let attrs = ctx.query();
51+
let mut has_v_for = None;
52+
let mut has_key = false;
53+
for attr in attrs.iter() {
54+
if let Some(dir_any) = attr.as_any_vue_directive() {
55+
match dir_any {
56+
AnyVueDirective::VueDirective(dir) => {
57+
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-for") {
58+
has_v_for = Some(dir.range());
59+
}
60+
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-bind")
61+
&& let Some(arg) = dir.arg()
62+
&& is_v_bind_key_argument(&arg)
63+
{
64+
has_key = true;
65+
}
66+
}
67+
AnyVueDirective::VueVBindShorthandDirective(sh) => {
68+
if let Ok(arg) = sh.arg()
69+
&& is_v_bind_key_argument(&arg)
70+
{
71+
has_key = true;
72+
}
73+
}
74+
_ => {}
75+
}
76+
if has_v_for.is_some() && has_key {
77+
// early exit if both found
78+
return None;
79+
}
80+
}
81+
}
82+
if let (Some(v_for_range), false) = (has_v_for, has_key) {
83+
return Some(v_for_range);
84+
}
85+
None
86+
}
87+
88+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
89+
Some(
90+
RuleDiagnostic::new(
91+
rule_category!(),
92+
state,
93+
markup! {
94+
"This element is using "<Emphasis>"v-for"</Emphasis>", but the "<Emphasis>"key"</Emphasis>" is missing."
95+
},
96+
)
97+
.note(markup! {
98+
"Using a unique key with "<Emphasis>"v-for"</Emphasis>" helps Vue optimize rendering and track elements efficiently. Failing to provide a key can result in unexpected behavior during updates. For example, if you want to do transition animations, using a key is necessary so Vue knows which element to apply the animation to."
99+
})
100+
.note(markup! {
101+
"Provide the key using "<Emphasis>":key=\"value\""</Emphasis>", and have the value be a unique value from the items you are iterating over. For example: `<li v-for=\"item in items\" :key=\"item.id\">{{ item }}</li>`"
102+
}),
103+
)
104+
}
105+
}
106+
107+
#[inline(always)]
108+
fn is_v_bind_key_argument(arg: &VueDirectiveArgument) -> bool {
109+
let Ok(arg) = arg.arg() else {
110+
return false;
111+
};
112+
let Some(static_arg) = arg.as_vue_static_argument() else {
113+
return false;
114+
};
115+
static_arg
116+
.name_token()
117+
.is_ok_and(|name| name.text() == "key")
118+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- should generate diagnostics -->
2+
<ul>
3+
<li v-for="item in items">{{ item }}</li>
4+
</ul>
5+
6+
<div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>
7+
8+
<template v-for="item in items">
9+
<li>{{ item }}</li>
10+
</template>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
source: crates/biome_html_analyze/tests/spec_tests.rs
3+
expression: invalid.vue
4+
---
5+
# Input
6+
```html
7+
<!-- should generate diagnostics -->
8+
<ul>
9+
<li v-for="item in items">{{ item }}</li>
10+
</ul>
11+
12+
<div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>
13+
14+
<template v-for="item in items">
15+
<li>{{ item }}</li>
16+
</template>
17+
18+
```
19+
20+
# Diagnostics
21+
```
22+
invalid.vue:3:7 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
24+
i This element is using v-for, but the key is missing.
25+
26+
1 │ <!-- should generate diagnostics -->
27+
2 │ <ul>
28+
> 3 │ <li v-for="item in items">{{ item }}</li>
29+
│ ^^^^^^^^^^^^^^^^^^^^^
30+
4 │ </ul>
31+
5 │
32+
33+
i Using a unique key with v-for helps Vue optimize rendering and track elements efficiently. Failing to provide a key can result in unexpected behavior during updates. For example, if you want to do transition animations, using a key is necessary so Vue knows which element to apply the animation to.
34+
35+
i Provide the key using :key="value", and have the value be a unique value from the items you are iterating over. For example: `<li v-for="item in items" :key="item.id">{{ item }}</li>`
36+
37+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
38+
39+
40+
```
41+
42+
```
43+
invalid.vue:6:22 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44+
45+
i This element is using v-for, but the key is missing.
46+
47+
4 │ </ul>
48+
5 │
49+
> 6 │ <div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>
50+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
51+
7 │
52+
8 │ <template v-for="item in items">
53+
54+
i Using a unique key with v-for helps Vue optimize rendering and track elements efficiently. Failing to provide a key can result in unexpected behavior during updates. For example, if you want to do transition animations, using a key is necessary so Vue knows which element to apply the animation to.
55+
56+
i Provide the key using :key="value", and have the value be a unique value from the items you are iterating over. For example: `<li v-for="item in items" :key="item.id">{{ item }}</li>`
57+
58+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
59+
60+
61+
```
62+
63+
```
64+
invalid.vue:8:11 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65+
66+
i This element is using v-for, but the key is missing.
67+
68+
6 <div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>
69+
7 │
70+
> 8 │ <template v-for="item in items">
71+
│ ^^^^^^^^^^^^^^^^^^^^^
72+
9 │ <li>{{ item }}</li>
73+
10 │ </template>
74+
75+
i Using a unique key with v-for helps Vue optimize rendering and track elements efficiently. Failing to provide a key can result in unexpected behavior during updates. For example, if you want to do transition animations, using a key is necessary so Vue knows which element to apply the animation to.
76+
77+
i Provide the key using :key="value", and have the value be a unique value from the items you are iterating over. For example: `<li v-for="item in items" :key="item.id">{{ item }}</li>`
78+
79+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
80+
81+
82+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- should not generate diagnostics -->
2+
<ul>
3+
<li v-for="item in items" :key="item.id">{{ item }}</li>
4+
</ul>
5+
6+
<ul>
7+
<li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
8+
</ul>
9+
10+
<div v-for="(item, i) in items" :key="i"></div>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
source: crates/biome_html_analyze/tests/spec_tests.rs
3+
expression: valid.vue
4+
---
5+
# Input
6+
```html
7+
<!-- should not generate diagnostics -->
8+
<ul>
9+
<li v-for="item in items" :key="item.id">{{ item }}</li>
10+
</ul>
11+
12+
<ul>
13+
<li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
14+
</ul>
15+
16+
<div v-for="(item, i) in items" :key="i"></div>
17+
18+
```

crates/biome_rule_options/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ pub mod use_vue_consistent_v_on_style;
402402
pub mod use_vue_define_macros_order;
403403
pub mod use_vue_hyphenated_attributes;
404404
pub mod use_vue_multi_word_component_names;
405+
pub mod use_vue_v_for_key;
405406
pub mod use_vue_valid_template_root;
406407
pub mod use_vue_valid_v_bind;
407408
pub mod use_vue_valid_v_cloak;

0 commit comments

Comments
 (0)