Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .changeset/add-v-for-key-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@biomejs/biome": patch
---

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`.

**Invalid**

```vue
<li v-for="item in items">{{ item }}</li>
```

**Valid**

```vue
<li v-for="item in items" :key="item.id">{{ item }}</li>
```
67 changes: 44 additions & 23 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/biome_html_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod no_vue_v_if_with_v_for;
pub mod use_vue_consistent_v_bind_style;
pub mod use_vue_consistent_v_on_style;
pub mod use_vue_hyphenated_attributes;
pub mod use_vue_v_for_key;
pub mod use_vue_valid_template_root;
pub mod use_vue_valid_v_bind;
pub mod use_vue_valid_v_cloak;
Expand All @@ -21,4 +22,4 @@ pub mod use_vue_valid_v_on;
pub mod use_vue_valid_v_once;
pub mod use_vue_valid_v_pre;
pub mod use_vue_valid_v_text;
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 ,] } }
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 ,] } }
121 changes: 121 additions & 0 deletions crates/biome_html_analyze/src/lint/nursery/use_vue_v_for_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_html_syntax::{AnyVueDirective, HtmlAttributeList, VueDirectiveArgument};
use biome_rowan::{AstNode, AstNodeList, TextRange};
use biome_rule_options::use_vue_v_for_key::UseVueVForKeyOptions;

declare_lint_rule! {
/// Enforce that elements using `v-for` also specify a unique `key`.
///
/// When rendering lists with `v-for`, Vue relies on a `key` to track elements efficiently.
/// The `key` can be provided via longhand `v-bind:key` or shorthand `:key`. If you need to
/// animate the entrance/exit of an item in a list, the key should be a unique identifier for
/// each item in the list, and not the index of the item.
///
/// For more information, see the Vue documentation on [list rendering](https://vuejs.org/guide/essentials/list#maintaining-state-with-key).
///
/// ## Examples
///
/// ### Invalid
///
/// ```vue,expect_diagnostic
/// <li v-for="item in items">{{ item }}</li>
/// ```
///
/// ### Valid
///
/// ```vue
/// <li v-for="item in items" :key="item.id">{{ item }}</li>
/// ```
///
/// ```vue
/// <li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
/// ```
///
pub UseVueVForKey {
version: "next",
name: "useVueVForKey",
language: "html",
recommended: true,
domains: &[RuleDomain::Vue],
sources: &[RuleSource::EslintVueJs("require-v-for-key").same()],
}
}

impl Rule for UseVueVForKey {
type Query = Ast<HtmlAttributeList>;
type State = TextRange;
type Signals = Option<Self::State>;
type Options = UseVueVForKeyOptions;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let attrs = ctx.query();
let mut has_v_for = None;
let mut has_key = false;
for attr in attrs.iter() {
if let Some(dir_any) = attr.as_any_vue_directive() {
match dir_any {
AnyVueDirective::VueDirective(dir) => {
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-for") {
has_v_for = Some(dir.range());
}
if dir.name_token().is_ok_and(|t| t.text_trimmed() == "v-bind")
&& let Some(arg) = dir.arg()
&& is_v_bind_key_argument(&arg)
{
has_key = true;
}
}
AnyVueDirective::VueVBindShorthandDirective(sh) => {
if let Ok(arg) = sh.arg()
&& is_v_bind_key_argument(&arg)
{
has_key = true;
}
}
_ => {}
}
if has_v_for.is_some() && has_key {
// early exit if both found
return None;
}
}
}
if let (Some(v_for_range), false) = (has_v_for, has_key) {
return Some(v_for_range);
}
None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
state,
markup! {
"This element is using "<Emphasis>"v-for"</Emphasis>", but the "<Emphasis>"key"</Emphasis>" attribute is missing."
},
)
.note(markup! {
"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>"."
})
.note(markup! {
"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>`"
}),
)
}
}

fn is_v_bind_key_argument(arg: &VueDirectiveArgument) -> bool {
let Ok(arg) = arg.arg() else {
return false;
};
let Some(static_arg) = arg.as_vue_static_argument() else {
return false;
};
static_arg
.name_token()
.is_ok_and(|name| name.text() == "key")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- should generate diagnostics -->
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>

<div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>

<template v-for="item in items">
<li>{{ item }}</li>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.vue
---
# Input
```html
<!-- should generate diagnostics -->
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>

<div class="wrapper" v-for="(item, i) in items" data-id="foo"></div>

<template v-for="item in items">
<li>{{ item }}</li>
</template>

```

# Diagnostics
```
invalid.vue:3:7 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This element is using v-for, but the key attribute is missing.

1 │ <!-- should generate diagnostics -->
2 │ <ul>
> 3 │ <li v-for="item in items">{{ item }}</li>
│ ^^^^^^^^^^^^^^^^^^^^^
4 │ </ul>
5 │

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.

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>`

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.


```

```
invalid.vue:6:22 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This element is using v-for, but the key attribute is missing.

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">

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.

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>`

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.


```

```
invalid.vue:8:11 lint/nursery/useVueVForKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This element is using v-for, but the key attribute is missing.

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>

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.

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>`

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.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- should not generate diagnostics -->
<ul>
<li v-for="item in items" :key="item.id">{{ item }}</li>
</ul>

<ul>
<li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
</ul>

<div v-for="(item, i) in items" :key="i"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: valid.vue
---
# Input
```html
<!-- should not generate diagnostics -->
<ul>
<li v-for="item in items" :key="item.id">{{ item }}</li>
</ul>

<ul>
<li v-for="item in items" v-bind:key="item.id">{{ item }}</li>
</ul>

<div v-for="(item, i) in items" :key="i"></div>

```
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ pub mod use_vue_consistent_v_on_style;
pub mod use_vue_define_macros_order;
pub mod use_vue_hyphenated_attributes;
pub mod use_vue_multi_word_component_names;
pub mod use_vue_v_for_key;
pub mod use_vue_valid_template_root;
pub mod use_vue_valid_v_bind;
pub mod use_vue_valid_v_cloak;
Expand Down
6 changes: 6 additions & 0 deletions crates/biome_rule_options/src/use_vue_v_for_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseVueVForKeyOptions {}
14 changes: 14 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.