Skip to content

Commit 9a8c98d

Browse files
authored
feat(analyze/html/vue): add useVueVForKey (#8587)
<!-- IMPORTANT!! If you generated this PR with the help of any AI assistance, please disclose it in the PR. https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#ai-assistance-notice --> <!-- Thanks for submitting a Pull Request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your PR. Once created, your PR will be automatically labeled according to changed files. Learn more about contributing: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md --> ## Summary <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve?--> This adds useVueVForKey, a port of https://eslint.vuejs.org/rules/require-v-for-key.html The first draft was written by AI, but I effectively rewrote the whole thing myself. <!-- Link any relevant issues if necessary or include a transcript of any Discord discussion. --> <!-- If you create a user-facing change, please write a changeset: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#writing-a-changeset (your changeset is often a good starting point for this summary as well) --> ## Test Plan <!-- What demonstrates that your implementation is correct? --> snapshots ## Docs <!-- If you're submitting a new rule or action (or an option for them), the documentation is part of the code. Make sure rules and actions have example usages, and that all options are documented. --> <!-- For other features, please submit a documentation PR to the `next` branch of our website: https://github.com/biomejs/website/. Link the PR here once it's ready. -->
1 parent e295960 commit 9a8c98d

File tree

13 files changed

+349
-24
lines changed

13 files changed

+349
-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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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`. If you need to
14+
/// animate the entrance/exit of an item in a list, the key should be a unique identifier for
15+
/// each item in the list, and not the index of the item.
16+
///
17+
/// For more information, see the Vue documentation on [list rendering](https://vuejs.org/guide/essentials/list#maintaining-state-with-key).
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```vue,expect_diagnostic
24+
/// <li v-for="item in items">{{ item }}</li>
25+
/// ```
26+
///
27+
/// ### Valid
28+
///
29+
/// ```vue
30+
/// <li v-for="item in items" :key="item.id">{{ item }}</li>
31+
/// ```
32+
///
33+
/// ```vue
34+
/// <li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
35+
/// ```
36+
///
37+
pub UseVueVForKey {
38+
version: "next",
39+
name: "useVueVForKey",
40+
language: "html",
41+
recommended: true,
42+
domains: &[RuleDomain::Vue],
43+
sources: &[RuleSource::EslintVueJs("require-v-for-key").same()],
44+
}
45+
}
46+
47+
impl Rule for UseVueVForKey {
48+
type Query = Ast<HtmlAttributeList>;
49+
type State = TextRange;
50+
type Signals = Option<Self::State>;
51+
type Options = UseVueVForKeyOptions;
52+
53+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
54+
let attrs = ctx.query();
55+
let mut has_v_for = None;
56+
let mut has_key = false;
57+
for attr in attrs.iter() {
58+
if let Some(dir_any) = attr.as_any_vue_directive() {
59+
match dir_any {
60+
AnyVueDirective::VueDirective(dir) => {
61+
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-for") {
62+
has_v_for = Some(dir.range());
63+
}
64+
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-bind")
65+
&& let Some(arg) = dir.arg()
66+
&& is_v_bind_key_argument(&arg)
67+
{
68+
has_key = true;
69+
}
70+
}
71+
AnyVueDirective::VueVBindShorthandDirective(sh) => {
72+
if let Ok(arg) = sh.arg()
73+
&& is_v_bind_key_argument(&arg)
74+
{
75+
has_key = true;
76+
}
77+
}
78+
_ => {}
79+
}
80+
if has_v_for.is_some() && has_key {
81+
// early exit if both found
82+
return None;
83+
}
84+
}
85+
}
86+
if let (Some(v_for_range), false) = (has_v_for, has_key) {
87+
return Some(v_for_range);
88+
}
89+
None
90+
}
91+
92+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
93+
Some(
94+
RuleDiagnostic::new(
95+
rule_category!(),
96+
state,
97+
markup! {
98+
"This element is using "<Emphasis>"v-for"</Emphasis>", but the "<Emphasis>"key"</Emphasis>" attribute is missing."
99+
},
100+
)
101+
.note(markup! {
102+
"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. "<Hyperlink href="https://vuejs.org/guide/essentials/list#maintaining-state-with-key">"See the Vue docs for more info"</Hyperlink>"."
103+
})
104+
.note(markup! {
105+
"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>`"
106+
}),
107+
)
108+
}
109+
}
110+
111+
fn is_v_bind_key_argument(arg: &VueDirectiveArgument) -> bool {
112+
let Ok(arg) = arg.arg() else {
113+
return false;
114+
};
115+
let Some(static_arg) = arg.as_vue_static_argument() else {
116+
return false;
117+
};
118+
static_arg
119+
.name_token()
120+
.is_ok_and(|name| name.text() == "key")
121+
}
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 attribute 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. See the Vue docs for more info.
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 attribute 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. See the Vue docs for more info.
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 attribute 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. See the Vue docs for more info.
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
@@ -404,6 +404,7 @@ pub mod use_vue_consistent_v_on_style;
404404
pub mod use_vue_define_macros_order;
405405
pub mod use_vue_hyphenated_attributes;
406406
pub mod use_vue_multi_word_component_names;
407+
pub mod use_vue_v_for_key;
407408
pub mod use_vue_valid_template_root;
408409
pub mod use_vue_valid_v_bind;
409410
pub mod use_vue_valid_v_cloak;

0 commit comments

Comments
 (0)