Skip to content

Commit 794a504

Browse files
committed
feat(lint/vue): add noVueVIfWithVFor, useVueHyphenatedAttributes
1 parent 47d940e commit 794a504

38 files changed

Lines changed: 1544 additions & 214 deletions

.changeset/evil-experts-repeat.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noVueVIfWithVFor`](https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for/). This rule disallows `v-for` and `v-if` on the same element.
6+
7+
```vue
8+
<!-- Invalid -->
9+
<div v-for="item in items" v-if="item.isActive">
10+
{{ item.name }}
11+
</div>
12+
```

.changeset/great-mammals-hide.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useVueHyphenatedAttributes`](https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes/), which encourages using kebab case for attribute names, per the Vue style guide's recommendations.
6+
7+
```vue
8+
<!-- Invalid -->
9+
<MyComponent myProp="value" />
10+
11+
<!-- Valid -->
12+
<MyComponent my-prop="value" />
13+
```

.changeset/loose-chairs-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed an issue with the HTML parser where it would treat Vue directives with dynamic arguments as static arguments instead.

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

Lines changed: 86 additions & 44 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ define_categories! {
194194
"lint/nursery/noVueDuplicateKeys": "https://biomejs.dev/linter/rules/no-vue-duplicate-keys",
195195
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
196196
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
197+
"lint/nursery/noVueVIfWithVFor": "https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for",
197198
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
198199
"lint/nursery/useArraySortCompare": "https://biomejs.dev/linter/rules/use-array-sort-compare",
199200
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
@@ -214,6 +215,7 @@ define_categories! {
214215
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
215216
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",
216217
"lint/nursery/useVueDefineMacrosOrder": "https://biomejs.dev/linter/rules/use-vue-define-macros-order",
218+
"lint/nursery/useVueHyphenatedAttributes": "https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes",
217219
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
218220
"lint/nursery/useVueValidVBind": "https://biomejs.dev/linter/rules/use-vue-valid-v-bind",
219221
"lint/nursery/useVueValidVElse": "https://biomejs.dev/linter/rules/use-vue-valid-v-else",

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
55
use biome_analyze::declare_lint_group;
66
pub mod no_sync_scripts;
7+
pub mod no_vue_v_if_with_v_for;
8+
pub mod use_vue_hyphenated_attributes;
79
pub mod use_vue_valid_v_bind;
810
pub mod use_vue_valid_v_else;
911
pub mod use_vue_valid_v_else_if;
1012
pub mod use_vue_valid_v_html;
1113
pub mod use_vue_valid_v_if;
1214
pub mod use_vue_valid_v_on;
13-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_sync_scripts :: NoSyncScripts , self :: use_vue_valid_v_bind :: UseVueValidVBind , 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 ,] } }
15+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_v_bind :: UseVueValidVBind , 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 ,] } }
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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::HtmlAttributeList;
6+
use biome_rowan::{AstNode, AstNodeList, TextRange};
7+
use biome_rule_options::no_vue_v_if_with_v_for::NoVueVIfWithVForOptions;
8+
9+
declare_lint_rule! {
10+
/// Disallow using `v-if` and `v-for` directives on the same element.
11+
///
12+
/// There are two common cases where this can be tempting:
13+
/// - To filter items in a list (e.g. v-for="user in users" v-if="user.isActive"). In these cases, replace users with a new computed property that returns your filtered list (e.g. activeUsers).
14+
/// - To avoid rendering a list if it should be hidden (e.g. v-for="user in users" v-if="shouldShowUsers"). In these cases, move the v-if to a container element (e.g. ul, ol).
15+
///
16+
/// ## Examples
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```vue,expect_diagnostic
21+
/// <TodoItem
22+
/// v-if="complete"
23+
/// v-for="todo in todos"
24+
/// :todo="todo"
25+
/// />
26+
/// ```
27+
///
28+
/// ### Valid
29+
///
30+
/// ```vue
31+
/// <ul v-if="complete">
32+
/// <TodoItem
33+
/// v-for="todo in todos"
34+
/// :todo="todo"
35+
/// />
36+
/// </ul>
37+
/// ```
38+
///
39+
pub NoVueVIfWithVFor {
40+
version: "next",
41+
name: "noVueVIfWithVFor",
42+
language: "html",
43+
recommended: true,
44+
domains: &[RuleDomain::Vue],
45+
sources: &[RuleSource::EslintVueJs("no-use-v-if-with-v-for").same()],
46+
}
47+
}
48+
49+
pub struct State {
50+
v_for_range: TextRange,
51+
v_if_range: TextRange,
52+
}
53+
54+
impl Rule for NoVueVIfWithVFor {
55+
type Query = Ast<HtmlAttributeList>;
56+
type State = State;
57+
type Signals = Option<Self::State>;
58+
type Options = NoVueVIfWithVForOptions;
59+
60+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
61+
let node = ctx.query();
62+
63+
let mut v_for = None;
64+
let mut v_if = None;
65+
for attr in node.iter() {
66+
if let Some(directive) = attr
67+
.as_any_vue_directive()
68+
.and_then(|dir| dir.as_vue_directive())
69+
{
70+
if directive
71+
.name_token()
72+
.is_ok_and(|t| t.text_trimmed() == "v-if")
73+
{
74+
v_if = Some(directive.range());
75+
} else if directive
76+
.name_token()
77+
.is_ok_and(|t| t.text_trimmed() == "v-for")
78+
{
79+
v_for = Some(directive.range());
80+
}
81+
}
82+
}
83+
84+
if let (Some(v_if_range), Some(v_for_range)) = (v_if, v_for) {
85+
return Some(State {
86+
v_for_range,
87+
v_if_range,
88+
});
89+
}
90+
None
91+
}
92+
93+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
94+
Some(
95+
RuleDiagnostic::new(
96+
rule_category!(),
97+
state.v_for_range,
98+
markup! {
99+
"Using "<Emphasis>"v-if"</Emphasis>" and "<Emphasis>"v-for"</Emphasis>" on the same element is discouraged."
100+
},
101+
)
102+
.note(markup! {
103+
"Using "<Emphasis>"v-if"</Emphasis>" and "<Emphasis>"v-for"</Emphasis>" on the same element can lead to unexpected behavior and performance issues."
104+
})
105+
.detail(state.v_if_range, markup! {
106+
"This "<Emphasis>"v-if"</Emphasis>" should be moved to the wrapper element, or you should use a computed property to filter the list instead."
107+
}),
108+
)
109+
}
110+
}

0 commit comments

Comments
 (0)