-
-
Notifications
You must be signed in to change notification settings - Fork 880
feat(analyze/html/vue): add useVueVForKey
#8587
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
Changes from all commits
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 |
|---|---|---|
| @@ -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> | ||
| ``` |
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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> | ||
|
|
||
| ``` |
| 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 {} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.