-
-
Notifications
You must be signed in to change notification settings - Fork 859
feat(analyze/html/vue): add a few more simple vue lint rules #8583
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,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> | ||
| ``` |
| 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" /> | ||
| ``` |
| 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" /> | ||
| ``` |
| 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" /> | ||
| ``` |
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,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, | ||
| } | ||
| } | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.rsRepository: 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 -30Repository: 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 -40Repository: 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 -20Repository: 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 -40Repository: 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 -40Repository: 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.rsRepository: 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 -50Repository: 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 -30Repository: 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.rsRepository: biomejs/biome Length of output: 126 This code has a compilation error— The method signature requires for child in el.children() {
mutation.remove_node(child);
}The fix kind is correctly specified as 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nursery rules should have
recommended: false.Line 43 sets
recommended: true, but per CONTRIBUTING.md guidelines, new nursery rules should berecommended: falseuntil 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