Skip to content

feat(lint/html): add useVueScopedStyles#9185

Open
dyc3 wants to merge 2 commits intomainfrom
dyc3/use-vue-scoped-styles
Open

feat(lint/html): add useVueScopedStyles#9185
dyc3 wants to merge 2 commits intomainfrom
dyc3/use-vue-scoped-styles

Conversation

@dyc3
Copy link
Contributor

@dyc3 dyc3 commented Feb 21, 2026

Summary

This adds a new html rule that enforces that scoped be on all <style> blocks.

Mostly generated with glm-5

Test Plan

snapshots

Docs

@changeset-bot
Copy link

changeset-bot bot commented Feb 21, 2026

🦋 Changeset detected

Latest commit: 97d1e15

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages labels Feb 21, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 21, 2026

Merging this PR will not alter performance

✅ 64 untouched benchmarks
⏩ 152 skipped benchmarks1


Comparing dyc3/use-vue-scoped-styles (97d1e15) with main (b834078)

Open in CodSpeed

Footnotes

  1. 152 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3af7edc and a43adc2.

⛔ Files ignored due to path filters (8)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (7)
  • .changeset/add-use-vue-scoped-styles.md
  • crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/use_scoped_styles.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/biome_rule_options/src/use_scoped_styles.rs
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue
  • crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro

Walkthrough

Adds a new HTML lint rule UseScopedStyles that detects unscoped <style> blocks in Vue SFCs and is:global directives in Astro components, emits diagnostics, and implements automatic fixes (add scoped for Vue, remove is:global for Astro). Introduces GlobalStylesKind and registers the rule via declare_lint_rule!. Adds an empty UseScopedStylesOptions struct, exports a new use_scoped_styles module, includes test fixtures (valid/invalid) for Vue and Astro, and adds a changeset entry.

Possibly related PRs

Suggested reviewers

  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding a new HTML lint rule for Vue scoped styles.
Description check ✅ Passed The description explains the motivation (enforcing scoped attribute on style blocks) and references AI assistance, though it's minimal on implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dyc3/use-vue-scoped-styles

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/add-use-vue-scoped-styles.md:
- Around line 1-5: Update the changeset to reflect this is a new lint rule:
change the release type from "patch" to "minor" and add a note in the body that
the new rule useVueScopedStyles defaults to Severity::Information (i.e.,
RuleMetadata::default()) when no explicit severity is set in declare_lint_rule!,
so callers should be aware of the default severity level; keep the concise
description about enforcing scoped/module on <style> blocks and mention the
default severity explicitly.

In `@crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs`:
- Around line 115-119: Update the user-facing autofix message passed into
RuleAction::new (the markup! invocation created via ctx.metadata() in
use_vue_scoped_styles) to fix the grammar: change "Add the \"scoped\" attribute
so the styles in will only apply to this component." to a clean sentence such as
"Add the \"scoped\" attribute so the styles will only apply to this component."
Ensure the corrected string is used in the same markup! call that constructs the
RuleAction description.
- Around line 103-110: The token currently embeds a leading space by calling
HtmlSyntaxToken::new_detached(HtmlSyntaxKind::HTML_LITERAL, " scoped", ...,
...); instead remove the embedded whitespace (use "scoped" or empty literal as
appropriate) and attach the space as leading trivia using
TriviaPiece::whitespace() so spacing is handled consistently (see the pattern in
use_vue_vapor.rs); update the HtmlSyntaxToken creation for the token used by
make::html_attribute_name and keep the surrounding flow that wraps it in
AnyHtmlAttribute::HtmlAttribute and the attribute list unchanged.

In `@crates/biome_rule_options/src/use_vue_scoped_styles.rs`:
- Around line 1-6: Add an inline rustdoc comment above the
UseVueScopedStylesOptions struct: insert a one-line /// comment that briefly
describes the purpose of this options struct (e.g., "Options for the
use_vue_scoped_styles lint/assist" or similar) so it appears in generated docs;
place it immediately above the pub struct UseVueScopedStylesOptions {}
declaration and update the wording if fields are added later.

Comment on lines 1 to 5
---
"@biomejs/biome": patch
---

Added the `useVueScopedStyles` lint rule for Vue SFCs. This rule enforces that `<style>` blocks have the `scoped` attribute (or `module` for CSS Modules), preventing style leakage and conflicts between components.
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 | 🟡 Minor

Changeset should be minor and mention the default severity.

This is a new lint rule, so the change type should be minor. Also please note that the rule defaults to Severity::Information because no explicit severity is set.

♻️ Proposed update
---
-"@biomejs/biome": patch
+"@biomejs/biome": minor
---

 Added the `useVueScopedStyles` lint rule for Vue SFCs. This rule enforces that `<style>` blocks have the `scoped` attribute (or `module` for CSS Modules), preventing style leakage and conflicts between components.
+Note: This rule defaults to `Severity::Information` unless configured.

As per coding guidelines: Use change types in changesets: patch for bug fixes and non-breaking changes (targets main branch), minor for new features and non-breaking additions (targets next branch), major for breaking changes (targets next branch).
Based on learnings: In changeset markdown files (e.g., .changeset/*), document that when a lint rule does not explicitly set the severity in declare_lint_rule!, the default severity is Severity::Information (RuleMetadata::default()), not Severity::Error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/add-use-vue-scoped-styles.md around lines 1 - 5, Update the
changeset to reflect this is a new lint rule: change the release type from
"patch" to "minor" and add a note in the body that the new rule
useVueScopedStyles defaults to Severity::Information (i.e.,
RuleMetadata::default()) when no explicit severity is set in declare_lint_rule!,
so callers should be aware of the default severity level; keep the concise
description about enforcing scoped/module on <style> blocks and mention the
default severity explicitly.

Comment on lines 103 to 110
let token = HtmlSyntaxToken::new_detached(HtmlSyntaxKind::HTML_LITERAL, " scoped", [], []);

let attr = AnyHtmlAttribute::HtmlAttribute(
make::html_attribute(make::html_attribute_name(token)).build(),
);
let mut items: Vec<AnyHtmlAttribute> = old_attributes.iter().collect();
items.push(attr);
let new_attributes = make::html_attribute_list(items);
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual file and its context
cat -n crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs | head -130 | tail -40

Repository: biomejs/biome

Length of output: 2151


🏁 Script executed:

# Check for similar detached token constructions with HTML_LITERAL
rg -n "new_detached.*HTML_LITERAL" -A 2 -B 2

Repository: biomejs/biome

Length of output: 621


🏁 Script executed:

# Search for how html_attribute_name is typically used
rg -n "html_attribute_name" -A 2 -B 2

Repository: biomejs/biome

Length of output: 2625


🏁 Script executed:

# Check the html_factory module for available helper functions
fd html_factory crates/ --type f

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look at how other attributes are created in the same file
cat -n crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs | grep -A 10 "make::html_attribute"

Repository: biomejs/biome

Length of output: 881


🏁 Script executed:

# Look at how vapor_token is created in the related file
cat -n crates/biome_html_analyze/src/lint/nursery/use_vue_vapor.rs | grep -B 10 "vapor_token"

Repository: biomejs/biome

Length of output: 935


🏁 Script executed:

# Check if html_factory has helper functions for token creation
rg -n "pub fn" crates/biome_html_factory/src/generated/node_factory.rs | head -20

Repository: biomejs/biome

Length of output: 1190


🏁 Script executed:

# Search for TriviaPiece usage patterns in html files
rg -n "TriviaPiece" crates/biome_html_analyze/ -A 2 -B 2

Repository: biomejs/biome

Length of output: 4714


🏁 Script executed:

# Check the imports and what's available in the file
head -30 crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs

Repository: biomejs/biome

Length of output: 1036


Use trivia for spacing instead of embedding whitespace in the token text.

The current code embeds a space in " scoped", which can cause issues when trivia is already present. The established pattern in the codebase (see use_vue_vapor.rs for reference) handles spacing via TriviaPiece::whitespace() instead.

Suggested adjustment
-use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
+use biome_rowan::{AstNode, AstNodeList, BatchMutationExt, TriviaPiece};

-        let token = HtmlSyntaxToken::new_detached(HtmlSyntaxKind::HTML_LITERAL, " scoped", [], []);
+        let token = HtmlSyntaxToken::new_detached(
+            HtmlSyntaxKind::HTML_LITERAL,
+            "scoped",
+            [TriviaPiece::whitespace(1)],
+            [],
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs` around
lines 103 - 110, The token currently embeds a leading space by calling
HtmlSyntaxToken::new_detached(HtmlSyntaxKind::HTML_LITERAL, " scoped", ...,
...); instead remove the embedded whitespace (use "scoped" or empty literal as
appropriate) and attach the space as leading trivia using
TriviaPiece::whitespace() so spacing is handled consistently (see the pattern in
use_vue_vapor.rs); update the HtmlSyntaxToken creation for the token used by
make::html_attribute_name and keep the surrounding flow that wraps it in
AnyHtmlAttribute::HtmlAttribute and the attribute list unchanged.

Comment on lines 115 to 119
Some(biome_analyze::RuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Add the "<Emphasis>"scoped"</Emphasis>" attribute so the styles in will only apply to this component." }.to_owned(),
mutation,
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 | 🟡 Minor

Tidy the autofix message grammar.

There’s a tiny typo gremlin in the user-facing text.

✍️ Suggested tweak
-            markup! { "Add the "<Emphasis>"scoped"</Emphasis>" attribute so the styles in will only apply to this component." }.to_owned(),
+            markup! { "Add the "<Emphasis>"scoped"</Emphasis>" attribute so the styles in this block only apply to this component." }.to_owned(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs` around
lines 115 - 119, Update the user-facing autofix message passed into
RuleAction::new (the markup! invocation created via ctx.metadata() in
use_vue_scoped_styles) to fix the grammar: change "Add the \"scoped\" attribute
so the styles in will only apply to this component." to a clean sentence such as
"Add the \"scoped\" attribute so the styles will only apply to this component."
Ensure the corrected string is used in the same markup! call that constructs the
RuleAction description.

Comment on lines 1 to 6
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseVueScopedStylesOptions {}
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 | 🟡 Minor

Add a rustdoc comment for the options struct.

Even a one-liner helps when this shows up in generated docs.

✍️ Suggested tweak
+/// Options for the `useVueScopedStyles` rule.
+/// Currently empty; reserved for future configuration.
 #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
 #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
 pub struct UseVueScopedStylesOptions {}

As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseVueScopedStylesOptions {}
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
/// Options for the `useVueScopedStyles` rule.
/// Currently empty; reserved for future configuration.
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseVueScopedStylesOptions {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/use_vue_scoped_styles.rs` around lines 1 - 6,
Add an inline rustdoc comment above the UseVueScopedStylesOptions struct: insert
a one-line /// comment that briefly describes the purpose of this options struct
(e.g., "Options for the use_vue_scoped_styles lint/assist" or similar) so it
appears in generated docs; place it immediately above the pub struct
UseVueScopedStylesOptions {} declaration and update the wording if fields are
added later.

@dyc3 dyc3 force-pushed the dyc3/use-vue-scoped-styles branch from 68d5e2b to f61ced2 Compare February 21, 2026 21:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/biome_html_analyze/src/lint/nursery/use_vue_scoped_styles.rs`:
- Line 124: Fix the grammar in the action message inside the markup! call in
use_vue_scoped_styles.rs: locate the string that currently reads "Add the
"scoped" attribute so the styles in will only apply to this component." (the
markup! { ... } containing Emphasis) and change it to a grammatically correct
phrase such as "Add the \"scoped\" attribute so the styles will only apply to
this component." or "Add the \"scoped\" attribute so the styles in this block
will only apply to this component."
- Line 109: The token creation embeds a leading space in the token text instead
of using trivia; change the HtmlSyntaxToken::new_detached call that creates the
variable `token` (currently using HtmlSyntaxKind::HTML_LITERAL and text "
scoped") to use the text "scoped" and pass a leading trivia piece of
TriviaPiece::whitespace(1) (following the pattern used in use_vue_vapor.rs) so
spacing is represented as trivia rather than in the token content.

In `@crates/biome_rule_options/src/use_vue_scoped_styles.rs`:
- Around line 1-6: Add a brief Rust doc comment for the
UseVueScopedStylesOptions struct so it shows up in generated docs: locate the
pub struct UseVueScopedStylesOptions declaration and add a short /// doc comment
describing what the options control (one sentence) and any important
fields/behavior; keep it concise and above the #[derive(...)] attributes so
rustdoc picks it up when docs are generated.

@dyc3 dyc3 requested review from a team February 22, 2026 13:48
@ematipico
Copy link
Member

@dyc3, can we make the rule more general and not tailored to Vue? (We can still assign the Vue domain). That's because also Astro allows you to do so: https://docs.astro.build/en/guides/styling/#global-styles

@dyc3
Copy link
Contributor Author

dyc3 commented Feb 22, 2026

Ah, I didn't know that astro had that. I had it vue specific because svelte's component styles are scoped automatically. Sure, we can do that.

@ematipico
Copy link
Member

ematipico commented Feb 22, 2026

I mean, even Svelte has global styles.... https://svelte.dev/docs/svelte/global-styles 😅 😆

@dyc3
Copy link
Contributor Author

dyc3 commented Feb 22, 2026

Yeah true. I'll stick to just the HTML side of this rule for now, and leave the CSS part to another PR.

@dyc3 dyc3 force-pushed the dyc3/use-vue-scoped-styles branch from f61ced2 to 3af7edc Compare February 27, 2026 19:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs (1)

69-72: Keep GlobalStylesKind private and place it below impl Rule.

This enum is internal rule state; it does not need pub, and local convention is to place helper types below the rule impl.

♻️ Suggested patch (visibility)
-pub enum GlobalStylesKind {
+enum GlobalStylesKind {
     Vue,
     Astro { directive: AstroIsDirective },
 }

Based on learnings: In crates/biome_analyze/**/*.rs rule files, all helper functions, structs, and enums must be placed below the impl Rule block. The only exception is when declaring a node union to use in the rule's Query type, which can be kept above the rule block for better readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs` around lines
69 - 72, Make the enum GlobalStylesKind non-public and move its declaration
below the impl Rule block: change its visibility from pub to private (remove
pub) and cut/paste the enum definition so it appears after the impl Rule
implementation in use_scoped_styles.rs, keeping the variant names (Vue, Astro
and Astro { directive: AstroIsDirective }) intact so references inside the rule
still compile.
crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro (1)

1-7: Please add a valid.astro companion fixture.

You’ve got the failing Astro case; adding a passing <style>...</style> case would pin the default-scoped behaviour too.

Based on learnings: Applies to crates/biome_analyze/tests/specs/**/* : Create snapshot tests for rules inside tests/specs/ folder organized by group and rule name. Use files prefixed with invalid for code that triggers the rule and valid for code that doesn't.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro`
around lines 1 - 7, Add a companion passing fixture named valid.astro next to
the failing invalid.astro to assert default-scoped behavior: create a file
alongside invalid.astro containing a normal <style> block (e.g., <style> .foo {
color: red; } </style>) that should NOT produce diagnostics; this follows the
tests/specs/* organization and naming convention for the useScopedStyles rule so
snapshot tests compare invalid (triggers) vs valid (no diagnostics).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/add-use-vue-scoped-styles.md:
- Line 5: Update the changeset text to match the implemented rule name and
behavior: replace references to `useVueScopedStyles` and "Vue-only" with
`useScopedStyles` and mention that it also enforces Astro `is:global` handling
(or that it accepts `module` for CSS Modules where applicable); ensure the
description explains that the rule applies to Vue SFCs and Astro files and
enforces scoped/module or Astro `is:global` usage to prevent style leakage.

In `@crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs`:
- Line 177: The message in the markup! string contains a typo ("styles in will
only apply…"); update the text used in the markup! call (the string passed to
markup! and then .to_owned()) so it reads correctly, e.g. "Add the \"scoped\"
attribute so the styles will only apply to this component." — locate the markup!
{ "Add the "<Emphasis>"scoped"</Emphasis>" attribute ..." } expression and
replace the malformed sentence accordingly.
- Around line 15-20: Update the rustdoc lead for the rule in
use_scoped_styles.rs so it reflects current behavior: instead of saying it
applies to Vue-only, state that the rule enforces scoped styles for Vue SFCs and
also diagnoses Astro `is:global` usage; edit the top comment block (the
docstring above the rule implementation, e.g., for the UseScopedStyles lint) to
mention both Vue and Astro `is:global` handling and briefly describe the
difference in how each framework treats scoped/global styles.
- Around line 109-113: The loop over is_directives currently uses the try
operator (?), which can early-return from the whole function on a malformed
directive and skip remaining directives; change the per-directive extraction to
handle failures locally (e.g., use directive.value().and_then(|v|
v.name().ok()).and_then(|n| n.token_text_trimmed()).map(|name_text| (name_text,
directive.clone())) or use an if-let chain) so that if any step yields None you
continue to the next directive instead of returning; if
name_text.eq_ignore_ascii_case("global") then return
Some(GlobalStylesKind::Astro { directive }) as before.

In `@crates/biome_rule_options/src/use_scoped_styles.rs`:
- Around line 3-6: Add an inline rustdoc comment for the UseScopedStylesOptions
struct describing its purpose and that it currently has no configurable fields
(so it acts as a marker/default options type); update the doc to be a short
one-line summary above the declaration of UseScopedStylesOptions and mention
that additional options can be added later, referencing the struct name to
locate where to add the comment.

---

Nitpick comments:
In `@crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs`:
- Around line 69-72: Make the enum GlobalStylesKind non-public and move its
declaration below the impl Rule block: change its visibility from pub to private
(remove pub) and cut/paste the enum definition so it appears after the impl Rule
implementation in use_scoped_styles.rs, keeping the variant names (Vue, Astro
and Astro { directive: AstroIsDirective }) intact so references inside the rule
still compile.

In `@crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro`:
- Around line 1-7: Add a companion passing fixture named valid.astro next to the
failing invalid.astro to assert default-scoped behavior: create a file alongside
invalid.astro containing a normal <style> block (e.g., <style> .foo { color:
red; } </style>) that should NOT produce diagnostics; this follows the
tests/specs/* organization and naming convention for the useScopedStyles rule so
snapshot tests compare invalid (triggers) vs valid (no diagnostics).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f61ced2 and 3af7edc.

⛔ Files ignored due to path filters (8)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (7)
  • .changeset/add-use-vue-scoped-styles.md
  • crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/use_scoped_styles.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_rule_options/src/lib.rs

"@biomejs/biome": patch
---

Added the nursery rule `useVueScopedStyles` for Vue SFCs. This rule enforces that `<style>` blocks have the `scoped` attribute (or `module` for CSS Modules), preventing style leakage and conflicts between components.
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 | 🟡 Minor

Changeset description is out of date with the implementation.

It still says useVueScopedStyles and Vue-only scope, while the shipped rule is useScopedStyles and also handles Astro is:global.

♻️ Suggested patch
-Added the nursery rule `useVueScopedStyles` for Vue SFCs. This rule enforces that `<style>` blocks have the `scoped` attribute (or `module` for CSS Modules), preventing style leakage and conflicts between components.
+Added the nursery rule `useScopedStyles` for Vue and Astro components. It enforces component-scoped styles by requiring `<style scoped>` (or `module`) in Vue SFCs and disallowing `<style is:global>` in Astro, helping prevent style leakage across components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/add-use-vue-scoped-styles.md at line 5, Update the changeset text
to match the implemented rule name and behavior: replace references to
`useVueScopedStyles` and "Vue-only" with `useScopedStyles` and mention that it
also enforces Astro `is:global` handling (or that it accepts `module` for CSS
Modules where applicable); ensure the description explains that the rule applies
to Vue SFCs and Astro files and enforces scoped/module or Astro `is:global`
usage to prevent style leakage.

Comment on lines +109 to +113
for directive in is_directives {
let name = directive.value().ok()?.name().ok()?;
let name_text = name.token_text_trimmed()?;
if name_text.eq_ignore_ascii_case("global") {
return Some(GlobalStylesKind::Astro { directive });
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 | 🟡 Minor

Avoid bailing out the Astro scan on one malformed directive.

Using ? here can return early and skip checking later directives on the same <style> tag.

♻️ Suggested patch
             for directive in is_directives {
-                let name = directive.value().ok()?.name().ok()?;
-                let name_text = name.token_text_trimmed()?;
-                if name_text.eq_ignore_ascii_case("global") {
+                let is_global = directive
+                    .value()
+                    .ok()
+                    .and_then(|value| value.name().ok())
+                    .and_then(|name| name.token_text_trimmed())
+                    .is_some_and(|name| name.eq_ignore_ascii_case("global"));
+                if is_global {
                     return Some(GlobalStylesKind::Astro { directive });
                 }
             }

Based on learnings: Applies to crates/biome_analyze/**/*.rs : Use functional methods like map, filter, and and_then on Result and Option types instead of nested if let statements to avoid excessive indentation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for directive in is_directives {
let name = directive.value().ok()?.name().ok()?;
let name_text = name.token_text_trimmed()?;
if name_text.eq_ignore_ascii_case("global") {
return Some(GlobalStylesKind::Astro { directive });
for directive in is_directives {
let is_global = directive
.value()
.ok()
.and_then(|value| value.name().ok())
.and_then(|name| name.token_text_trimmed())
.is_some_and(|name| name.eq_ignore_ascii_case("global"));
if is_global {
return Some(GlobalStylesKind::Astro { directive });
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/use_scoped_styles.rs` around lines
109 - 113, The loop over is_directives currently uses the try operator (?),
which can early-return from the whole function on a malformed directive and skip
remaining directives; change the per-directive extraction to handle failures
locally (e.g., use directive.value().and_then(|v| v.name().ok()).and_then(|n|
n.token_text_trimmed()).map(|name_text| (name_text, directive.clone())) or use
an if-let chain) so that if any step yields None you continue to the next
directive instead of returning; if name_text.eq_ignore_ascii_case("global") then
return Some(GlobalStylesKind::Astro { directive }) as before.

@dyc3 dyc3 force-pushed the dyc3/use-vue-scoped-styles branch from 3af7edc to a43adc2 Compare February 27, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-HTML Language: HTML and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants