Skip to content

Commit 83be210

Browse files
authored
feat(analyze/html/vue): add a few more simple vue lint rules (#8583)
<!-- 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 a few more Vue HTML lint rules. The reason I put them all in one PR is because they are all pretty trivial, logic wise. Almost completely AI generated, except for some manual tweaking of the diagnostics. <!-- 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 1df2121 commit 83be210

File tree

34 files changed

+1368
-16
lines changed

34 files changed

+1368
-16
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`useVueValidTemplateRoot`](https://biomejs.dev/linter/rules/use-vue-valid-template-root/).
6+
7+
This rule validates only root-level `<template>` elements in Vue single-file components. If the `<template>` has a `src` attribute, it must be empty. Otherwise, it must contain content.
8+
9+
Invalid examples:
10+
11+
```vue
12+
<template src="./foo.html">content</template>
13+
```
14+
15+
```vue
16+
<template></template>
17+
```
18+
19+
Valid examples:
20+
21+
```vue
22+
<template>content</template>
23+
```
24+
25+
```vue
26+
<template src="./foo.html"></template>
27+
```

‎.changeset/add-v-once-rule.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`useVueValidVOnce`](https://biomejs.dev/linter/rules/use-vue-valid-v-once/). Enforces that usages of the `v-once` directive in Vue.js SFC are valid.
6+
7+
```vue
8+
<!-- Valid -->
9+
<div v-once />
10+
11+
<!-- Invalid -->
12+
<div v-once:aaa />
13+
<div v-once.bbb />
14+
<div v-once="ccc" />
15+
```

‎.changeset/v-cloak-rule.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`useVueValidVCloak`](https://biomejs.dev/linter/rules/use-vue-valid-v-cloak/). Enforces that usages of the `v-cloak` directive in Vue.js SFC are valid.
6+
7+
```vue
8+
<!-- Valid -->
9+
<div v-cloak />
10+
11+
<!-- Invalid -->
12+
<div v-cloak:aaa />
13+
<div v-cloak.bbb />
14+
<div v-cloak="ccc" />
15+
```

‎.changeset/v-pre-rule.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`useVueValidVPre`](https://biomejs.dev/linter/rules/use-vue-valid-v-pre/). Enforces that usages of the `v-pre` directive in Vue.js SFC are valid.
6+
7+
```vue
8+
<!-- Valid -->
9+
<div v-pre />
10+
11+
<!-- Invalid -->
12+
<div v-pre:aaa />
13+
<div v-pre.bbb />
14+
<div v-pre="ccc" />
15+
```

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

Lines changed: 99 additions & 15 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: 4 additions & 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ pub mod no_script_url;
88
pub mod no_sync_scripts;
99
pub mod no_vue_v_if_with_v_for;
1010
pub mod use_vue_hyphenated_attributes;
11+
pub mod use_vue_valid_template_root;
1112
pub mod use_vue_valid_v_bind;
13+
pub mod use_vue_valid_v_cloak;
1214
pub mod use_vue_valid_v_else;
1315
pub mod use_vue_valid_v_else_if;
1416
pub mod use_vue_valid_v_html;
1517
pub mod use_vue_valid_v_if;
1618
pub mod use_vue_valid_v_on;
19+
pub mod use_vue_valid_v_once;
20+
pub mod use_vue_valid_v_pre;
1721
pub mod use_vue_valid_v_text;
18-
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_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 , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
22+
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_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 ,] } }
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use biome_analyze::{
2+
Ast, FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext,
3+
declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_html_syntax::{HtmlElement, HtmlRoot};
7+
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
8+
use biome_rule_options::use_vue_valid_template_root::UseVueValidTemplateRootOptions;
9+
10+
declare_lint_rule! {
11+
/// Enforce valid Vue `<template>` root usage.
12+
///
13+
/// This rule reports only root-level `<template>` elements. If the
14+
/// `<template>` has a `src` attribute, the element must be empty. Otherwise,
15+
/// the element must contain content.
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```vue,expect_diagnostic
22+
/// <template src="./foo.html">content</template>
23+
/// ```
24+
///
25+
/// ```vue,expect_diagnostic
26+
/// <template></template>
27+
/// ```
28+
///
29+
/// ### Valid
30+
///
31+
/// ```vue
32+
/// <template>content</template>
33+
/// ```
34+
///
35+
/// ```vue
36+
/// <template src="./foo.html"></template>
37+
/// ```
38+
///
39+
pub UseVueValidTemplateRoot {
40+
version: "next",
41+
name: "useVueValidTemplateRoot",
42+
language: "html",
43+
recommended: true,
44+
domains: &[RuleDomain::Vue],
45+
sources: &[RuleSource::EslintVueJs("valid-template-root").same()],
46+
fix_kind: FixKind::Unsafe,
47+
}
48+
}
49+
50+
pub enum ViolationKind {
51+
MustBeEmpty(HtmlElement),
52+
MustHaveContent(HtmlElement),
53+
}
54+
55+
impl Rule for UseVueValidTemplateRoot {
56+
type Query = Ast<HtmlRoot>;
57+
type State = ViolationKind;
58+
type Signals = Option<Self::State>;
59+
type Options = UseVueValidTemplateRootOptions;
60+
61+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
62+
let root = ctx.query();
63+
// Find top-level `<template>` elements only
64+
let element = root
65+
.html()
66+
.into_iter()
67+
.filter_map(|el| HtmlElement::cast(el.into_syntax()))
68+
.find(|el| {
69+
el.opening_element()
70+
.ok()
71+
.and_then(|op| op.name().ok())
72+
.and_then(|name| name.value_token().ok())
73+
.is_some_and(|tok| tok.text_trimmed() == "template")
74+
})?;
75+
76+
let has_src = element.find_attribute_by_name("src").is_some();
77+
let has_non_whitespace_content = !element.children().is_empty();
78+
79+
if has_src {
80+
if has_non_whitespace_content {
81+
return Some(ViolationKind::MustBeEmpty(element));
82+
}
83+
} else if !has_non_whitespace_content {
84+
return Some(ViolationKind::MustHaveContent(element));
85+
}
86+
87+
None
88+
}
89+
90+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
91+
Some(match state {
92+
ViolationKind::MustBeEmpty(el) => RuleDiagnostic::new(
93+
rule_category!(),
94+
el.range(),
95+
markup! {
96+
"The root `<template>` with a " <Emphasis>"src"</Emphasis> " attribute must be empty."
97+
},
98+
)
99+
.note(markup! {
100+
"The src attribute indicates that the content is loaded from an external file."
101+
})
102+
.note(markup! {
103+
"Remove content when using the " <Emphasis>"src"</Emphasis> " attribute."
104+
}),
105+
ViolationKind::MustHaveContent(el) => RuleDiagnostic::new(
106+
rule_category!(),
107+
el.range(),
108+
markup! {
109+
"The root `<template>` is empty."
110+
},
111+
)
112+
.note(markup! {
113+
"The root `<template>` must contain content when no " <Emphasis>"src"</Emphasis> " attribute is present."
114+
})
115+
.note(markup! {
116+
"Add content inside the `<template>` or use the " <Emphasis>"src"</Emphasis> " attribute."
117+
}),
118+
})
119+
}
120+
121+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
122+
match state {
123+
// Unsafe fix: remove the content when `src` is present
124+
ViolationKind::MustBeEmpty(el) => {
125+
let mut mutation = BatchMutationExt::begin(ctx.root());
126+
mutation.remove_node(el.children());
127+
Some(biome_analyze::RuleAction::new(
128+
ctx.metadata().action_category(ctx.category(), ctx.group()),
129+
ctx.metadata().applicability(),
130+
markup! { "Remove inline content from `<template>`." }.to_owned(),
131+
mutation,
132+
))
133+
}
134+
ViolationKind::MustHaveContent(_el) => None,
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)