Skip to content

fix(lint): noUnusedImports/noUnusedVariables now flag truly-unused script bindings in Svelte/Vue/Astro#10473

Open
Mokto wants to merge 20 commits into
biomejs:mainfrom
Mokto:fix/svelte-vue-astro-unused-self-suppression
Open

fix(lint): noUnusedImports/noUnusedVariables now flag truly-unused script bindings in Svelte/Vue/Astro#10473
Mokto wants to merge 20 commits into
biomejs:mainfrom
Mokto:fix/svelte-vue-astro-unused-self-suppression

Conversation

@Mokto

@Mokto Mokto commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #10103.

With html.experimentalFullSupportEnabled enabled, supports: noUnusedImports / noUnusedVariables / useImportType

AI assistance disclosure

Implemented with Claude Code, with manual review and direction at each step (diagnosis, the is_embedded_source gate, enumerating + fixing the extraction gap classes against a real codebase, and the tests).

…ript bindings in Svelte/Vue/Astro

The two rules consult `EmbeddedExportedBindings` to avoid false positives on
imports/vars referenced only in templates. But under
`html.experimentalFullSupportEnabled`, the source `<script>` block's own
top-level bindings get registered into that set via `visit_js_source_snippet`,
so the membership check at no_unused_variables.rs:357 and no_unused_imports.rs:686
self-suppresses every import and let/const in the script — including truly unused
ones (biomejs#8590).

Gate the embedded-binding suppression on `!file_source.is_embedded_source()`:
for `{@const}` / Vue v-for / Svelte snippet params we keep the existing
behavior, but for the `<script>` block itself rely on `EmbeddedValueReferences`
(precise per-name template references) instead. `noUnusedImports` now consults
that service too, mirroring `useImportType`/`noUnusedVariables`.

Also extracts `HtmlSpreadAttribute` arguments as non-source snippets so
`<input {...props} />` properly registers `props` as a template reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented May 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5052fca

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

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

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

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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Organic activity

No automation signals detected in the analyzed events.

View full analysis →

This is an automated analysis by AgentScan

@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels May 26, 2026
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

This PR completes cross-language support for no-unused rules by threading embedded-template references through the analysis pipeline. HTML parsing now captures Svelte spread attributes, quoted interpolations (e.g. style="top: {top}px"), and directive names (use:, transition:, etc.); EmbeddedValueReferences is refactored to separate type-only identifiers from value references; and the lint rules consult these references to treat template-used bindings as in-use, whilst preventing self-suppression for embedded <script> source contexts.

Possibly related PRs

  • biomejs/biome#9878: Fixes noUnusedImports default specifier false positives in Svelte/Astro/Vue by tracking import patterns alongside embedded references.
  • biomejs/biome#9886: Refactors embedded reference tracking storage and lookup to support the dual value/type-reference distinction that this PR builds upon.

Suggested labels

A-Parser, A-Diagnostic

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarises the main change: fixing cross-language lint rules (noUnusedImports/noUnusedVariables) to correctly handle truly-unused bindings in Svelte/Vue/Astro templates.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #8590 by implementing cross-language support for noUnusedImports, noUnusedVariables, and useImportType rules via EmbeddedValueReferences and improved template binding extraction.
Out of Scope Changes check ✅ Passed All changes are scoped to supporting cross-language lint rules: embedded reference tracking, template interpolation/directive extraction, and semantic wiring for the target rules.
Description check ✅ Passed The PR description clearly relates to the changeset, describing fixes for noUnusedImports/noUnusedVariables rules for Svelte/Vue/Astro templating.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/tasty-kiwis-roll.md:
- Line 5: The changeset sentence ends with a colon after "...unusedLet" but per
guidelines it must end with a full stop; edit the sentence in
.changeset/tasty-kiwis-roll.md to replace the trailing ":" with "." (keep the
following example paragraph/block as-is) so the sentence ending containing
"unusedLet" is terminated with a period.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 94636377-0a75-44dd-9eec-3bd95262d38a

📥 Commits

Reviewing files that changed from the base of the PR and between 1948b72 and 4898586.

⛔ Files ignored due to path filters (16)
  • crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/full_support.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/return_in_template_expression_should_error.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/full_support.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/full_support_ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/full_support.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/full_support_jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/full_support_ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/full_support_tsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/lint_vue_should_not_add_extra_newlines_in_embedded_snippet.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/unused_suppression_has_correct_span_in_vue_file.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/no_unused_imports_flags_truly_unused_script_bindings.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/no_unused_imports_or_variables_for_template_references.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/spread_attribute_counts_as_template_reference.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/use_import_type_fires_on_type_only_value_import.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/use_import_type_quiet_for_template_referenced_imports.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedVariables/valid-vue-embedded-bindings.vue.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (8)
  • .changeset/tasty-kiwis-roll.md
  • crates/biome_cli/tests/cases/handle_vue_files.rs
  • crates/biome_cli/tests/cases/mod.rs
  • crates/biome_cli/tests/cases/svelte_cross_language_rules.rs
  • crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs
  • crates/biome_js_analyze/src/lint/correctness/no_unused_variables.rs
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedVariables/valid-vue-embedded-bindings.vue
  • crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs

Comment thread .changeset/tasty-kiwis-roll.md Outdated
@Mokto Mokto marked this pull request as draft May 26, 2026 20:09
Mokto and others added 2 commits May 27, 2026 05:55
…ation, directives, bind shorthand, snippet params

Removing the source-script over-suppression (previous commit) exposed several
Svelte template constructs whose references the extractor never collected, each
becoming a false positive on noUnusedImports/noUnusedVariables:

- `{expr}` inside quoted attribute values (`style="top: {top}px"`): the parser
  stores these as one opaque string token, so a brace/string-aware scanner
  (`svelte_interpolations`) pulls the expressions out and parses them as JS.
- Directive names `use:action` / `transition:fn` / `in:` / `out:` / `animate:`
  reference imported values; the name token is now registered.
- Shorthand `bind:open` (no initializer) reads the local `open`.
- `{#snippet foo(param)}` params used only in the snippet body — noUnused
  FunctionParameters now consults EmbeddedValueReferences like the other rules.

Verified against a real SvelteKit codebase: 70 false positives → 0, while
genuinely-unused script bindings are still reported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mokto Mokto marked this pull request as ready for review May 27, 2026 07:11

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/tasty-kiwis-roll.md:
- Line 17: Change the sentence that currently ends with "removed:" to end with a
full stop (replace the trailing colon with a period) in the .changeset entry
describing noUnusedImports/noUnusedFunctionParameters and
EmbeddedValueReferences; leave the following bullet list unchanged and ensure
the sentence follows the project guideline that every sentence in a changeset
ends with a full stop.

In
`@crates/biome_js_analyze/src/lint/correctness/no_unused_function_parameters.rs`:
- Around line 145-152: The current early return uses only the identifier text
via refs.is_used_as_value(name), which can incorrectly suppress unused-parameter
diagnostics when an unrelated template binding shares the same name; instead
resolve the parameter's actual symbol/scope before checking
EmbeddedValueReferences: obtain the parameter's symbol (the binding or symbol id
for the identifier `name` from the semantic model or ctx), then ask
EmbeddedValueReferences whether that specific symbol (or symbol id/scope) is
used in templates (e.g., add or use a method like
is_used_as_value_for_symbol(symbol_id) or filter refs by scope) and only return
None when the symbol-matched template usage is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 27afbe83-7867-4020-934e-8a163be16416

📥 Commits

Reviewing files that changed from the base of the PR and between 4898586 and cf37c86.

⛔ Files ignored due to path filters (4)
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/attribute_string_interpolation_counts_as_reference.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/bind_shorthand_counts_as_reference.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/directive_names_count_as_references.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_svelte_cross_language_rules/snippet_parameter_used_in_body_is_not_unused.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • .changeset/tasty-kiwis-roll.md
  • crates/biome_cli/tests/cases/svelte_cross_language_rules.rs
  • crates/biome_js_analyze/src/lint/correctness/no_unused_function_parameters.rs
  • crates/biome_service/src/workspace/document/services/embedded_value_references.rs
  • crates/biome_service/src/workspace/server.rs

Comment thread .changeset/tasty-kiwis-roll.md Outdated
Comment on lines +145 to +152
// A Svelte `{#snippet foo(param)}` parameter can be referenced only in
// the snippet's template body (a separate embedded snippet the JS
// semantic model can't see). Treat template references as a use.
if let Some(refs) = ctx.get_service::<EmbeddedValueReferences>()
&& refs.is_used_as_value(name)
{
return None;
}

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 | 🟠 Major | 🏗️ Heavy lift

Name-only template matching can hide genuinely unused parameters.

At Line 148, this suppression is keyed only by identifier text, not symbol identity/scope. That can silence a real unused parameter when an unrelated template binding happens to share the same name. Please narrow this to actual snippet-parameter symbols (or otherwise scope-aware matching) before returning early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@crates/biome_js_analyze/src/lint/correctness/no_unused_function_parameters.rs`
around lines 145 - 152, The current early return uses only the identifier text
via refs.is_used_as_value(name), which can incorrectly suppress unused-parameter
diagnostics when an unrelated template binding shares the same name; instead
resolve the parameter's actual symbol/scope before checking
EmbeddedValueReferences: obtain the parameter's symbol (the binding or symbol id
for the identifier `name` from the semantic model or ctx), then ask
EmbeddedValueReferences whether that specific symbol (or symbol id/scope) is
used in templates (e.g., add or use a method like
is_used_as_value_for_symbol(symbol_id) or filter refs by scope) and only return
None when the symbol-matched template usage is found.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a deliberate property of the whole EmbeddedValueReferences mechanism rather than something specific to this rule. Template expressions, {#snippet} headers, directives, etc. are parsed as separate embedded snippets, so there is no shared semantic model — and therefore no symbol identity — spanning the script block and the template. References are necessarily collected as raw token text, and the same name-based check already backs noUnusedImports, noUnusedVariables, and useImportType. Adding this rule to that set keeps it consistent.

The failure mode of a name collision is a false negative (a genuinely-unused parameter goes unreported because an unrelated template binding shares its name), not a false positive. For an unused-* rule that direction is the safe one — it never flags/auto-fixes something that is actually used. The previous behaviour here was strictly worse: it produced false positives on real code (e.g. {#snippet card(label)} using {label} in its body).

True scope-aware matching would require a unified semantic model across embedded-snippet boundaries, which doesn't exist today; that's a larger architectural effort tracked under the broader cross-language work (#8590) rather than this fix. Leaving as-is for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Mokto Given that, it's best to add this note as a comment. It's knowledge base that must be kept around. Avoid LLM-generated comments, write a comment yourself. These LLM generated comments are verbose, and use useless technical words.

Mokto and others added 2 commits May 27, 2026 09:58
…port rules

An import used only as a type inside a template snippet — e.g. `icon: IconType`
in a `{#snippet}` parameter type — was flagged as unused. `EmbeddedValueReferences`
dropped type-only references (so `useImportType` could distinguish value vs type
usage), leaving them invisible to noUnusedImports/noUnusedVariables.

Track type-only references in a parallel channel. `useImportType` keeps using the
value-only set (unchanged); the unused-* rules now consult `is_used` (value OR
type). A genuinely-unused type import is still reported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@ematipico ematipico left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you. The architecture of the Svelte interpolation is incorrect. In fact, I fail to see a case where we have e.g. {top}px where top is undefined.

Also remove all those CLI tests. Our analyzer infra already supports those languages, so the cases can be moved there.

/// Identifiers referenced only as types (e.g. `icon: IconType` in a
/// `{#snippet}` parameter type). Tracked separately so `useImportType`
/// can keep distinguishing value vs type usage, while the unused-* rules
/// can treat either as a use.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's make comments generics and properly targeted. We don't care about who uses the list, we're interested to its business logic.

Also, how are "types" deemed real types? In typescript, some constructs create both a value and a type e.g. class Foo {}

/// constructs the parser leaves opaque or unattached: directive names
/// (`use:action`) and `{expr}` interpolations embedded inside quoted
/// attribute values (`style="top: {top}px"`).
pub(crate) fn visit_html_root(&mut self, root: &HtmlRoot, is_svelte: bool) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Passing is_svelte is short-sighted. It's best to send the HtmlFileSource

Comment on lines +83 to +86
/// When `is_svelte` is set, also extracts references from Svelte-only
/// constructs the parser leaves opaque or unattached: directive names
/// (`use:action`) and `{expr}` interpolations embedded inside quoted
/// attribute values (`style="top: {top}px"`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

constructs the parser leaves opaque or unattached

What does that mean? Please use language targeted to human developers, be specific and use simple language.

// we register the directive name itself (`action` / `fn`). `bind:`,
// `class:`, and `style:` are excluded — their property is a DOM/CSS
// name, not a JS reference.
if let Some(directive) = SvelteUseDirective::cast_ref(&node) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Create a new function to visit svelte directives. Use AnySvelteDirective to catch all the nodes. Also, use match instead of if/else. Using a match will make the compiler fail in case we add new nodes.

Comment on lines +130 to +133
// Shorthand `bind:open` (no `={...}`) binds the local variable
// named by the property. With an explicit initializer
// (`bind:value={x}`) the property is a DOM/component name and
// the reference `x` is extracted from the initializer snippet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is superfluous, or it should be moved as docstring of the function register_svelte_binding_property

/// Identifiers used as values.
value: Vec<Vec<(TextRange, TokenText)>>,
/// Identifiers used only as types (e.g. `icon: IconType`).
type_only: Vec<Vec<(TextRange, TokenText)>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: ty_list or types

Comment on lines +27 to +29
/// Check if an identifier is referenced at all (value or type) in any
/// tracked non-source snippet. The unused-* rules use this: a type-only
/// usage in a template (`icon: IconType`) still counts as a use.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/// Check if an identifier is referenced at all (value or type) in any
/// tracked non-source snippet. The unused-* rules use this: a type-only
/// usage in a template (`icon: IconType`) still counts as a use.
/// Check if an identifier is referenced as value or type in any
/// tracked non-source snippet.

/// tracked non-source snippet. The unused-* rules use this: a type-only
/// usage in a template (`icon: IconType`) still counts as a use.
pub(crate) fn is_used(&self, identifier: &str) -> bool {
Self::contains(&self.value, identifier) || Self::contains(&self.type_only, identifier)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I think it's best if we don't use the contains function, and just use simple iterators.

self.value.iter().chain(self.type_only).any()

Comment on lines +145 to +152
// A Svelte `{#snippet foo(param)}` parameter can be referenced only in
// the snippet's template body (a separate embedded snippet the JS
// semantic model can't see). Treat template references as a use.
if let Some(refs) = ctx.get_service::<EmbeddedValueReferences>()
&& refs.is_used_as_value(name)
{
return None;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Mokto Given that, it's best to add this note as a comment. It's knowledge base that must be kept around. Avoid LLM-generated comments, write a comment yourself. These LLM generated comments are verbose, and use useless technical words.

Comment thread .changeset/tasty-kiwis-roll.md Outdated
Comment on lines +5 to +23
Fixed [#8590](https://github.com/biomejs/biome/issues/8590): [`noUnusedImports`](https://biomejs.dev/linter/rules/no-unused-imports/) and [`noUnusedVariables`](https://biomejs.dev/linter/rules/no-unused-variables/) now correctly flag truly-unused imports and `let`/`const` declarations inside Svelte, Vue, and Astro `<script>` blocks (when `html.experimentalFullSupportEnabled` is on). Previously, the script's own top-level bindings were registered into the embedded-binding set, which self-suppressed every diagnostic; precise template-reference tracking now drives the suppression instead. The following Svelte component now correctly reports `unusedImport` and `unusedLet`.

```svelte
<script lang="ts">
import { unusedImport } from "./other";
let unusedLet = 42;
import Button from "./Button.svelte";
let name = "alice";
</script>
<Button>{name}</Button>
```

[`noUnusedImports`](https://biomejs.dev/linter/rules/no-unused-imports/) and [`noUnusedFunctionParameters`](https://biomejs.dev/linter/rules/no-unused-function-parameters/) now consult `EmbeddedValueReferences` too, so bindings used only in the template stay quiet. Template-reference extraction was extended to cover the Svelte constructs the parser leaves unattached or opaque, all of which previously produced false positives once the over-suppression was removed.

- Spread attributes — `<input {...props} />` (Svelte and Astro).
- `{expr}` interpolations inside quoted attribute values — `style="top: {top}px"`, `class="card {cls}"`.
- Directive names — `use:action`, `transition:fn`, `in:fn`, `out:fn`, `animate:fn`.
- Shorthand `bind:` — `<Dialog bind:open />` references the local `open`.
- `{#snippet foo(param)}` parameters referenced only in the snippet body.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This changeset can be shorted to something like "improved rules ... for Svelte, Astro and Vue".

Remove the technicalities. This is covered in the contribution guidelines.

@ematipico ematipico left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I meant to block the PR

Mokto and others added 4 commits May 27, 2026 14:13
…h on directives, move tests

- Move attribute-string `{...}` interpolation extraction into parse_embedded_nodes
  so each is a tracked snippet, instead of re-parsing and discarding it in the
  references visitor.
- Replace the directive if/else chain with an exhaustive match on AnySvelteDirective.
- visit_html_root takes &HtmlFileSource instead of an is_svelte bool.
- Rename EmbeddedValueReferences fields to values/types, drop the contains helper.
- Move the Svelte CLI cases to analyzer spec fixtures.
- Trim comments and the changeset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…UnusedVariables

Verifies that genuinely-unused script bindings are still flagged after
the embedded-reference tracking fix; previously only valid (clean) cases
existed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ro templates

When an imported name appears only in type position inside a template
snippet (e.g. `{#snippet card(p: { icon: IconType })}`), the semantic
model of the <script> block has no references to it, so is_only_used_as_type
returned false and the rule stayed silent. Now it falls back to checking
the embedded type_references channel, correctly suggesting `import type`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the case where `import type { T }` is never referenced in either
the script or the template — it should still be reported by noUnusedImports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mokto

Mokto commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@ematipico thank you for the quick feedback! I can see my first PR was pretty rough.

I think it's ready for another round of review now

@ematipico ematipico left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Much better now, but we still don't have error tests for svelte interpolations e.g. {foo}px where foo isn't defined anywhere

Comment thread crates/biome_service/src/workspace/document/services/embedded_value_references.rs Outdated
Mokto and others added 2 commits May 28, 2026 11:19
…value_references.rs

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
…olation test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mokto

Mokto commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Thank you! Happy to hear it's better. I've pushed the latest fix

let unused = 0;
</script>

<div style="top: {foo}px"></div>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's not what I asked. foo should be flagged by noUndeclaredVariables

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah sorry, I misunderstood. i've just added it

…e interpolation

Verifies that undeclared variables used in attribute interpolations like
{foo}px are flagged by noUndeclaredVariables.

@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.

There's a lot of changes individual bug fixes here so its a bit hard to review. Trying my best tho.

I wouldn't say its enough to close #8590 though. Although, it could be enough to close #10103.

Comment on lines +361 to +372
// Interpolations inside a quoted attribute value:
// `style="top: {top}px"`, `class="card {cls}"`.
if let Some(string) = HtmlString::cast_ref(&element) {
for candidate in build_svelte_string_interpolation_candidates(&string) {
ctx.parse_and_push(
&candidate,
&doc_file_source,
Some(embedded_file_source),
&mut nodes,
);
}
}

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 is a hack for a change that should go in the parser. We would want to emit a new node like SvelteInterpolatedString or something like that so we could just extract the expressions without needing to do string parsing this high in the architecture.

Maybe leave this fix out of this PR to not bloat it further.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

so do you want me to do something about it afterwards, or in a PR to the current PR ?

Or will you create an issue for later

@ematipico ematipico May 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Mokto, revert the changes regarding the interpolation, and then send a new PR that probably takes care of the parsing of the interpolated strings in the HTML parser.

You're free to create an issue, but not mandatory.

Comment on lines +412 to +424
/// Returns the range of each `{...}` interpolation's inner expression within
/// `input`. Brace counting is aware of JS string and template literals, so a
/// `}` inside a string (`{ok ? 'a}b' : c}`) or a nested object (`{ {x: 1} }`)
/// does not end the group early.
fn svelte_interpolation_ranges(input: &str) -> Vec<TextRange> {
let bytes = input.as_bytes();
let mut ranges = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'{' {
i += 1;
continue;
}

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.

Per my previous comment, this is the kind of string parsing that we want to defer to the actual parser. Otherwise we're effectively double-processing the same text.

Comment on lines +72 to +77
/// Tracks identifiers referenced from the template that the JS semantic
/// model of the `<script>` block can't see: component names (`<Button />`)
/// and, for Svelte, directive names (`use:action`). Interpolations are
/// parsed as snippets earlier and reach us via [`Self::visit_non_source_snippet`].
pub(crate) fn visit_html_root(&mut self, root: &HtmlRoot, file_source: &HtmlFileSource) {
let is_svelte = file_source.is_svelte();

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 is a worse comment than it was previously. Too verbose, not easily glancible, and is missing information that the previous version had.

Comment on lines +93 to +105
/// Registers the directive name when it resolves to an imported binding.
/// `use`/`transition`/`in`/`out`/`animate` name a function; shorthand
/// `bind:open` reads the local `open`. `style:`/`class:` name a CSS
/// property, not a binding.
fn register_svelte_directive_reference(
&mut self,
directive: &AnySvelteDirective,
) -> Option<()> {
let value = match directive {
AnySvelteDirective::SvelteUseDirective(d) => d.value().ok()?,
AnySvelteDirective::SvelteTransitionDirective(d) => d.value().ok()?,
AnySvelteDirective::SvelteInDirective(d) => d.value().ok()?,
AnySvelteDirective::SvelteOutDirective(d) => d.value().ok()?,

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.

I couldn't find any tests that exercise this.

Comment on lines +120 to +128
fn register_svelte_binding_property(
&mut self,
property: Option<AnySvelteBindingProperty>,
) -> Option<()> {
let token = match property? {
AnySvelteBindingProperty::SvelteName(name) => name.ident_token().ok()?,
AnySvelteBindingProperty::SvelteMemberProperty(_)
| AnySvelteBindingProperty::SvelteLiteral(_) => return None,
};

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.

Same here, couldn't find tests.

self.register_svelte_binding_property(value.property().ok())
}

fn register_svelte_binding_property(

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.

add a comment explaining what syntax this touches. code samples are a good way to do that.

@codspeed-hq

codspeed-hq Bot commented May 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 125 untouched benchmarks
⏩ 129 skipped benchmarks1


Comparing Mokto:fix/svelte-vue-astro-unused-self-suppression (5052fca) with main (263c7cc)

Open in CodSpeed

Footnotes

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

Mokto added 3 commits May 28, 2026 20:07
…nt, add bind: tests

- Rename EmbeddedValueReferences::references to value_references (pairs
  cleaner with type_references)
- Restore the detailed doc comment on visit_html_root with examples
- Add unit tests for bind: shorthand (registers reference) and bind:
  with initializer (does not register directive name as reference)
@ematipico

Copy link
Copy Markdown
Member

@Mokto thank you. Can you fix the CI? Also, if comments have been addressed, re-request a review from the right side of the webpage. This will send a notification to the reviewers, notify "this is ready for a new review"

@Mokto

Mokto commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

I'm waiting on #10504 to be merged per #10473 (comment)

Mokto added 2 commits June 11, 2026 04:49
…0555

Replace the embedded-node string-parsing hack with consumption of the
HtmlAttributeSingleTextExpression nodes that biomejs#10555 now emits inside
SvelteTemplateAttributeValue for interpolated quoted attribute values
(`style="top: {top}px"`).

- Drop build_svelte_string_interpolation_candidates / svelte_interpolation_ranges
  and the HtmlString branch in the Svelte pass. The interpolation nodes are
  captured by the existing Pass-4 HtmlAttributeSingleTextExpression branch,
  whose parent guard already admits them.
- Remove the now-unused HtmlString / TextRange imports and the hack's unit test.
- visit_html_root doc comment: interpolations flow through the snippet path,
  not this function.
- Collapse a nested if in use_import_type into a let-chain (clippy).
Mokto and others added 2 commits June 11, 2026 10:51
…s unused

The `parse_embedded_nodes` function was silently skipping `SvelteHtmlBlock`
nodes (`{@html expr}`), so the JS expression inside them was never added to
the embedded snippet list. As a result, any variable referenced only via
`{@html variable}` was incorrectly reported as unused by `noUnusedVariables`.

Now `SvelteHtmlBlock` is handled the same way as other expression-bearing
blocks (`SvelteIfBlock`, `SvelteKeyBlock`, etc.): the expression is parsed as
a JS snippet and visited so its identifier references are tracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Svelte files, <pre> was being parsed the same way as <script>/<style>:
its entire content was consumed as a single raw HTML_LITERAL token inside
HtmlEmbeddedContent.  That prevented the expression-tracking passes in
parse_embedded_nodes from seeing any Svelte blocks inside <pre>, so
variables referenced only via <pre>{@html expr}</pre> or <pre>{expr}</pre>
were still incorrectly reported as unused by noUnusedVariables.

The formatter already has an independent HTML_VERBATIM_TAGS list that
includes "pre", which causes it to emit <pre> content verbatim regardless
of how the parser represents the children.  Skipping the embedded-language
path in the parser for Svelte is therefore safe: formatting is unchanged,
and {@html} / interpolation nodes inside <pre> are now visible as proper
AST descendants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added A-Parser Area: parser L-HTML Language: HTML and super languages labels Jun 11, 2026

@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.

This is a great improvement! Just make sure that there are enough snapshot tests for the lint rules to cover all these cases. We plan to eventually replace the EmbeddedBindings/References services with framework specific semantic models for more precise analysis, and we want to make sure there are enough tests in place to prevent regressions.

…snapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants