Skip to content

feat: improve noUndeclaredClasses#9503

Merged
ematipico merged 6 commits intonextfrom
feat/improve-undeclared-classes
Mar 30, 2026
Merged

feat: improve noUndeclaredClasses#9503
ematipico merged 6 commits intonextfrom
feat/improve-undeclared-classes

Conversation

@ematipico
Copy link
Copy Markdown
Member

@ematipico ematipico commented Mar 16, 2026

Summary

This PR improves noUndeclaredClasses on two fronts:

  • tracking of CSS files imported using dynamic imports
  • correctly parsing and tracking the Astro class:list bindings
  • correctly track the use of classes with JSX bindings e.g. className={someClass}

Tests created with an AI agent.
Architecture designed by me and implemented partially by me.

Test Plan

Added many tests. Some CLI tests were moved into the analyze infra.

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 16, 2026

⚠️ No Changeset found

Latest commit: c895967

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 no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

@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 labels Mar 16, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 16, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 168 skipped benchmarks1


Comparing feat/improve-undeclared-classes (c895967) with next (d16e32b)

Open in CodSpeed

Footnotes

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

@ematipico ematipico force-pushed the feat/improve-undeclared-classes branch from a4385e6 to 20bfcdd Compare March 25, 2026 15:01
@github-actions github-actions Bot added the L-HTML Language: HTML and super languages label Mar 25, 2026
@ematipico ematipico changed the title Feat/improve undeclared classes feat: improve noUndeclaredClasses Mar 25, 2026
@ematipico ematipico marked this pull request as ready for review March 25, 2026 15:15
@ematipico ematipico requested review from a team March 25, 2026 15:16
@github-actions github-actions Bot added the L-JSON Language: JSON and super languages label Mar 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

Walkthrough

This PR refactors the noUndeclaredClasses rule implementation to expand class-name extraction from JSX attributes and function calls (such as clsx), introduces dynamic import tracking throughout the module graph, and standardises HTML test fixture comments. The changes modify the embedding kind system to distinguish class-attribute expressions, update HTML/CSS module info structures to capture both static and dynamic imports, and extend test coverage for undeclared classes across JSX, HTML, Astro, Vue, and Svelte files.

Possibly related PRs

Suggested labels

L-CSS

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main objective of the changeset: improving the noUndeclaredClasses rule with enhanced tracking and parsing capabilities.
Description check ✅ Passed The description clearly outlines the PR's improvements to noUndeclaredClasses rule: dynamic imports tracking, Astro class:list bindings, and JSX bindings. It also discloses AI assistance usage.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/improve-undeclared-classes

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

Copy link
Copy Markdown
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_module_graph/src/module_graph.rs (1)

515-524: ⚠️ Potential issue | 🟡 Minor

Missing dynamic_import_paths in build_import_tree_for_html CSS imports collection.

Other locations (e.g., lines 391-401, 646-650) consistently chain dynamic_import_paths.values() alongside static_import_paths.values() when collecting CSS imports for HTML modules. This location only chains static_import_paths, which appears to be an oversight.

🔧 Proposed fix
         let css_imports: Vec<_> = html_info
             .imported_stylesheets
             .iter()
             .chain(html_info.static_import_paths.values())
+            .chain(html_info.dynamic_import_paths.values())
             .filter_map(|stylesheet_path| {
                 let path = stylesheet_path.as_path()?;
                 self.css_module_info_for_path(path)?;
                 Some(path.to_path_buf())
             })
             .collect();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/module_graph.rs` around lines 515 - 524, In
build_import_tree_for_html, the css_imports collection only chains
html_info.static_import_paths.values() and omits
html_info.dynamic_import_paths.values(); update the iterator chain that builds
css_imports (the html_info.imported_stylesheets.iter().chain(...)) to also chain
html_info.dynamic_import_paths.values(), preserving the existing filter_map call
to call self.css_module_info_for_path(path)? and collect the PathBufs so dynamic
stylesheet imports are included alongside static ones.
🧹 Nitpick comments (6)
crates/biome_html_analyze/benches/html_analyzer.rs (1)

8-8: Minor style inconsistency between the two analyze calls.

The new import at line 8 is used at line 161 (HtmlAnalyzerServices::default()), but the first call at line 117 still uses the fully qualified path biome_html_analyze::HtmlAnalyzerServices::default(). Consider picking one style for consistency.

♻️ Optional: use import consistently

Either remove the import and use the fully qualified path at line 161:

-                    HtmlAnalyzerServices::default(),
+                    biome_html_analyze::HtmlAnalyzerServices::default(),

Or update line 117 to use the import (and remove biome_html_analyze:: prefix there).

Also applies to: 161-161

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

In `@crates/biome_html_analyze/benches/html_analyzer.rs` at line 8, The two calls
to create the analyzer are inconsistent: one uses the fully-qualified
biome_html_analyze::HtmlAnalyzerServices::default() and the other uses the
imported HtmlAnalyzerServices::default(); pick one style and make them
consistent by either removing the use import and using the fully-qualified path
in both places or by changing the fully-qualified call to use the imported
HtmlAnalyzerServices::default() so both analyze invocations use the same symbol
form.
crates/biome_module_graph/src/html_module_info/mod.rs (1)

116-124: Minor doc clarity: consider updating the static imports comment.

The comment at lines 119-120 states dynamic imports "are ignored for upward-traversal", which remains true. However, now that dynamic_import_paths is explicitly tracked (line 123-124), it might be worth adding a brief note explaining the distinction—static imports drive upward traversal, whilst dynamic imports are tracked separately for CSS class resolution.

🤖 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 116 -
124, Update the doc comment for the struct fields to clarify the distinction
between static and dynamic imports: modify the comment for static_import_paths
to state that static imports (tracked in static_import_paths) drive
upward-traversal, whereas dynamic imports are tracked separately in
dynamic_import_paths and do not participate in upward-traversal but are still
recorded (e.g., for CSS class resolution). Reference the fields
static_import_paths and dynamic_import_paths when adding the brief explanatory
sentence so readers can quickly see the intended behavior.
crates/biome_test_utils/src/lib.rs (1)

1084-1084: Consider impact of ModuleGraphResolutionKind::Modules on all workspace tests.

This change enables module graph resolution for all analyze_with_workspace calls, not just noUndeclaredClasses tests. While necessary for project-domain rules, it may increase test execution time for rules that don't require the module graph.

If test performance becomes a concern, consider parameterising the resolution kind or using a separate helper for module-graph-dependent tests.

🤖 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` at line 1084, The change unconditionally
sets ModuleGraphResolutionKind::Modules for analyze_with_workspace, affecting
all workspace tests and possibly slowing ones that don't need module graph
resolution; update the test helper to accept a ModuleGraphResolutionKind
parameter (defaulting to the previous, cheaper kind) or provide a new helper
(e.g., analyze_with_workspace_with_graph) so only tests like noUndeclaredClasses
pass ModuleGraphResolutionKind::Modules while others use the lightweight
variant; update calls to analyze_with_workspace or replace them with the new
helper where module-graph resolution is required.
crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs (3)

321-406: Code duplication between semantic and non-semantic extraction.

collect_class_names_from_expression_no_semantic (lines 321-406) and collect_class_names_from_expression (lines 439-591) share nearly identical logic for handling objects, arrays, and call expressions. Only the identifier handling differs.

You could consider extracting the common logic into a shared helper that accepts an optional closure for identifier resolution. That said, the current approach is clear and self-contained — just something to keep an eye on if more expression types are added later.

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

In `@crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs` around
lines 321 - 406, The two functions
collect_class_names_from_expression_no_semantic and
collect_class_names_from_expression duplicate handling for CallExpression,
ObjectExpression, ArrayExpression, and literals; extract the shared traversal
into a helper (e.g., traverse_expression_common) that takes the expression and
an optional callback/closure used only for identifier resolution differences,
then have both original functions call that helper supplying the appropriate
identifier-resolver closure (one that uses semantic model, one that returns
None/uses tokens), keeping unique identifier logic out of the common traversal.

119-144: Consider building the import tree outside the loop.

The import_tree is built for each undeclared class (lines 133-137), but the tree structure doesn't change between iterations — it depends only on file_path and is_html_like. Building it once before the loop and cloning for each UndeclaredClass would avoid redundant traversal.

♻️ Proposed optimisation
+        // Build import tree once for diagnostics (only if we have undeclared classes)
+        let mut import_tree_cache: Option<Option<ImportTreeNode>> = None;
+
         for entry in &class_entries {
             let found_class = css_steps.iter().any(|step| {
                 step.css_classes
                     .iter()
                     .any(|c| c.text() == entry.name.as_ref())
             });

             if !found_class {
-                let import_tree = if is_html_like {
-                    module_graph.build_import_tree_for_html(file_path)
-                } else {
-                    module_graph.build_import_tree(file_path)
-                };
+                let import_tree = import_tree_cache
+                    .get_or_insert_with(|| {
+                        if is_html_like {
+                            module_graph.build_import_tree_for_html(file_path)
+                        } else {
+                            module_graph.build_import_tree(file_path)
+                        }
+                    })
+                    .clone();
                 signals.push(UndeclaredClass {
                     range: entry.range,
                     name: entry.name.clone(),
                     import_tree,
                 });
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs` around
lines 119 - 144, The loop over class_entries repeatedly calls
module_graph.build_import_tree_for_html or build_import_tree for every
undeclared class even though import_tree depends only on file_path and
is_html_like; compute the import_tree once before iterating (use the same
condition used now with is_html_like) and then clone or reuse that import_tree
when constructing each UndeclaredClass pushed to signals, keeping the rest of
the logic (found_class check on css_steps and pushing UndeclaredClass with
range/name) unchanged.

263-316: run_without_semantic duplicates the core checking loop.

The CSS step collection and undeclared class iteration (lines 284-315) mirrors the logic in run() (lines 107-144). If this pattern grows, consider extracting a shared helper. For now, it's manageable.

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

In `@crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs` around
lines 263 - 316, run_without_semantic duplicates the CSS collection and
undeclared-class iteration logic present in run(); extract the shared logic into
a helper (e.g., a new function like collect_undeclared_classes_for_file or
validate_classes_against_css) that accepts the module_graph, file_path, and the
collected entries (Vec<Entry> from
collect_class_names_from_expression_no_semantic) and returns
Vec<UndeclaredClass>, then call that helper from both run_without_semantic and
run() replacing the duplicated block that uses
module_graph.traverse_import_tree_for_html_classes,
module_graph.build_import_tree_for_html, and the loop that builds signals;
ensure the helper references the same types (UndeclaredClass, entry.range,
entry.name) and preserves the early-return behavior when css_steps.is_empty().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 515-524: In build_import_tree_for_html, the css_imports collection
only chains html_info.static_import_paths.values() and omits
html_info.dynamic_import_paths.values(); update the iterator chain that builds
css_imports (the html_info.imported_stylesheets.iter().chain(...)) to also chain
html_info.dynamic_import_paths.values(), preserving the existing filter_map call
to call self.css_module_info_for_path(path)? and collect the PathBufs so dynamic
stylesheet imports are included alongside static ones.

---

Nitpick comments:
In `@crates/biome_html_analyze/benches/html_analyzer.rs`:
- Line 8: The two calls to create the analyzer are inconsistent: one uses the
fully-qualified biome_html_analyze::HtmlAnalyzerServices::default() and the
other uses the imported HtmlAnalyzerServices::default(); pick one style and make
them consistent by either removing the use import and using the fully-qualified
path in both places or by changing the fully-qualified call to use the imported
HtmlAnalyzerServices::default() so both analyze invocations use the same symbol
form.

In `@crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs`:
- Around line 321-406: The two functions
collect_class_names_from_expression_no_semantic and
collect_class_names_from_expression duplicate handling for CallExpression,
ObjectExpression, ArrayExpression, and literals; extract the shared traversal
into a helper (e.g., traverse_expression_common) that takes the expression and
an optional callback/closure used only for identifier resolution differences,
then have both original functions call that helper supplying the appropriate
identifier-resolver closure (one that uses semantic model, one that returns
None/uses tokens), keeping unique identifier logic out of the common traversal.
- Around line 119-144: The loop over class_entries repeatedly calls
module_graph.build_import_tree_for_html or build_import_tree for every
undeclared class even though import_tree depends only on file_path and
is_html_like; compute the import_tree once before iterating (use the same
condition used now with is_html_like) and then clone or reuse that import_tree
when constructing each UndeclaredClass pushed to signals, keeping the rest of
the logic (found_class check on css_steps and pushing UndeclaredClass with
range/name) unchanged.
- Around line 263-316: run_without_semantic duplicates the CSS collection and
undeclared-class iteration logic present in run(); extract the shared logic into
a helper (e.g., a new function like collect_undeclared_classes_for_file or
validate_classes_against_css) that accepts the module_graph, file_path, and the
collected entries (Vec<Entry> from
collect_class_names_from_expression_no_semantic) and returns
Vec<UndeclaredClass>, then call that helper from both run_without_semantic and
run() replacing the duplicated block that uses
module_graph.traverse_import_tree_for_html_classes,
module_graph.build_import_tree_for_html, and the loop that builds signals;
ensure the helper references the same types (UndeclaredClass, entry.range,
entry.name) and preserves the early-return behavior when css_steps.is_empty().

In `@crates/biome_module_graph/src/html_module_info/mod.rs`:
- Around line 116-124: Update the doc comment for the struct fields to clarify
the distinction between static and dynamic imports: modify the comment for
static_import_paths to state that static imports (tracked in
static_import_paths) drive upward-traversal, whereas dynamic imports are tracked
separately in dynamic_import_paths and do not participate in upward-traversal
but are still recorded (e.g., for CSS class resolution). Reference the fields
static_import_paths and dynamic_import_paths when adding the brief explanatory
sentence so readers can quickly see the intended behavior.

In `@crates/biome_test_utils/src/lib.rs`:
- Line 1084: The change unconditionally sets ModuleGraphResolutionKind::Modules
for analyze_with_workspace, affecting all workspace tests and possibly slowing
ones that don't need module graph resolution; update the test helper to accept a
ModuleGraphResolutionKind parameter (defaulting to the previous, cheaper kind)
or provide a new helper (e.g., analyze_with_workspace_with_graph) so only tests
like noUndeclaredClasses pass ModuleGraphResolutionKind::Modules while others
use the lightweight variant; update calls to analyze_with_workspace or replace
them with the new helper where module-graph resolution is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2feba897-f9c1-4664-b2d0-70939cc0a7e0

📥 Commits

Reviewing files that changed from the base of the PR and between a5c7dd9 and aa38dcf.

⛔ Files ignored due to path filters (49)
  • 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_silent_without_style_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalidHtmlAamRoleGeneric.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/astro/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/svelte/valid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/valid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/invalid.astro.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.html.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-css-import-astro/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/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.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_html_analyze/tests/specs/nursery/useVueValidVCloak/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVElseIf/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVElseIf/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/invalid.html.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/valid/valid.jsx.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 **
📒 Files selected for processing (42)
  • crates/biome_cli/tests/cases/html.rs
  • crates/biome_html_analyze/benches/html_analyzer.rs
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalidHtmlAamRoleGeneric.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/valid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.html
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVCloak/invalid.vue
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/invalid.html
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/valid.jsx
  • crates/biome_js_syntax/src/file_source.rs
  • crates/biome_json_analyze/tests/compat_sortpkg_tests.rs
  • crates/biome_module_graph/src/css_module_info/traverse.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/module_graph.rs
  • crates/biome_service/src/embed/types.rs
  • crates/biome_service/src/file_handlers/html.rs
  • crates/biome_test_utils/src/lib.rs

@ematipico ematipico force-pushed the feat/improve-undeclared-classes branch from aa38dcf to c895967 Compare March 29, 2026 18:32
Copy link
Copy Markdown
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/biome_module_graph/src/module_graph.rs (1)

515-524: ⚠️ Potential issue | 🟡 Minor

Missing dynamic_import_paths in build_import_tree_for_html.

This location only chains static_import_paths but omits dynamic_import_paths. Elsewhere in this file (e.g., lines 371-372, 395, 472-475), both are chained together. This inconsistency may cause dynamic CSS imports to be missing from the diagnostic import tree.

🛠️ Suggested fix
         let css_imports: Vec<_> = html_info
             .imported_stylesheets
             .iter()
             .chain(html_info.static_import_paths.values())
+            .chain(html_info.dynamic_import_paths.values())
             .filter_map(|stylesheet_path| {
                 let path = stylesheet_path.as_path()?;
                 self.css_module_info_for_path(path)?;
                 Some(path.to_path_buf())
             })
             .collect();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/module_graph.rs` around lines 515 - 524, In
build_import_tree_for_html, the css_imports construction only chains
html_info.static_import_paths and thus omits dynamic CSS imports; update the
iterator chain to include html_info.dynamic_import_paths (i.e., chain
html_info.imported_stylesheets.iter().chain(html_info.static_import_paths.values()).chain(html_info.dynamic_import_paths.values()))
so css_module_info_for_path is invoked for dynamic imports as well and dynamic
paths are collected into css_imports.
crates/biome_test_utils/src/lib.rs (1)

1101-1120: ⚠️ Potential issue | 🟠 Major

Duplicate index_files_for_test call.

Lines 1101-1107 already index all files (the primary test file plus sidecars via files_to_index). The second call at lines 1109-1120 indexes the exact same set of files again (sidecar_paths + virtual_file_path = files_to_index).

This redundant indexing may cause unexpected behaviour or simply waste cycles. One of these blocks should be removed.

🛠️ Suggested fix — remove the duplicate block
     workspace.index_files_for_test(
         project_key,
         files_to_index.into_iter().map(|path| {
             let document_file_source = DocumentFileSource::from_well_known(path.as_path(), true);
             (BiomePath::new(path), document_file_source)
         }),
     );

-    // Index all files through the internal indexing path so that:
-    // - Embedded CSS/JS blocks are parsed and stored
-    // - The module graph is populated with CSS classes and import edges
-    // This is required for project-domain rules like noUndeclaredClasses.
-    let mut all_virtual_files = sidecar_paths;
-    all_virtual_files.push(virtual_file_path.clone());
-    let files_with_sources = all_virtual_files.iter().map(|path| {
-        let biome_path = BiomePath::new(path.clone());
-        let doc_source = DocumentFileSource::from_well_known(path.as_path(), true);
-        (biome_path, doc_source)
-    });
-    workspace.index_files_for_test(project_key, files_with_sources);
🤖 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 1101 - 1120, Remove the
duplicate indexing call: the second block that builds all_virtual_files from
sidecar_paths plus virtual_file_path and then calls
workspace.index_files_for_test duplicates the earlier
workspace.index_files_for_test over files_to_index; delete the second block (the
all_virtual_files/files_with_sources construction and its
workspace.index_files_for_test call) so files are only indexed once, keeping the
original files_to_index-based call and symbols workspace.index_files_for_test,
files_to_index, sidecar_paths, and virtual_file_path intact.
🧹 Nitpick comments (1)
crates/biome_module_graph/src/html_module_info/mod.rs (1)

122-125: Consider expanding the docstring for dynamic_import_paths.

The docstring for static_import_paths (lines 116-121) explains the key/value semantics and notes that dynamic imports are excluded. This new field would benefit from similar detail — e.g., clarifying that these are import("…") expressions from embedded scripts.

🤖 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 122 -
125, The docstring for the new field dynamic_import_paths is too terse; expand
it to mirror the explanatory detail given for static_import_paths by clarifying
key/value semantics and what qualifies as a dynamic import (e.g., import("...")
expressions in embedded scripts), noting that these are resolved paths of
modules loaded via dynamic imports and how/when they are populated; update the
doc comment above the dynamic_import_paths field (and optionally adjust
static_import_paths for parity) to include that information and any differences
from static imports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 515-524: In build_import_tree_for_html, the css_imports
construction only chains html_info.static_import_paths and thus omits dynamic
CSS imports; update the iterator chain to include html_info.dynamic_import_paths
(i.e., chain
html_info.imported_stylesheets.iter().chain(html_info.static_import_paths.values()).chain(html_info.dynamic_import_paths.values()))
so css_module_info_for_path is invoked for dynamic imports as well and dynamic
paths are collected into css_imports.

In `@crates/biome_test_utils/src/lib.rs`:
- Around line 1101-1120: Remove the duplicate indexing call: the second block
that builds all_virtual_files from sidecar_paths plus virtual_file_path and then
calls workspace.index_files_for_test duplicates the earlier
workspace.index_files_for_test over files_to_index; delete the second block (the
all_virtual_files/files_with_sources construction and its
workspace.index_files_for_test call) so files are only indexed once, keeping the
original files_to_index-based call and symbols workspace.index_files_for_test,
files_to_index, sidecar_paths, and virtual_file_path intact.

---

Nitpick comments:
In `@crates/biome_module_graph/src/html_module_info/mod.rs`:
- Around line 122-125: The docstring for the new field dynamic_import_paths is
too terse; expand it to mirror the explanatory detail given for
static_import_paths by clarifying key/value semantics and what qualifies as a
dynamic import (e.g., import("...") expressions in embedded scripts), noting
that these are resolved paths of modules loaded via dynamic imports and how/when
they are populated; update the doc comment above the dynamic_import_paths field
(and optionally adjust static_import_paths for parity) to include that
information and any differences from static imports.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a7eb5ae-c49a-475d-8b88-e7c937975142

📥 Commits

Reviewing files that changed from the base of the PR and between aa38dcf and c895967.

⛔ Files ignored due to path filters (29)
  • 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_silent_without_style_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalidHtmlAamRoleGeneric.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/svelte/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/valid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/invalid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVCloak/invalid.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVElseIf/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/invalid.html.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/valid/valid.jsx.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 **
📒 Files selected for processing (37)
  • crates/biome_cli/tests/cases/html.rs
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalidHtmlAamRoleGeneric.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/styles.css
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.html
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVCloak/invalid.vue
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/invalid.html
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/valid.jsx
  • crates/biome_js_syntax/src/file_source.rs
  • crates/biome_module_graph/src/css_module_info/traverse.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/module_graph.rs
  • crates/biome_service/src/embed/types.rs
  • crates/biome_service/src/file_handlers/html.rs
  • crates/biome_test_utils/src/lib.rs
💤 Files with no reviewable changes (1)
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
✅ Files skipped from review due to trivial changes (24)
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/useVueValidVCloak/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalidHtmlAamRoleGeneric.html
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/styles.css
  • crates/biome_html_analyze/tests/specs/a11y/useKeyWithMouseEvents/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-no-style-info/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html
  • crates/biome_cli/tests/cases/html.rs
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/valid.astro
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/invalid-css-import-astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/useAnchorContent/vue/invalid.vue
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/biome_html_analyze/tests/specs/nursery/noUndeclaredClasses/valid-css-import-astro/styles.css
  • crates/biome_module_graph/src/css_module_info/traverse.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/valid/valid.jsx
  • crates/biome_module_graph/src/html_module_info/visitor.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUndeclaredClasses/invalid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs

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.

this snapshot looks orphaned

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I double checked, the source file is there

Comment on lines +22 to +25
/// This rule checks string literals, variable references (resolved through the semantic
/// model), call expressions like `clsx(...)` / `classnames(...)`, object expression keys,
/// and array expressions. Dynamic class names that cannot be statically resolved are
/// silently skipped.
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.

tailwind users are going to get a bunch of false positives from this rule, at least until we start handling tailwind configs. I would at least leave a note about that.

Copy link
Copy Markdown
Member Author

@ematipico ematipico Mar 29, 2026

Choose a reason for hiding this comment

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

Yeah definitely. It's on my to-do list

@ematipico ematipico merged commit 9ee64f8 into next Mar 30, 2026
17 checks passed
@ematipico ematipico deleted the feat/improve-undeclared-classes branch March 30, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Linter Area: linter A-Parser Area: parser A-Project Area: project L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants