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
27 changes: 27 additions & 0 deletions .changeset/add-template-root-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@biomejs/biome": patch
---

Added the new nursery rule [`useVueValidTemplateRoot`](https://biomejs.dev/linter/rules/use-vue-valid-template-root/).

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.

Invalid examples:

```vue
<template src="./foo.html">content</template>
```

```vue
<template></template>
```

Valid examples:

```vue
<template>content</template>
```

```vue
<template src="./foo.html"></template>
```
15 changes: 15 additions & 0 deletions .changeset/add-v-once-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@biomejs/biome": patch
---

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.

```vue
<!-- Valid -->
<div v-once />

<!-- Invalid -->
<div v-once:aaa />
<div v-once.bbb />
<div v-once="ccc" />
```
15 changes: 15 additions & 0 deletions .changeset/v-cloak-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@biomejs/biome": patch
---

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.

```vue
<!-- Valid -->
<div v-cloak />

<!-- Invalid -->
<div v-cloak:aaa />
<div v-cloak.bbb />
<div v-cloak="ccc" />
```
15 changes: 15 additions & 0 deletions .changeset/v-pre-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@biomejs/biome": patch
---

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.

```vue
<!-- Valid -->
<div v-pre />

<!-- Invalid -->
<div v-pre:aaa />
<div v-pre.bbb />
<div v-pre="ccc" />
```
114 changes: 99 additions & 15 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

4 changes: 4 additions & 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.

6 changes: 5 additions & 1 deletion crates/biome_html_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ pub mod no_script_url;
pub mod no_sync_scripts;
pub mod no_vue_v_if_with_v_for;
pub mod use_vue_hyphenated_attributes;
pub mod use_vue_valid_template_root;
pub mod use_vue_valid_v_bind;
pub mod use_vue_valid_v_cloak;
pub mod use_vue_valid_v_else;
pub mod use_vue_valid_v_else_if;
pub mod use_vue_valid_v_html;
pub mod use_vue_valid_v_if;
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_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 ,] } }
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 ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use biome_analyze::{
Ast, FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext,
declare_lint_rule,
};
use biome_console::markup;
use biome_html_syntax::{HtmlElement, HtmlRoot};
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
use biome_rule_options::use_vue_valid_template_root::UseVueValidTemplateRootOptions;

declare_lint_rule! {
/// Enforce valid Vue `<template>` root usage.
///
/// This rule reports only root-level `<template>` elements. If the
/// `<template>` has a `src` attribute, the element must be empty. Otherwise,
/// the element must contain content.
///
/// ## Examples
///
/// ### Invalid
///
/// ```vue,expect_diagnostic
/// <template src="./foo.html">content</template>
/// ```
///
/// ```vue,expect_diagnostic
/// <template></template>
/// ```
///
/// ### Valid
///
/// ```vue
/// <template>content</template>
/// ```
///
/// ```vue
/// <template src="./foo.html"></template>
/// ```
///
pub UseVueValidTemplateRoot {
version: "next",
name: "useVueValidTemplateRoot",
language: "html",
recommended: true,
domains: &[RuleDomain::Vue],
sources: &[RuleSource::EslintVueJs("valid-template-root").same()],
fix_kind: FixKind::Unsafe,
}
Comment on lines +39 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nursery rules should have recommended: false.

Line 43 sets recommended: true, but per CONTRIBUTING.md guidelines, new nursery rules should be recommended: false until they graduate. Based on learnings from the repository.

🔎 Proposed fix
     pub UseVueValidTemplateRoot {
         version: "next",
         name: "useVueValidTemplateRoot",
         language: "html",
-        recommended: true,
+        recommended: false,
         domains: &[RuleDomain::Vue],
         sources: &[RuleSource::EslintVueJs("valid-template-root").same()],
         fix_kind: FixKind::Unsafe,
     }
🤖 Prompt for AI Agents
In crates/biome_html_analyze/src/lint/nursery/use_vue_valid_template_root.rs
around lines 39–47, the nursery rule is incorrectly marked recommended: true;
update the rule metadata to set recommended: false so the new nursery rule is
not enabled by default (leave all other fields unchanged).

}

pub enum ViolationKind {
MustBeEmpty(HtmlElement),
MustHaveContent(HtmlElement),
}

impl Rule for UseVueValidTemplateRoot {
type Query = Ast<HtmlRoot>;
type State = ViolationKind;
type Signals = Option<Self::State>;
type Options = UseVueValidTemplateRootOptions;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let root = ctx.query();
// Find top-level `<template>` elements only
let element = root
.html()
.into_iter()
.filter_map(|el| HtmlElement::cast(el.into_syntax()))
.find(|el| {
el.opening_element()
.ok()
.and_then(|op| op.name().ok())
.and_then(|name| name.value_token().ok())
.is_some_and(|tok| tok.text_trimmed() == "template")
})?;

let has_src = element.find_attribute_by_name("src").is_some();
let has_non_whitespace_content = !element.children().is_empty();

if has_src {
if has_non_whitespace_content {
return Some(ViolationKind::MustBeEmpty(element));
}
} else if !has_non_whitespace_content {
return Some(ViolationKind::MustHaveContent(element));
}

None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(match state {
ViolationKind::MustBeEmpty(el) => RuleDiagnostic::new(
rule_category!(),
el.range(),
markup! {
"The root `<template>` with a " <Emphasis>"src"</Emphasis> " attribute must be empty."
},
)
.note(markup! {
"The src attribute indicates that the content is loaded from an external file."
})
.note(markup! {
"Remove content when using the " <Emphasis>"src"</Emphasis> " attribute."
}),
ViolationKind::MustHaveContent(el) => RuleDiagnostic::new(
rule_category!(),
el.range(),
markup! {
"The root `<template>` is empty."
},
)
.note(markup! {
"The root `<template>` must contain content when no " <Emphasis>"src"</Emphasis> " attribute is present."
})
.note(markup! {
"Add content inside the `<template>` or use the " <Emphasis>"src"</Emphasis> " attribute."
}),
})
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> {
match state {
// Unsafe fix: remove the content when `src` is present
ViolationKind::MustBeEmpty(el) => {
let mut mutation = BatchMutationExt::begin(ctx.root());
mutation.remove_node(el.children());
Some(biome_analyze::RuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Remove inline content from `<template>`." }.to_owned(),
mutation,
))
}
ViolationKind::MustHaveContent(_el) => None,
}
}
Comment on lines +121 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for remove_node usage patterns with children()
rg -A 2 -B 2 'remove_node.*children' --type rust crates/biome_html_analyze/

Repository: biomejs/biome

Length of output: 733


🏁 Script executed:

# Check the declare_lint_rule! macro in this specific file to see if fix_kind is specified
head -50 crates/biome_html_analyze/src/lint/nursery/use_vue_valid_template_root.rs

Repository: biomejs/biome

Length of output: 1455


🏁 Script executed:

# Also search for remove_node patterns more broadly to understand common usage
rg 'remove_node\(' --type rust -A 1 crates/biome_html_analyze/ | head -30

Repository: biomejs/biome

Length of output: 2071


🏁 Script executed:

# Search for remove_node implementation/trait to see if it accepts collections
rg 'fn remove_node' --type rust -A 3 crates/biome_rowan/

Repository: biomejs/biome

Length of output: 301


🏁 Script executed:

# Check the BatchMutationExt trait specifically
rg 'impl.*BatchMutationExt' --type rust -A 20 crates/biome_rowan/

Repository: biomejs/biome

Length of output: 1467


🏁 Script executed:

# Look at the type returned by el.children() to understand what's being passed
rg 'fn children' --type rust -B 2 -A 2 crates/biome_html_syntax/ | head -40

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Check what type children() returns
rg 'type.*Children|fn children' --type rust -A 1 crates/biome_html_syntax/src/ | head -20

Repository: biomejs/biome

Length of output: 421


🏁 Script executed:

# Look for how other code handles removing multiple children
rg -B 3 -A 3 'for.*children\(\).*remove' --type rust crates/biome_html_analyze/ | head -40

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Verify the exact implementation to see if this is actually a bug
cat -n crates/biome_html_analyze/src/lint/nursery/use_vue_valid_template_root.rs | sed -n '120,136p'

Repository: biomejs/biome

Length of output: 943


🏁 Script executed:

# Check if there's any working iteration pattern in related files
rg 'for.*in.*\.children\(\)' --type rust crates/biome_html_analyze/ -A 3 | head -40

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Check if SyntaxNodeChildren implements AstNode or if there's a trait impl that converts it
rg 'impl.*AstNode.*SyntaxNodeChildren|impl.*From.*SyntaxNodeChildren' --type rust crates/biome_rowan/

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Search for any generic implementations of remove_node that might accept iterables
rg 'fn remove_node' --type rust -B 2 -A 10 crates/biome_rowan/src/ast/batch.rs

Repository: biomejs/biome

Length of output: 466


🏁 Script executed:

# Check if there are any other examples of removing multiple children in the codebase
rg 'for.*children\(\)' --type rust crates/biome_html_analyze/ -B 2 -A 5 | head -50

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Check the actual type signature - confirm SyntaxNodeChildren doesn't implement AstNode
rg 'pub struct SyntaxNodeChildren|impl.*SyntaxNodeChildren' --type rust crates/biome_rowan/ -A 2 | head -30

Repository: biomejs/biome

Length of output: 1152


🏁 Script executed:

# Try to compile just this file to see if there's a compilation error
cd crates/biome_html_analyze && cargo check 2>&1 | grep -A 5 "use_vue_valid_template_root"

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Check git history to see if this code was recently added or if there's a known issue
git log --oneline -20 crates/biome_html_analyze/src/lint/nursery/use_vue_valid_template_root.rs

Repository: biomejs/biome

Length of output: 126


This code has a compilation error—remove_node() expects a single node, not a collection.

The method signature requires T: AstNode, but el.children() returns an Iterator. You need to iterate over the children and remove each one individually:

for child in el.children() {
    mutation.remove_node(child);
}

The fix kind is correctly specified as FixKind::Unsafe in the rule declaration.

🤖 Prompt for AI Agents
In crates/biome_html_analyze/src/lint/nursery/use_vue_valid_template_root.rs
around lines 121 to 136, the call mutation.remove_node(el.children()) fails to
compile because remove_node expects a single AstNode, not an iterator; replace
that single call by iterating over el.children() and calling
mutation.remove_node(child) for each child (i.e., loop over the children and
remove them individually) so the BatchMutation removes each node correctly while
keeping the FixKind::Unsafe as declared.

}
Loading
Loading