Skip to content

feat: collect local and global styles for noUndeclearedClasses#9297

Merged
ematipico merged 8 commits into
nextfrom
feat/collect-local-global-styles
Mar 2, 2026
Merged

feat: collect local and global styles for noUndeclearedClasses#9297
ematipico merged 8 commits into
nextfrom
feat/collect-local-global-styles

Conversation

@ematipico

Copy link
Copy Markdown
Member

Summary

This PR continues the work of #9158

Note

The code has been written in a mixed way:

  • myself to create the architecture I wanted
  • AI agent to do the trivial work

Rule renaming

The rules have been renamed to noUndelcaredClasses and noUnusedClasses. I updated the issues too

Module graph enhancement

The module graph has been updated to track:

  • whether a CSS snippet has global or local applicability. While implementing that, I also added Unknown because applicability is limited to embedded snippets for now, so I wanted to keep it scoped to that.
  • The module graph is now able to resolve the imports of other modules inside vue/astro/svelte files. It resolves components and such, so we track CSS classes defined by those components
  • dynamic imports AREN'T supported yet, I will follow up with another PR.
  • Enhanced the snapshot capabilities by implementing the Format trait to the new types we've added so far

noUndelcaredClasses enhancement

The rule noUndelcaredClasses for HTML-ish languages has been enhanced to use the module graph. It now detects local and global styles that were declared locally and by the imported components.

Test Plan

Added many tests

Docs

I will do that in a subsequent PR

@changeset-bot

changeset-bot Bot commented Mar 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 037ffab

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

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

Click here to learn what changesets are, and how to add one.

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

@ematipico ematipico requested review from a team March 2, 2026 11:31
@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages L-CSS Language: CSS and super languages A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages labels Mar 2, 2026
@codspeed-hq

codspeed-hq Bot commented Mar 2, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 151 skipped benchmarks1


Comparing feat/collect-local-global-styles (db999d4) with next (b805cd2)2

Open in CodSpeed

Footnotes

  1. 151 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.

  2. No successful run was found on next (8cd3da1) during the generation of this report, so b805cd2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Renames lint rules noUndeclaredStyles → noUndeclaredClasses and noUnusedStyles → noUnusedClasses; introduces EmbeddingStyleApplicability (Local/Global/Unknown) and CssClassDefinition (name + applicability); makes CssFileSource embedding-aware for Vue/Astro/Svelte and exposes embedding APIs; replaces previous class-collection with HTML-aware traversal and import-tree diagnostics (new traverse/build helpers); updates module-graph APIs and formatting to emit HTML-embedded content; and adds/updates tests and snapshots covering linked, scoped and upward-traversal scenarios.

Possibly related PRs

  • PR 7708: Coordinated rename of noUndeclaredStyles/noUnusedStyles to noUndeclaredClasses/noUnusedClasses and changes to module-graph HTML/CSS embedding — directly touches the same rule and graph surfaces.
  • PR 7702: Modifies HTML-ish embedding and module-graph APIs (EmbeddingStyleApplicability/EmbeddingHtmlKind, HtmlEmbeddedContent, import-tree traversal) that overlap this PR’s core changes.
  • PR 8329: Extends module-graph with HTML/CSS embedding support and related types (CssModuleInfo/HtmlEmbeddedContent), affecting the same traversal and formatting paths.

Suggested reviewers

  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title mentions collecting local and global styles for noUndeclaredClasses, which aligns with the main PR objective of enhancing the rule to detect both local and global styles using the module graph.
Description check ✅ Passed The description clearly relates to the changeset, detailing rule renaming, module graph enhancements, and the noUndeclaredClasses rule improvements across multiple files and frameworks.

✏️ 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 feat/collect-local-global-styles

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (9)
crates/biome_html_syntax/src/element_ext.rs (1)

295-297: New public method lacks rustdoc and has a somewhat generic signature.

This public method would benefit from a brief rustdoc comment explaining its purpose. The method name is_style_scoped suggests checking for the scoped attribute specifically, but the parameter allows any attribute name—consider whether a more descriptive name or dedicated methods (e.g., is_style_scoped() without params, has_style_attribute(name)) would be clearer.

Suggested documentation
+    /// Returns `true` if this is a `<style>` tag that has the specified attribute.
+    ///
+    /// This is commonly used to detect scoped styles in frameworks like Vue (`<style scoped>`).
     pub fn is_style_scoped(&self, attribute_name: &str) -> bool {
         self.is_style_tag() && self.find_attribute_by_name(attribute_name).is_some()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_syntax/src/element_ext.rs` around lines 295 - 297, The new
public function is_style_scoped currently lacks rustdoc and has a generic
signature that accepts any attribute name; add a brief Rustdoc above pub fn
is_style_scoped(&self, attribute_name: &str) -> bool describing that it checks
whether this element is a <style> tag and contains the given attribute, and then
either (A) rename the method to has_style_attribute(&self, name: &str) to
reflect the general check and add rustdoc, or (B) keep is_style_scoped() as a
no-arg method that checks specifically for the "scoped" attribute and implement
a new pub fn has_style_attribute(&self, name: &str) -> bool that calls
self.is_style_tag() && self.find_attribute_by_name(name).is_some(); update
callers accordingly and document both methods; reference the existing helper
find_attribute_by_name when writing the docs.
crates/biome_html_analyze/tests/spec_tests.rs (1)

216-219: Scope snapshot normalisation more narrowly.

Line 218 replaces backslashes across the whole snapshot, which can also alter fixture source text. Prefer normalising only rendered path fragments.

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

In `@crates/biome_html_analyze/tests/spec_tests.rs` around lines 216 - 219, The
current unconditional snapshot.replace('\\', "/") (inside the needs_module_graph
branch) can alter fixture source text; change it to only normalize backslashes
inside rendered path fragments by applying the replacement to matched path-like
substrings rather than the whole snapshot. Locate the needs_module_graph block
and replace the global call on snapshot with a targeted transformation that
scans snapshot for path patterns (e.g., Windows drive or path-like regex
matches) and replaces backslashes with forward slashes only within those
matches, preserving other text in snapshot.
crates/biome_module_graph/tests/snap/mod.rs (1)

48-54: Stabilise snapshot file ordering once, then iterate without cloning.

Sorting by path in constructors avoids order-dependent snapshots and lets assert_snapshot iterate self.files directly.

Proposed patch
-        let files = fs
+        let mut files: Vec<_> = fs
             .files
             .read()
             .iter()
             .map(|(file, entry)| {
                 let content = entry.lock();
                 let content = String::from_utf8_lossy(content.as_slice()).into_owned();
                 (file.as_str().to_string(), content)
             })
             .collect();
+        files.sort_by(|(a, _), (b, _)| a.cmp(b));
         Self {
             module_graph,
             files,
             resolver: None,
         }

-    pub fn from_files(module_graph: &'a ModuleGraph, files: Vec<(String, String)>) -> Self {
+    pub fn from_files(
+        module_graph: &'a ModuleGraph,
+        mut files: Vec<(String, String)>,
+    ) -> Self {
+        files.sort_by(|(a, _), (b, _)| a.cmp(b));
         Self {
             module_graph,
             files,
             resolver: None,
         }
     }

-        let files: Vec<_> = self.files.clone();
-
         let dependency_data = self.module_graph.data();
-        for (file_name, source_code) in &files {
+        for (file_name, source_code) in &self.files {

Also applies to: 65-69

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

In `@crates/biome_module_graph/tests/snap/mod.rs` around lines 48 - 54, The
constructor from_files should stabilise file ordering by sorting the incoming
files Vec by path before storing it (e.g. call files.sort_by(|a,b|
a.0.cmp(&b.0)) or equivalent) so snapshots are order-independent, and then store
the sorted Vec in the files field (module_graph, files, resolver) rather than
leaving it unsorted; apply the same deterministic sort change to the other
constructor variant handling files (the one around lines 65-69) and ensure
callers/consumers (e.g. tests using assert_snapshot) iterate self.files directly
without cloning.
crates/biome_module_graph/src/html_module_info/visitor.rs (1)

72-79: Drop the now-unused stylesheet parameter from visit_html_element.

Since stylesheet extraction moved to visit_self_closing_element, this parameter is dead weight.

Suggested patch
             if let Some(element) = HtmlElement::cast(node.clone()) {
                 self.visit_html_element(
                     element,
-                    &mut referenced_classes,
-                    &mut imported_stylesheets,
+                    &mut referenced_classes,
                 );
             } else if let Some(element) = HtmlSelfClosingElement::cast(node) {
                 self.visit_self_closing_element(element, &mut imported_stylesheets);
             }
@@
     fn visit_html_element(
         &self,
         element: HtmlElement,
         referenced_classes: &mut Vec<CssClassReference>,
-        _imported_stylesheets: &mut Vec<ResolvedPath>,
     ) {

Also applies to: 131-136

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

In `@crates/biome_module_graph/src/html_module_info/visitor.rs` around lines 72 -
79, The call sites pass imported_stylesheets into visit_html_element but
stylesheet extraction was moved to visit_self_closing_element, so remove the
now-unused stylesheet parameter from the visit_html_element signature and all
its call sites (e.g., change the call inside the HtmlElement branch that
currently calls visit_html_element(element, &mut referenced_classes, &mut
imported_stylesheets) to only pass element and &mut referenced_classes), and
update the other occurrence referenced in the comment (around the second call at
the 131-136 region) to match; ensure visit_self_closing_element still accepts
&mut imported_stylesheets and that visit_html_element’s implementation no longer
expects or uses the stylesheet parameter.
crates/biome_service/src/workspace/server.rs (2)

164-167: Gate this test-only accessor behind the testing feature.

The doc says “for use in tests”, but Lines 164-167 are exposed in non-testing builds too. Keeping it feature-gated avoids widening the public surface by accident.

Suggested patch
     /// Returns the module graph, for use in tests.
+    #[cfg(feature = "testing")]
     pub fn module_graph(&self) -> Arc<ModuleGraph> {
         self.module_graph.clone()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_service/src/workspace/server.rs` around lines 164 - 167, Gate
the test-only accessor module_graph behind the testing feature by adding a
conditional compilation attribute to the method (e.g., annotate the pub fn
module_graph(&self) with #[cfg(feature = "testing")]) so it is only compiled
when that feature is enabled; optionally add #[cfg_attr(docsrs, doc(cfg(feature
= "testing")))] above the function to make the feature requirement visible in
docs and keep the signature (pub fn module_graph(&self) -> Arc<ModuleGraph>)
unchanged.

178-196: Don’t swallow indexing failures in the test helper.

Line 184 currently drops the Result. If parsing/indexing fails, tests can silently proceed with a partial graph.

Suggested patch
     #[cfg(feature = "testing")]
     pub fn index_files_for_test(
         &self,
         project_key: ProjectKey,
         files: impl IntoIterator<Item = (BiomePath, DocumentFileSource)>,
-    ) {
+    ) -> Result<(), WorkspaceError> {
         for (path, document_file_source) in files {
-            let _ = self.open_file_internal(
+            self.open_file_internal(
                 OpenFileReason::Index(IndexTrigger::InitialScan),
                 OpenFileParams {
                     project_key,
                     path,
                     content: FileContent::FromServer,
                     document_file_source: Some(document_file_source),
                     persist_node_cache: false,
                     inline_config: None,
                 },
-            );
+            )?;
         }
+        Ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_service/src/workspace/server.rs` around lines 178 - 196, The
helper index_files_for_test currently drops the Result from open_file_internal
which hides parsing/indexing failures; update index_files_for_test to surface
errors by changing its signature to return a Result (e.g., Result<(), E> or
anyhow::Result<()>) and propagate the open_file_internal result using the ?
operator (or collect and return the first Err), or alternatively call
.expect/.unwrap with a clear message to fail fast in tests; ensure you reference
the open_file_internal call inside index_files_for_test and propagate errors
originating from OpenFileReason::Index / OpenFileParams so tests don't continue
on partial indexing.
crates/biome_module_graph/src/html_module_info/mod.rs (1)

96-103: Rustdoc for style_classes is stale after the type change.

The comment still says “Each TokenText…”, but the field is now IndexSet<CssClassDefinition>.

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

In `@crates/biome_module_graph/src/html_module_info/mod.rs` around lines 96 - 103,
The rustdoc for the field style_classes is out of date: it still refers to
TokenText even though the field type is now IndexSet<CssClassDefinition>. Update
the comment for the style_classes field to describe that it stores
CssClassDefinition entries (not TokenText), mention that these are collected
from CssClassSelector nodes in embedded CSS ASTs (no re-parsing), and briefly
note what a CssClassDefinition represents (e.g., a class name plus any
associated metadata like source/span) so readers know why the richer type is
used.
crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs (1)

139-148: Build the import tree once per attribute, not once per missing class.

Lines 139-148 recompute the same tree for every missing class token. Cache it lazily in run and reuse it for all emitted signals.

🤖 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/no_undeclared_classes.rs` around
lines 139 - 148, The code currently calls
module_graph.build_import_tree_for_html(file_path) for every missing class
inside the loop (when !found_class), which recomputes the same import tree
repeatedly; change this by computing and caching the import tree once per
attribute-run: add an Option or lazy variable in run (e.g., let mut
cached_import_tree: Option<ImportTree> = None) and inside the loop, when you
first need it call module_graph.build_import_tree_for_html(file_path) and store
it in cached_import_tree, then reuse cached_import_tree.as_ref() for all
subsequent UndeclaredClass signals instead of calling build_import_tree_for_html
repeatedly; update references in the block that constructs UndeclaredClass (the
import_tree field) to use the cached value.
crates/biome_test_utils/src/lib.rs (1)

374-388: Make recursive file collection deterministic for stable snapshots.

read_dir ordering is filesystem-dependent; sorting entries here reduces snapshot churn and “works on my machine” noise.

Suggested patch
 fn collect_all_files_in_dir(dir: &Utf8Path, out: &mut Vec<Utf8PathBuf>) {
     let Ok(entries) = std::fs::read_dir(dir.as_std_path()) else {
         return;
     };
-    for entry in entries.flatten() {
+    let mut entries: Vec<_> = entries.flatten().collect();
+    entries.sort_by_key(|entry| entry.path());
+    for entry in entries {
         let Ok(path) = Utf8PathBuf::try_from(entry.path()) else {
             continue;
         };
         if path.is_dir() {
             collect_all_files_in_dir(&path, out);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_test_utils/src/lib.rs` around lines 374 - 388, The recursive
collector collect_all_files_in_dir uses std::fs::read_dir which yields
filesystem-dependent ordering; make it deterministic by collecting entries into
a Vec, mapping/filtering to Utf8PathBufs (using the existing try_from logic),
sorting that Vec by a stable key (e.g., path.as_str() or path.to_string()) and
then iterating the sorted list to recurse or push to out so snapshots are stable
across machines; preserve the existing error/continue behavior for entries that
fail conversion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_css_syntax/src/file_source.rs`:
- Around line 223-231: The is_applied_locally method currently only considers
Vue local embeddings; update its match to also return true for Astro and Svelte
local embeddings by adding their corresponding patterns (e.g.,
EmbeddingHtmlKind::Astro { applicability: EmbeddingApplicability::Local } and
EmbeddingHtmlKind::Svelte { applicability: EmbeddingApplicability::Local })
under the matches against self.embedding_kind (keeping the existing
EmbeddingKind and EmbeddingHtmlKind/EmbeddingApplicability symbols to locate the
code).

In `@crates/biome_css_syntax/src/selector_ext.rs`:
- Around line 47-51: is_global_pseudo currently compares the token text with
exact "global", which fails for uppercase or mixed-case pseudo names; update the
comparison in is_global_pseudo so the token text is matched case-insensitively
(e.g., use an ASCII case-insensitive comparison like eq_ignore_ascii_case or
lowercasing) when checking token.text_trimmed() == "global" so :GLOBAL and
:Global also match; keep the existing name()/value_token() chain and only change
the final equality check.

In `@crates/biome_html_syntax/src/element_ext.rs`:
- Around line 290-293: The doc and impl disagree: update the is_sass_lang
function to return true for both "scss" and "sass" values; modify the body of
is_sass_lang (which already calls is_style_tag() and has_attribute_with_value)
to check has_attribute_with_value("lang", "scss") ||
has_attribute_with_value("lang", "sass") (or equivalent case-insensitive check)
so the function matches the doc comment.

In `@crates/biome_module_graph/tests/snap/mod.rs`:
- Around line 27-35: The snapshot helper currently calls
std::str::from_utf8(content.as_slice()).unwrap() inside the map over
fs.files.read(), which will panic on non-UTF8 fixture bytes; replace that unwrap
with a lossy conversion such as
String::from_utf8_lossy(content.as_slice()).to_string() (or otherwise handle
invalid UTF-8 without panicking) in the closure where entry.lock() and
content.as_slice() are used so snapshot generation remains resilient.

In `@crates/biome_module_graph/tests/spec_tests.rs`:
- Around line 2937-3198: The test names and inline comments (e.g.,
test_html_inline_style_classes_are_global,
test_vue_unscoped_style_classes_are_global,
test_vue_scoped_style_classes_are_local_and_hidden,
test_vue_mixed_scoped_and_unscoped, test_astro_local_style_classes_are_hidden,
test_astro_global_style_classes_are_visible,
test_svelte_local_style_classes_are_hidden) contradict the asserted behavior
(EmbeddingApplicability::Unknown for plain HTML inline styles and same-file
visibility for local/scoped styles). Rename tests and update their comments to
reflect the actual assertions (e.g., "html inline styles are Unknown", "vue
scoped styles are Local but visible in same-file traversal", "astro/svelte local
styles are Local but appear in same-file traversal", "vue mixed shows both
stored but scoped only applies same-file") so names and commentary match the
asserted applicability and traversal expectations.

In `@crates/biome_rule_options/src/no_undeclared_classes.rs`:
- Line 6: Add an inline rustdoc comment describing the purpose and usage of the
public options type NoUndeclaredClassesOptions (e.g., a one-line summary
explaining what rule options it represents and when to use/modify them) directly
above the pub struct NoUndeclaredClassesOptions {} declaration so the public
type is documented per the project coding guidelines.

In `@crates/biome_service/src/lib.rs`:
- Line 17: Hide the test utilities behind a cargo feature by gating the
test_utils module with the testing feature: replace the unconditional pub mod
test_utils declaration with a conditional compilation attribute so test_utils is
only compiled and exposed when the "testing" feature is enabled (use
#[cfg(feature = "testing")] on the pub mod test_utils declaration, and consider
#[cfg_attr(not(feature = "testing"), allow(dead_code))] or similar only if
needed for warnings).

---

Nitpick comments:
In `@crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs`:
- Around line 139-148: The code currently calls
module_graph.build_import_tree_for_html(file_path) for every missing class
inside the loop (when !found_class), which recomputes the same import tree
repeatedly; change this by computing and caching the import tree once per
attribute-run: add an Option or lazy variable in run (e.g., let mut
cached_import_tree: Option<ImportTree> = None) and inside the loop, when you
first need it call module_graph.build_import_tree_for_html(file_path) and store
it in cached_import_tree, then reuse cached_import_tree.as_ref() for all
subsequent UndeclaredClass signals instead of calling build_import_tree_for_html
repeatedly; update references in the block that constructs UndeclaredClass (the
import_tree field) to use the cached value.

In `@crates/biome_html_analyze/tests/spec_tests.rs`:
- Around line 216-219: The current unconditional snapshot.replace('\\', "/")
(inside the needs_module_graph branch) can alter fixture source text; change it
to only normalize backslashes inside rendered path fragments by applying the
replacement to matched path-like substrings rather than the whole snapshot.
Locate the needs_module_graph block and replace the global call on snapshot with
a targeted transformation that scans snapshot for path patterns (e.g., Windows
drive or path-like regex matches) and replaces backslashes with forward slashes
only within those matches, preserving other text in snapshot.

In `@crates/biome_html_syntax/src/element_ext.rs`:
- Around line 295-297: The new public function is_style_scoped currently lacks
rustdoc and has a generic signature that accepts any attribute name; add a brief
Rustdoc above pub fn is_style_scoped(&self, attribute_name: &str) -> bool
describing that it checks whether this element is a <style> tag and contains the
given attribute, and then either (A) rename the method to
has_style_attribute(&self, name: &str) to reflect the general check and add
rustdoc, or (B) keep is_style_scoped() as a no-arg method that checks
specifically for the "scoped" attribute and implement a new pub fn
has_style_attribute(&self, name: &str) -> bool that calls self.is_style_tag() &&
self.find_attribute_by_name(name).is_some(); update callers accordingly and
document both methods; reference the existing helper find_attribute_by_name when
writing the docs.

In `@crates/biome_module_graph/src/html_module_info/mod.rs`:
- Around line 96-103: The rustdoc for the field style_classes is out of date: it
still refers to TokenText even though the field type is now
IndexSet<CssClassDefinition>. Update the comment for the style_classes field to
describe that it stores CssClassDefinition entries (not TokenText), mention that
these are collected from CssClassSelector nodes in embedded CSS ASTs (no
re-parsing), and briefly note what a CssClassDefinition represents (e.g., a
class name plus any associated metadata like source/span) so readers know why
the richer type is used.

In `@crates/biome_module_graph/src/html_module_info/visitor.rs`:
- Around line 72-79: The call sites pass imported_stylesheets into
visit_html_element but stylesheet extraction was moved to
visit_self_closing_element, so remove the now-unused stylesheet parameter from
the visit_html_element signature and all its call sites (e.g., change the call
inside the HtmlElement branch that currently calls visit_html_element(element,
&mut referenced_classes, &mut imported_stylesheets) to only pass element and
&mut referenced_classes), and update the other occurrence referenced in the
comment (around the second call at the 131-136 region) to match; ensure
visit_self_closing_element still accepts &mut imported_stylesheets and that
visit_html_element’s implementation no longer expects or uses the stylesheet
parameter.

In `@crates/biome_module_graph/tests/snap/mod.rs`:
- Around line 48-54: The constructor from_files should stabilise file ordering
by sorting the incoming files Vec by path before storing it (e.g. call
files.sort_by(|a,b| a.0.cmp(&b.0)) or equivalent) so snapshots are
order-independent, and then store the sorted Vec in the files field
(module_graph, files, resolver) rather than leaving it unsorted; apply the same
deterministic sort change to the other constructor variant handling files (the
one around lines 65-69) and ensure callers/consumers (e.g. tests using
assert_snapshot) iterate self.files directly without cloning.

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 164-167: Gate the test-only accessor module_graph behind the
testing feature by adding a conditional compilation attribute to the method
(e.g., annotate the pub fn module_graph(&self) with #[cfg(feature = "testing")])
so it is only compiled when that feature is enabled; optionally add
#[cfg_attr(docsrs, doc(cfg(feature = "testing")))] above the function to make
the feature requirement visible in docs and keep the signature (pub fn
module_graph(&self) -> Arc<ModuleGraph>) unchanged.
- Around line 178-196: The helper index_files_for_test currently drops the
Result from open_file_internal which hides parsing/indexing failures; update
index_files_for_test to surface errors by changing its signature to return a
Result (e.g., Result<(), E> or anyhow::Result<()>) and propagate the
open_file_internal result using the ? operator (or collect and return the first
Err), or alternatively call .expect/.unwrap with a clear message to fail fast in
tests; ensure you reference the open_file_internal call inside
index_files_for_test and propagate errors originating from OpenFileReason::Index
/ OpenFileParams so tests don't continue on partial indexing.

In `@crates/biome_test_utils/src/lib.rs`:
- Around line 374-388: The recursive collector collect_all_files_in_dir uses
std::fs::read_dir which yields filesystem-dependent ordering; make it
deterministic by collecting entries into a Vec, mapping/filtering to
Utf8PathBufs (using the existing try_from logic), sorting that Vec by a stable
key (e.g., path.as_str() or path.to_string()) and then iterating the sorted list
to recurse or push to out so snapshots are stable across machines; preserve the
existing error/continue behavior for entries that fail conversion.

ℹ️ 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 8cd3da1 and 8009005.

⛔ Files ignored due to path filters (56)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_undeclared_classes_shows_checked_import_chain.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_component_hierarchy_child_imports_css.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_component_hierarchy_nested_three_levels.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_component_hierarchy_parent_imports_css.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_dynamic_classname_not_collected.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_global_pseudo_class_is_exempt.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_mixed_html_jsx_consumers.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_referenced_class_not_flagged.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_selector_patterns_all_referenced.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_selector_patterns_partial_reference.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_transitive_css_import.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_css_files/no_unused_classes_unreferenced_class_flagged.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_html/no_undeclared_classes_passes_when_declared.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_html/no_undeclared_classes_reports_undeclared_classes.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_html/no_undeclared_classes_silent_without_style_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/invalid.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/partial-invalid/partial_invalid.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/transitive-valid/transitive_theme.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/transitive-valid/transitive_valid.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/tsx_valid.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/valid.css.snap is excluded by !**/*.snap and included by **
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedStyles/valid.css.snap is excluded by !**/*.snap 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/noUndeclaredClasses/invalid-linked-stylesheet/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-scoped-styles/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/App.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/Button.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/Page.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/App.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/Button.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/Page.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-linked-stylesheet/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-scoped-styles/valid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-direct-import/directImport.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level2.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level3.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level4.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/multilevel.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/App.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Block.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Button.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Page.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_astro_component_imports_snapshot.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_jsx_imports_css_file.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_svelte_component_imports_snapshot.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_vue_component_imports_snapshot.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (86)
  • .changeset/add-no-undeclared-styles.md
  • .changeset/add-no-unused-styles.md
  • crates/biome_cli/tests/cases/handle_css_files.rs
  • crates/biome_cli/tests/cases/html.rs
  • crates/biome_css_analyze/src/lint/nursery/no_unused_classes.rs
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/invalid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/partial-invalid/partial_invalid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/partial-invalid/partial_invalid.jsx
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/transitive-valid/transitive_theme.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/transitive-valid/transitive_valid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/transitive-valid/transitive_valid.jsx
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/tsx_valid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/tsx_valid.tsx
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/valid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedClasses/valid.jsx
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedStyles/invalid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedStyles/valid.css
  • crates/biome_css_syntax/src/file_source.rs
  • crates/biome_css_syntax/src/lib.rs
  • crates/biome_css_syntax/src/selector_ext.rs
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_html_analyze/tests/spec_tests.rs
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-linked-stylesheet/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-linked-stylesheet/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-scoped-styles/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/App.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/Button.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/Page.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-astro/app.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/App.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/Button.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/Page.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-upward-traversal-vue/app.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-linked-stylesheet/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-linked-stylesheet/valid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-scoped-styles/valid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.svelte
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid.vue
  • crates/biome_html_syntax/src/element_ext.rs
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/button.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-direct-import/directImport.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-direct-import/styles.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level1.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level2.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level2.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level3.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level3.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level4.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/level4.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/multi-level-chain/multilevel.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/App.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Block.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Button.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/Page.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/upward-traversal/app.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/styles.css
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/valid.jsx
  • crates/biome_module_graph/Cargo.toml
  • crates/biome_module_graph/src/css_module_info/mod.rs
  • crates/biome_module_graph/src/css_module_info/traverse.rs
  • crates/biome_module_graph/src/css_module_info/visitor.rs
  • crates/biome_module_graph/src/format_module_graph.rs
  • crates/biome_module_graph/src/html_module_info/mod.rs
  • crates/biome_module_graph/src/html_module_info/visitor.rs
  • crates/biome_module_graph/src/js_module_info/visitor.rs
  • crates/biome_module_graph/src/lib.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/snap/mod.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_undeclared_classes.rs
  • crates/biome_rule_options/src/no_unused_classes.rs
  • crates/biome_service/Cargo.toml
  • crates/biome_service/src/file_handlers/html.rs
  • crates/biome_service/src/lib.rs
  • crates/biome_service/src/test_utils.rs
  • crates/biome_service/src/workspace/document/mod.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_service/src/workspace/server.tests.rs
  • crates/biome_test_utils/Cargo.toml
  • crates/biome_test_utils/src/lib.rs
💤 Files with no reviewable changes (2)
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedStyles/invalid.css
  • crates/biome_css_analyze/tests/specs/nursery/noUnusedStyles/valid.css

Comment thread crates/biome_css_syntax/src/file_source.rs Outdated
Comment thread crates/biome_css_syntax/src/selector_ext.rs Outdated
Comment thread crates/biome_html_syntax/src/element_ext.rs
Comment thread crates/biome_module_graph/tests/snap/mod.rs
Comment thread crates/biome_module_graph/tests/spec_tests.rs
Comment thread crates/biome_rule_options/src/no_undeclared_classes.rs
Comment thread crates/biome_service/src/lib.rs

@dyc3 dyc3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tests look good. Just a couple nits

Comment thread crates/biome_css_syntax/src/file_source.rs Outdated
Comment thread crates/biome_css_syntax/src/file_source.rs Outdated
Comment thread crates/biome_css_syntax/src/file_source.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/biome_html_syntax/src/element_ext.rs (1)

297-299: Please add a brief rustdoc for is_style_scoped.

Line 297 adds a public helper; a short doc comment would clarify intended attribute names (scoped, global, framework-specific variants) for callers.

Suggested tweak
+    /// Returns `true` when this is a `<style>` tag and `attribute_name` is present
+    /// on its opening tag (for example, `scoped` or framework-specific style flags).
     pub fn is_style_scoped(&self, attribute_name: &str) -> bool {
         self.is_style_tag() && self.find_attribute_by_name(attribute_name).is_some()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_syntax/src/element_ext.rs` around lines 297 - 299, Add a
short Rust doc comment for the public method is_style_scoped explaining its
purpose and the expected attribute names it checks (e.g., "scoped", "global",
and any framework-specific variants), and mention that it returns true only for
style tags when find_attribute_by_name(attribute_name) is Some. Update the doc
above pub fn is_style_scoped(&self, attribute_name: &str) -> bool to reference
related helpers is_style_tag and find_attribute_by_name for context.
crates/biome_module_graph/tests/snap/mod.rs (1)

65-69: Stabilise file ordering before rendering snapshots.

Snapshot output currently depends on incoming files order. Sorting once here will prevent order-based flakes across callers and environments.

Suggested patch
-        let files: Vec<_> = self.files.clone();
+        let mut files: Vec<_> = self.files.iter().collect();
+        files.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));

         let dependency_data = self.module_graph.data();
-        for (file_name, source_code) in &files {
+        for (file_name, source_code) in files {
             let file_name = Utf8PathBuf::from(file_name.as_str());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/tests/snap/mod.rs` around lines 65 - 69, The
snapshot flakiness is caused by relying on the incoming order of self.files; fix
it by sorting the cloned files Vec before rendering. After let files: Vec<_> =
self.files.clone(); sort the files collection (e.g., using
files.sort_unstable_by_key(|(name, _)| name.clone()) or a stable sort if needed)
so the subsequent loop for (file_name, source_code) in &files produces
deterministic ordering for snapshots.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_html_syntax/src/element_ext.rs`:
- Around line 297-299: Add a short Rust doc comment for the public method
is_style_scoped explaining its purpose and the expected attribute names it
checks (e.g., "scoped", "global", and any framework-specific variants), and
mention that it returns true only for style tags when
find_attribute_by_name(attribute_name) is Some. Update the doc above pub fn
is_style_scoped(&self, attribute_name: &str) -> bool to reference related
helpers is_style_tag and find_attribute_by_name for context.

In `@crates/biome_module_graph/tests/snap/mod.rs`:
- Around line 65-69: The snapshot flakiness is caused by relying on the incoming
order of self.files; fix it by sorting the cloned files Vec before rendering.
After let files: Vec<_> = self.files.clone(); sort the files collection (e.g.,
using files.sort_unstable_by_key(|(name, _)| name.clone()) or a stable sort if
needed) so the subsequent loop for (file_name, source_code) in &files produces
deterministic ordering for snapshots.

ℹ️ 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 8009005 and 4d5abfe.

⛔ Files ignored due to path filters (2)
  • 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)
  • crates/biome_css_syntax/src/file_source.rs
  • crates/biome_css_syntax/src/selector_ext.rs
  • crates/biome_html_syntax/src/element_ext.rs
  • crates/biome_module_graph/Cargo.toml
  • crates/biome_module_graph/tests/snap/mod.rs
  • crates/biome_rule_options/src/no_undeclared_classes.rs
  • crates/biome_service/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/biome_css_syntax/src/selector_ext.rs
  • crates/biome_service/src/lib.rs
  • crates/biome_module_graph/Cargo.toml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/biome_module_graph/src/html_module_info/mod.rs (1)

96-103: Doc comment is stale after the type change.

The text still says each entry is a TokenText, but the field now stores CssClassDefinition (name + applicability).

Suggested doc tweak
-    /// Each `TokenText` represents a single class name (e.g., "header" from `.header`).
+    /// Each `CssClassDefinition` stores a class name (e.g., "header" from `.header`)
+    /// plus its applicability (Local/Global/Unknown).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/html_module_info/mod.rs` around lines 96 - 103,
The doc comment for the field style_classes is stale: it still claims each entry
is a TokenText but the field type is now IndexSet<CssClassDefinition>. Update
the comment on style_classes to describe that each entry is a CssClassDefinition
containing the class name and applicability (or similar metadata) rather than
TokenText, and remove or adjust the references to TokenText so the documentation
matches the CssClassDefinition structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/src/html_module_info/visitor.rs`:
- Around line 68-80: The visitor currently only collects class attribute
references for HtmlElement in visit_html_element, while HtmlSelfClosingElement
nodes (handled by visit_self_closing_element) only handle stylesheets, causing
class="" on self-closing tags to be missed; update visit_self_closing_element
(or extract a shared attribute collector used by both visit_html_element and
visit_self_closing_element) to also extract and add class names into
referenced_classes (and ensure the same parsing logic used for classes in
visit_html_element is reused) — also apply the same change for the second
occurrence handling mentioned around the 167-195 range so self-closing elements
there also contribute to referenced_classes.

---

Nitpick comments:
In `@crates/biome_module_graph/src/html_module_info/mod.rs`:
- Around line 96-103: The doc comment for the field style_classes is stale: it
still claims each entry is a TokenText but the field type is now
IndexSet<CssClassDefinition>. Update the comment on style_classes to describe
that each entry is a CssClassDefinition containing the class name and
applicability (or similar metadata) rather than TokenText, and remove or adjust
the references to TokenText so the documentation matches the CssClassDefinition
structure.

ℹ️ 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 4d5abfe and 7307c56.

⛔ Files ignored due to path filters (2)
  • 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 (8)
  • crates/biome_css_syntax/src/file_source.rs
  • crates/biome_css_syntax/src/lib.rs
  • crates/biome_module_graph/src/css_module_info/mod.rs
  • crates/biome_module_graph/src/html_module_info/mod.rs
  • crates/biome_module_graph/src/html_module_info/visitor.rs
  • crates/biome_module_graph/src/lib.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_service/src/file_handlers/html.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_module_graph/src/css_module_info/mod.rs

Comment thread crates/biome_module_graph/src/html_module_info/visitor.rs
@ematipico ematipico force-pushed the feat/collect-local-global-styles branch from db999d4 to b1dd59b Compare March 2, 2026 20:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/biome_module_graph/tests/spec_tests.rs (1)

2899-3230: ⚠️ Potential issue | 🟡 Minor

Doc/test wording still disagrees with actual behaviour.

Line 2899 says plain HTML <style> is “always Global”, but Line 2962 asserts Unknown. Likewise, names like “are_global/hidden” in this block conflict with assertions that local classes appear in same-file traversal (for example Lines 3079, 3131, 3161, 3224). Please align names/comments with what is asserted.

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

In `@crates/biome_module_graph/tests/spec_tests.rs` around lines 2899 - 3230, The
docstrings/test names/comments misstate behavior: plain HTML inline style
applicability is EmbeddingStyleApplicability::Unknown (not "always Global") and
many tests that say "hidden" actually assert Local classes still appear in
same-file traversal; update the wording to match assertions. Edit the
html_css_source doc comment and the test name/comment for
test_html_inline_style_classes_are_global to reflect "Unknown" applicability (or
rename to test_html_inline_style_classes_are_unknown), and change comments/names
that claim "hidden"/"are_hidden" (e.g.,
test_astro_local_style_classes_are_hidden,
test_svelte_local_style_classes_are_hidden,
test_vue_scoped_style_classes_are_local_and_hidden) to indicate "Local (hidden
from cross-file traversal but present in same-file traversal)" or similar
consistent phrasing so the test names and inline comments match the assertions
using EmbeddingStyleApplicability and traverse_import_tree_for_html_classes
behavior.
🧹 Nitpick comments (1)
crates/biome_module_graph/src/html_module_info/visitor.rs (1)

145-168: Consider a shared class-attribute collector for both element kinds.

The class extraction logic is duplicated across visit_html_element and visit_self_closing_element; extracting one helper would reduce drift and prevent this bug class from reappearing.

Also applies to: 183-201

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

In `@crates/biome_module_graph/src/html_module_info/visitor.rs` around lines 145 -
168, Duplicate class-attribute extraction appears in visit_html_element and
visit_self_closing_element; refactor by extracting a helper (e.g.,
collect_class_attrs_from_opening) that takes the opening node (the value used in
opening.attributes()/opening) plus &self.file_path and referenced_classes and
encapsulates the logic currently inside both loops (checking
attr.as_html_attribute(), getting name_token via
attr.name().ok().and_then(|name| name.value_token().ok()), comparing
name_text.eq_ignore_ascii_case("class"), then calling
collect_class_attribute_reference on the initializer/value_node). Replace the
duplicated blocks in visit_html_element and visit_self_closing_element with a
single call to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/src/html_module_info/visitor.rs`:
- Around line 269-275: The rustdoc applicability table in visitor.rs incorrectly
states that plain HTML `<style>` maps to `Global`; update the documentation in
the comment block in crates::biome_module_graph::html_module_info::visitor (the
rustdoc table starting at the applicability list) to mark "Plain HTML `<style>`"
as `Unknown` to match current behaviour/tests so the docs reflect reality.

---

Duplicate comments:
In `@crates/biome_module_graph/tests/spec_tests.rs`:
- Around line 2899-3230: The docstrings/test names/comments misstate behavior:
plain HTML inline style applicability is EmbeddingStyleApplicability::Unknown
(not "always Global") and many tests that say "hidden" actually assert Local
classes still appear in same-file traversal; update the wording to match
assertions. Edit the html_css_source doc comment and the test name/comment for
test_html_inline_style_classes_are_global to reflect "Unknown" applicability (or
rename to test_html_inline_style_classes_are_unknown), and change comments/names
that claim "hidden"/"are_hidden" (e.g.,
test_astro_local_style_classes_are_hidden,
test_svelte_local_style_classes_are_hidden,
test_vue_scoped_style_classes_are_local_and_hidden) to indicate "Local (hidden
from cross-file traversal but present in same-file traversal)" or similar
consistent phrasing so the test names and inline comments match the assertions
using EmbeddingStyleApplicability and traverse_import_tree_for_html_classes
behavior.

---

Nitpick comments:
In `@crates/biome_module_graph/src/html_module_info/visitor.rs`:
- Around line 145-168: Duplicate class-attribute extraction appears in
visit_html_element and visit_self_closing_element; refactor by extracting a
helper (e.g., collect_class_attrs_from_opening) that takes the opening node (the
value used in opening.attributes()/opening) plus &self.file_path and
referenced_classes and encapsulates the logic currently inside both loops
(checking attr.as_html_attribute(), getting name_token via
attr.name().ok().and_then(|name| name.value_token().ok()), comparing
name_text.eq_ignore_ascii_case("class"), then calling
collect_class_attribute_reference on the initializer/value_node). Replace the
duplicated blocks in visit_html_element and visit_self_closing_element with a
single call to this helper.

ℹ️ 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 7307c56 and b1dd59b.

⛔ Files ignored due to path filters (1)
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
📒 Files selected for processing (2)
  • crates/biome_module_graph/src/html_module_info/visitor.rs
  • crates/biome_module_graph/tests/spec_tests.rs

Comment on lines +269 to +275
/// - Plain HTML `<style>` → `Global`
/// - Vue `<style>` (no `scoped`) → `Global`
/// - Vue `<style scoped>` → `Local`
/// - Astro `<style>` (default) → `Local`
/// - Astro `<style is:global>` → `Global`
/// - Svelte `<style>` (default) → `Local`
///

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rustdoc applicability table is stale for plain HTML.

Line 269 says plain HTML <style> is Global, but current behaviour/tests treat it as Unknown. Please update the doc to match reality and avoid future confusion.

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

In `@crates/biome_module_graph/src/html_module_info/visitor.rs` around lines 269 -
275, The rustdoc applicability table in visitor.rs incorrectly states that plain
HTML `<style>` maps to `Global`; update the documentation in the comment block
in crates::biome_module_graph::html_module_info::visitor (the rustdoc table
starting at the applicability list) to mark "Plain HTML `<style>`" as `Unknown`
to match current behaviour/tests so the docs reflect reality.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/biome_module_graph/src/html_module_info/visitor.rs (1)

135-139: Consider removing the unused _imported_stylesheets parameter.

This parameter is never used because <link> elements are self-closing and handled in visit_self_closing_element. If kept for API symmetry, a brief comment explaining the rationale would help future readers.

♻️ Suggested simplification
     fn visit_html_element(
         &self,
         element: HtmlElement,
         referenced_classes: &mut Vec<CssClassReference>,
-        _imported_stylesheets: &mut Vec<ResolvedPath>,
     ) {

And update the call site at line 73-77:

                 self.visit_html_element(
                     element,
                     &mut referenced_classes,
-                    &mut imported_stylesheets,
                 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/html_module_info/visitor.rs` around lines 135 -
139, The visit_html_element method currently accepts an unused parameter
_imported_stylesheets; remove this parameter from the visit_html_element
signature in visitor.rs and update all call sites (where
Visitor::visit_html_element is invoked) to no longer pass the
imported_stylesheets argument, or alternatively if you prefer API symmetry keep
the parameter but rename it to _imported_stylesheets and add a short comment
inside visit_html_element explaining that imported stylesheets are handled in
visit_self_closing_element (e.g., "Imported <link> stylesheets are handled in
visit_self_closing_element; parameter kept for symmetry"), and ensure any
compiler warnings are resolved by using the underscore-prefixed name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_module_graph/src/html_module_info/visitor.rs`:
- Around line 135-139: The visit_html_element method currently accepts an unused
parameter _imported_stylesheets; remove this parameter from the
visit_html_element signature in visitor.rs and update all call sites (where
Visitor::visit_html_element is invoked) to no longer pass the
imported_stylesheets argument, or alternatively if you prefer API symmetry keep
the parameter but rename it to _imported_stylesheets and add a short comment
inside visit_html_element explaining that imported stylesheets are handled in
visit_self_closing_element (e.g., "Imported <link> stylesheets are handled in
visit_self_closing_element; parameter kept for symmetry"), and ensure any
compiler warnings are resolved by using the underscore-prefixed name.

ℹ️ 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 b1dd59b and 037ffab.

📒 Files selected for processing (1)
  • crates/biome_module_graph/src/html_module_info/visitor.rs

@ematipico ematipico merged commit b805cd2 into next Mar 2, 2026
32 checks passed
@ematipico ematipico deleted the feat/collect-local-global-styles branch March 2, 2026 21:57
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-Parser Area: parser A-Project Area: project L-CSS Language: CSS and super languages L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants