Skip to content

Commit 0170dcb

Browse files
authored
feat(html/analyze): add useVueValidVIf, useVueValidVElseIf, useVueValidVElse, useVueValidVOn and useVueValidVHtml (#8077)
1 parent f102661 commit 0170dcb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2965
-2
lines changed
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+
Added the rule [`useVueValidVElseIf`](https://biomejs.dev/linter/rules/use-vue-valid-v-else-if/) to enforce valid `v-else-if` directives in Vue templates. This rule reports invalid `v-else-if` directives with missing conditional expressions or when not preceded by a `v-if` or `v-else-if` directive.

.changeset/use-vue-valid-v-else.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+
Added the rule [`useVueValidVElse`](https://biomejs.dev/linter/rules/use-vue-valid-v-else/) to enforce valid `v-else` directives in Vue templates. This rule reports `v-else` directives that are not preceded by a `v-if` or `v-else-if` directive.

.changeset/use-vue-valid-v-html.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+
Added the rule [`useVueValidVHtml`](https://biomejs.dev/linter/rules/use-vue-valid-v-html/) to enforce valid usage of the `v-html` directive in Vue templates. This rule reports `v-html` directives with missing expressions, unexpected arguments, or unexpected modifiers.

.changeset/use-vue-valid-v-if.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+
Added the rule [`useVueValidVIf`](https://biomejs.dev/linter/rules/use-vue-valid-v-if/) to enforce valid `v-if` directives in Vue templates. It disallows arguments and modifiers, and ensures a value is provided.

.changeset/use-vue-valid-v-on.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+
Added the rule [`useVueValidVOn`](https://biomejs.dev/linter/rules/use-vue-valid-v-on/) to enforce valid `v-on` directives in Vue templates. This rule reports invalid `v-on` / shorthand `@` directives with missing event names, invalid modifiers, or missing handler expressions.

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

Lines changed: 106 additions & 1 deletion
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ define_categories! {
191191
"lint/nursery/noVueDataObjectDeclaration": "https://biomejs.dev/linter/rules/no-vue-data-object-declaration",
192192
"lint/nursery/noVueDuplicateKeys": "https://biomejs.dev/linter/rules/no-vue-duplicate-keys",
193193
"lint/nursery/useVueValidVBind": "https://biomejs.dev/linter/rules/use-vue-valid-v-bind",
194+
"lint/nursery/useVueValidVIf": "https://biomejs.dev/linter/rules/use-vue-valid-v-if",
195+
"lint/nursery/useVueValidVElse": "https://biomejs.dev/linter/rules/use-vue-valid-v-else",
196+
"lint/nursery/useVueValidVElseIf": "https://biomejs.dev/linter/rules/use-vue-valid-v-else-if",
197+
"lint/nursery/useVueValidVFor": "https://biomejs.dev/linter/rules/use-vue-valid-v-for",
198+
"lint/nursery/useVueValidVHtml": "https://biomejs.dev/linter/rules/use-vue-valid-v-html",
199+
"lint/nursery/useVueValidVModel": "https://biomejs.dev/linter/rules/use-vue-valid-v-model",
200+
"lint/nursery/useVueValidVOn": "https://biomejs.dev/linter/rules/use-vue-valid-v-on",
194201
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
195202
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
196203
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44
55
use biome_analyze::declare_lint_group;
66
pub mod use_vue_valid_v_bind;
7-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: use_vue_valid_v_bind :: UseVueValidVBind ,] } }
7+
pub mod use_vue_valid_v_else;
8+
pub mod use_vue_valid_v_else_if;
9+
pub mod use_vue_valid_v_html;
10+
pub mod use_vue_valid_v_if;
11+
pub mod use_vue_valid_v_on;
12+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [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: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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, HtmlElement, HtmlSelfClosingElement, VueDirective};
6+
use biome_rowan::{AstNode, TextRange, declare_node_union};
7+
use biome_rule_options::use_vue_valid_v_else::UseVueValidVElseOptions;
8+
9+
declare_lint_rule! {
10+
/// Enforce valid usage of v-else.
11+
///
12+
/// This rule reports v-else directives in the following cases:
13+
/// - The directive has an argument. E.g. `<div v-if="foo"></div><div v-else:aaa></div>`
14+
/// - The directive has a modifier. E.g. `<div v-if="foo"></div><div v-else.bbb></div>`
15+
/// - The directive has an attribute value. E.g. `<div v-if="foo"></div><div v-else="bar"></div>`
16+
/// - The directive is on elements where the previous element doesn't have `v-if`/`v-else-if` directives. E.g. `<div v-else></div>`
17+
/// - The directive is on elements which have `v-if`/`v-else-if` directives. E.g. `<div v-if="foo" v-else></div>`
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```vue,expect_diagnostic
24+
/// <div v-else:arg></div>
25+
/// ```
26+
///
27+
/// ```vue,expect_diagnostic
28+
/// <div v-else.mod></div>
29+
/// ```
30+
///
31+
/// ```vue,expect_diagnostic
32+
/// <div v-else="value"></div>
33+
/// ```
34+
///
35+
/// ```vue,expect_diagnostic
36+
/// <div v-else></div>
37+
/// ```
38+
///
39+
/// ```vue,expect_diagnostic
40+
/// <div v-if="foo" v-else></div>
41+
/// ```
42+
///
43+
/// ### Valid
44+
///
45+
/// ```vue
46+
/// <div v-if="foo"></div>
47+
/// <div v-else></div>
48+
/// ```
49+
///
50+
/// ```vue
51+
/// <div v-if="foo"></div>
52+
/// <div v-else-if="bar"></div>
53+
/// <div v-else></div>
54+
/// ```
55+
///
56+
pub UseVueValidVElse {
57+
version: "next",
58+
name: "useVueValidVElse",
59+
language: "html",
60+
recommended: true,
61+
domains: &[RuleDomain::Vue],
62+
sources: &[RuleSource::EslintVueJs("valid-v-else").same()],
63+
}
64+
}
65+
66+
pub enum ViolationKind {
67+
HasArgument(TextRange),
68+
HasModifier(TextRange),
69+
HasValue(TextRange),
70+
MissingPreviousIfOrElseIf,
71+
CombinedWithIfOrElseIf(TextRange),
72+
}
73+
74+
declare_node_union! {
75+
pub AnyHtmlElement = HtmlElement | HtmlSelfClosingElement
76+
}
77+
78+
impl Rule for UseVueValidVElse {
79+
type Query = Ast<VueDirective>;
80+
type State = ViolationKind;
81+
type Signals = Option<Self::State>;
82+
type Options = UseVueValidVElseOptions;
83+
84+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
85+
let vue_directive = ctx.query();
86+
if vue_directive.name_token().ok()?.text_trimmed() != "v-else" {
87+
return None;
88+
}
89+
90+
// Check for argument
91+
if let Some(arg) = vue_directive.arg() {
92+
return Some(ViolationKind::HasArgument(arg.range()));
93+
}
94+
95+
// Check for modifiers
96+
let modifiers = vue_directive.modifiers();
97+
if let Some(modifier) = modifiers.into_iter().next() {
98+
return Some(ViolationKind::HasModifier(modifier.range()));
99+
}
100+
101+
// Check for value
102+
if let Some(initializer) = vue_directive.initializer() {
103+
return Some(ViolationKind::HasValue(initializer.range()));
104+
}
105+
106+
// Get parent element
107+
let parent_element = vue_directive
108+
.syntax()
109+
.ancestors()
110+
.skip(1)
111+
.find_map(|ancestor| AnyHtmlElement::cast_ref(&ancestor))?;
112+
113+
// Check if current element also has v-if or v-else-if
114+
if has_v_if_or_else_if_directives(&parent_element) {
115+
return Some(ViolationKind::CombinedWithIfOrElseIf(
116+
vue_directive.name_token().ok()?.text_range(),
117+
));
118+
}
119+
120+
// Check if previous sibling has v-if or v-else-if
121+
if !has_previous_sibling_with_v_if_or_else_if(&parent_element) {
122+
return Some(ViolationKind::MissingPreviousIfOrElseIf);
123+
}
124+
125+
None
126+
}
127+
128+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
129+
Some(
130+
match state {
131+
ViolationKind::HasArgument(range) => RuleDiagnostic::new(
132+
rule_category!(),
133+
range,
134+
markup! {
135+
"v-else must not have an argument."
136+
},
137+
)
138+
.note(markup! {
139+
"Remove the argument; v-else is a stand-alone control directive."
140+
}),
141+
ViolationKind::HasModifier(range) => RuleDiagnostic::new(
142+
rule_category!(),
143+
range,
144+
markup! {
145+
"v-else must not have modifiers."
146+
},
147+
)
148+
.note(markup! {
149+
"Remove the modifier; v-else is a stand-alone control directive."
150+
}),
151+
ViolationKind::HasValue(range) => RuleDiagnostic::new(
152+
rule_category!(),
153+
range,
154+
markup! {
155+
"v-else must not have a value."
156+
},
157+
)
158+
.note(markup! {
159+
"Remove the value; v-else is a stand-alone control directive."
160+
}),
161+
ViolationKind::MissingPreviousIfOrElseIf => RuleDiagnostic::new(
162+
rule_category!(),
163+
ctx.query().range(),
164+
markup! {
165+
"v-else requires a previous sibling element with v-if or v-else-if."
166+
},
167+
)
168+
.note(markup! {
169+
"Place v-else immediately after an element with v-if or v-else-if, within the same parent."
170+
}),
171+
ViolationKind::CombinedWithIfOrElseIf(range) => RuleDiagnostic::new(
172+
rule_category!(),
173+
range,
174+
markup! {
175+
"v-else cannot be used on the same element as v-if or v-else-if."
176+
},
177+
)
178+
.note(markup! {
179+
"Move v-else onto a separate element immediately following the v-if/v-else-if element."
180+
}),
181+
}
182+
)
183+
}
184+
}
185+
186+
fn has_v_if_or_else_if_directives(element: &AnyHtmlElement) -> bool {
187+
// Check attributes for v-if or v-else-if directives
188+
189+
let attribute_list = match element {
190+
AnyHtmlElement::HtmlElement(html_element) => {
191+
let Ok(opening_element) = html_element.opening_element() else {
192+
return false;
193+
};
194+
opening_element.attributes()
195+
}
196+
AnyHtmlElement::HtmlSelfClosingElement(self_closing) => self_closing.attributes(),
197+
};
198+
199+
for attribute in attribute_list {
200+
if let Ok(AnyVueDirective::VueDirective(vue_dir)) =
201+
AnyVueDirective::try_cast(attribute.syntax().clone())
202+
&& let Ok(name_token) = vue_dir.name_token()
203+
{
204+
let name = name_token.text();
205+
if name == "v-if" || name == "v-else-if" {
206+
return true;
207+
}
208+
}
209+
}
210+
211+
false
212+
}
213+
214+
fn has_previous_sibling_with_v_if_or_else_if(element: &AnyHtmlElement) -> bool {
215+
if let Some(sibling) = element
216+
.syntax()
217+
.prev_sibling()
218+
.and_then(|s| AnyHtmlElement::cast_ref(&s))
219+
{
220+
return has_v_if_or_else_if_directives(&sibling);
221+
}
222+
223+
false
224+
}

0 commit comments

Comments
 (0)