Skip to content

fix(core): tracking vue bindings inside directives#9053

Merged
ematipico merged 3 commits intonextfrom
fix/vue-binding-track
Feb 14, 2026
Merged

fix(core): tracking vue bindings inside directives#9053
ematipico merged 3 commits intonextfrom
fix/vue-binding-track

Conversation

@ematipico
Copy link
Copy Markdown
Member

@ematipico ematipico commented Feb 13, 2026

Summary

Closes #9013

Code generated with an agent. While steering the agent, I noticed some things that got wrong, so I took the opportunity to create a skill which should provide the correct behaviour inside the project. For example, the agent was trying to build the binary, while it could use the quick_test to read a CST. Hopefully it should help.

Test Plan

CI should stay green. Added new tests.

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 13, 2026

⚠️ No Changeset found

Latest commit: fc19fca

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 A-LSP Area: language server protocol L-JavaScript Language: JavaScript and super languages L-HTML Language: HTML and super languages labels Feb 13, 2026
@ematipico ematipico force-pushed the fix/vue-binding-track branch from f89c275 to 7baafa0 Compare February 13, 2026 18:22
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 13, 2026

Merging this PR will degrade performance by 7.46%

❌ 2 regressed benchmarks
✅ 1 untouched benchmark
🆕 1 new benchmark
⏩ 152 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
index_1033418810622582172.html[cached] 102 µs 109.6 µs -6.93%
index_1033418810622582172.html[uncached] 118.2 µs 127.7 µs -7.46%
🆕 html_analyzer[index_1033418810622582172.html] N/A 419.4 µs N/A

Comparing fix/vue-binding-track (fc19fca) with next (c047e86)

Open in CodSpeed

Footnotes

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

@ematipico ematipico force-pushed the fix/vue-binding-track branch from 7baafa0 to 2278431 Compare February 13, 2026 22:53
@ematipico ematipico marked this pull request as ready for review February 13, 2026 22:54
@github-actions github-actions Bot removed the A-LSP Area: language server protocol label Feb 13, 2026
@dyc3 dyc3 self-requested a review February 13, 2026 23:03
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 13, 2026

Walkthrough

This PR introduces support for parsing Vue and Svelte directive attributes as embedded JavaScript code. Changes include: adding a new AnySvelteDirective::initializer() method to extract directive initializer values; updating the HTML file handler to iterate through Vue and Svelte directives and parse their expressions as embedded JavaScript snippets; adding test cases for noUnusedVariables detection in Vue and Svelte directives; and creating documentation skills for Biome development practices and testing patterns.

Possibly related PRs

Suggested reviewers

  • dyc3
  • arendjr
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(core): tracking vue bindings inside directives' directly reflects the main change: resolving Vue variable binding tracking in directives to fix false positives.
Description check ✅ Passed The description explains the motivation (fixing issue #9013), mentions agent-assisted code generation with author corrections, introduces new skills, and confirms test coverage and CI expectations.
Linked Issues check ✅ Passed The PR addresses issue #9013 by implementing Vue and Svelte directive attribute handling in embedded parsing, enabling expressions in v-on and other directives to mark variables as used, which resolves the false positive issue.
Out of Scope Changes check ✅ Passed All changes are aligned with fixing Vue binding tracking in directives: skill documentation, directive parsing logic, HTML syntax extensions, and corresponding test cases. The author explicitly scoped out v-for and v-slot special handling for later work.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into next

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/vue-binding-track

No actionable comments were generated in the recent review. 🎉


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.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.claude/skills/biome-developer/SKILL.md:
- Around line 244-247: The word "Markdown" in the guidance section is
lowercased; update the content in SKILL.md where the bullet items reference
"markdown" (e.g., the lines "- ✅ Ensure all markdown tables follow..." and "- ✅
Test documentation changes with markdown linters before committing") to
capitalize it as "Markdown" so the proper noun is used consistently across the
document.

In `@crates/biome_service/src/file_handlers/html.rs`:
- Around line 1116-1186: The rustdoc comments above the private helpers
parse_directive_string_value and parse_directive_text_expression are using ///
(doctest-style) but do not contain doctest code blocks; change those doc
comments to regular line comments (//) to comply with the repo guideline for
private helpers, i.e., replace the leading /// comment blocks describing
behavior for both functions with non-doctest // comments (preserving the
explanatory text), and ensure no public API is accidentally documented as
doctests; touch the comment blocks immediately preceding the
parse_directive_string_value and parse_directive_text_expression function
definitions.
- Around line 766-785: The loop over html_root.syntax().descendants() currently
only processes directives when directive.initializer() is Some(), skipping
Svelte shorthand directives (e.g., bind:value, class:active) that lack an
initializer; update the handling in the block that uses
AnySvelteDirective::cast_ref and directive.initializer() so that when
initializer() is None you still record the shorthand as a used identifier:
derive a suitable expression/content from the directive itself (for example
using the directive name/target) and push a corresponding (content.into(),
doc_source) into nodes (reusing embedded_file_source.with_embedding_kind and
parse_directive_text_expression for cases with an initializer), so shorthand
directives are treated as usages instead of being ignored.
- Around line 619-699: The directive parsing treats all Vue directives as JS
expressions, which breaks v-for and v-slot because they contain binding
patterns; update the handler to special-case VueVForDirective and
VueVSlotDirective (and VueVSlotShorthandDirective) instead of sending them to
parse_directive_string_value: detect these directives where initializer()
exists, and either (A) call a new helper (e.g., parse_directive_binding_pattern)
that parses binding patterns and returns identifier bindings to push into
nodes/embedded-bindings, or (B) skip adding them as expression embeddings so the
embedded-bindings layer won’t emit false unused-variable diagnostics; implement
the new branch alongside the existing
VueDirective/VueVOnShorthand/VueVBindShorthand handling and reuse
embedded_file_source.with_embedding_kind and nodes so only pure-expression
directives go to parse_directive_string_value.

Comment thread .claude/skills/biome-developer/SKILL.md
Comment on lines +619 to +699
// Parse Vue directive attributes (v-on, v-bind, v-if, etc.)
for element in html_root.syntax().descendants() {
// Handle @click shorthand (VueVOnShorthandDirective)
if let Some(directive) = VueVOnShorthandDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let file_source =
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
cache,
biome_path,
settings,
file_source,
) {
nodes.push((content.into(), doc_source));
}
}

// Handle :prop shorthand (VueVBindShorthandDirective)
if let Some(directive) = VueVBindShorthandDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let file_source =
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
cache,
biome_path,
settings,
file_source,
) {
nodes.push((content.into(), doc_source));
}
}

// Handle #slot shorthand (VueVSlotShorthandDirective)
if let Some(directive) = VueVSlotShorthandDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let file_source =
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
cache,
biome_path,
settings,
file_source,
) {
nodes.push((content.into(), doc_source));
}
}

// Handle full directives (v-on:, v-bind:, v-if, v-show, etc.)
if let Some(directive) = VueDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let file_source =
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
cache,
biome_path,
settings,
file_source,
) {
nodes.push((content.into(), doc_source));
}
}
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

🧩 Analysis chain

🏁 Script executed:

# Find the parse_directive_string_value function definition
rg "fn parse_directive_string_value" -A 30 crates/

Repository: biomejs/biome

Length of output: 2905


🏁 Script executed:

# Also check for VueDirective type definitions and how they're used
rg "VueDirective|VueVSlotShorthandDirective|VueVOnShorthandDirective" crates/biome_service/src/file_handlers/html.rs -B 2 -A 2

Repository: biomejs/biome

Length of output: 1294


🏁 Script executed:

# Check if there's special handling for v-for directives
rg "v-for|VueVFor" crates/ -i

Repository: biomejs/biome

Length of output: 41634


🏁 Script executed:

# Look for embedded-bindings or binding pattern handling
rg "embedded.?binding|binding.?pattern" crates/ -i

Repository: biomejs/biome

Length of output: 50370


🏁 Script executed:

# Check if v-for is being parsed or excluded
rg "v-for" crates/biome_service/src/file_handlers/html.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look at the actual VueDirective type definition
rg "impl VueDirective|pub.*VueDirective" crates/biome_html_syntax --max-count=5 -B 2 -A 5

Repository: biomejs/biome

Length of output: 6097


🏁 Script executed:

# Check the test case that's commented out more carefully
cat crates/biome_cli/tests/cases/handle_vue_files.rs | head -100

Repository: biomejs/biome

Length of output: 2579


🏁 Script executed:

# Look for any directive name filtering logic
rg "directive.*name|name.*directive" crates/biome_service/src/file_handlers/html.rs -B 2 -A 2

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Check what the current test expects for v-for
cat crates/biome_cli/tests/snapshots/main_cases_handle_vue_files/no_unused_variables_in_vue_directives.snap

Repository: biomejs/biome

Length of output: 1214


🏁 Script executed:

# Check the HTML parser definition - look for Vue directive patterns
rg "VueDirective" crates/biome_html_parser/src --max-count=10 -B 1 -A 3

Repository: biomejs/biome

Length of output: 3351


🏁 Script executed:

# See if there's a test file specifically for v-for directive handling
find crates -name "*vueDirectives*" -o -name "*vue_directive*" | head -20

Repository: biomejs/biome

Length of output: 426


🏁 Script executed:

# Look at the noUnusedVariables test for vue directives
cat crates/biome_js_analyze/tests/specs/correctness/noUnusedVariables/vueDirectives.vue

Repository: biomejs/biome

Length of output: 878


🏁 Script executed:

# Check what the vueDirectives.vue.snap output actually shows
cat crates/biome_js_analyze/tests/specs/correctness/noUnusedVariables/vueDirectives.vue.snap

Repository: biomejs/biome

Length of output: 3883


🏁 Script executed:

# Check if there's a rule or setting that filters directives by name
rg "v-for|v-on|v-bind|v-if|v-show" crates/biome_service/src/file_handlers/html.rs -B 3 -A 3

Repository: biomejs/biome

Length of output: 666


🏁 Script executed:

# Look at the embedded_bindings to see if it has special logic for binding patterns
head -200 crates/biome_service/src/workspace/document/services/embedded_bindings.rs

Repository: biomejs/biome

Length of output: 8364


Confirm: v-for and v-slot directives need special handling. This code parses all Vue directive values as JavaScript expressions, but v-for (item in items) and v-slot ({ item }) contain binding patterns rather than plain expressions. When parsed as JS, the binding declarations are lost, and the embedded-bindings layer (which extracts only from variable declarations) cannot recognise them. This causes false unused-variable diagnostics—as evidenced by the test snapshot where items is flagged as unused despite appearing in v-for.

Either add specific parsing logic for these directive types or filter to parse only pure-expression directives.

🤖 Prompt for AI Agents
In `@crates/biome_service/src/file_handlers/html.rs` around lines 619 - 699, The
directive parsing treats all Vue directives as JS expressions, which breaks
v-for and v-slot because they contain binding patterns; update the handler to
special-case VueVForDirective and VueVSlotDirective (and
VueVSlotShorthandDirective) instead of sending them to
parse_directive_string_value: detect these directives where initializer()
exists, and either (A) call a new helper (e.g., parse_directive_binding_pattern)
that parses binding patterns and returns identifier bindings to push into
nodes/embedded-bindings, or (B) skip adding them as expression embeddings so the
embedded-bindings layer won’t emit false unused-variable diagnostics; implement
the new branch alongside the existing
VueDirective/VueVOnShorthand/VueVBindShorthand handling and reuse
embedded_file_source.with_embedding_kind and nodes so only pure-expression
directives go to parse_directive_string_value.

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.

The bot is correct here, at least for v-for.

The syntax for v-for isn't an expression, its more like the <iter> part of a for loop for (<iter>) { ... }, but it also includes a range syntax. its not really js. related is #8062, it's more complicated than it seems.

And for v-slot, I think the bot might be right as well. https://vuejs.org/api/built-in-directives.html#v-slot I think the value for a v-slot directive is a variable or a object destructuring. (see the example on that page)

Comment on lines +766 to +785
// Parse Svelte directive attributes (bind:, class:, use:, etc.)
// Note: on: event handlers are legacy Svelte 3/4 syntax and not supported.
// Svelte 5 runes mode uses regular attributes for event handlers.
for element in html_root.syntax().descendants() {
// Handle special Svelte directives (bind:, class:, etc.)
if let Some(directive) = AnySvelteDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let file_source = embedded_file_source
.with_embedding_kind(EmbeddingKind::Svelte { is_source: false });
if let Some((content, doc_source)) = parse_directive_text_expression(
&initializer,
cache,
biome_path,
settings,
file_source,
) {
nodes.push((content.into(), doc_source));
}
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let me examine the file and surrounding context
cd /repo && cat -n crates/biome_service/src/file_handlers/html.rs | sed -n '750,800p'

Repository: biomejs/biome

Length of output: 112


🏁 Script executed:

# Search for AnySvelteDirective definition
rg "enum AnySvelteDirective|struct AnySvelteDirective" -A 20

Repository: biomejs/biome

Length of output: 1891


🏁 Script executed:

# Check what directive types are included
rg "AnySvelteDirective" -B 2 -A 5 | head -100

Repository: biomejs/biome

Length of output: 7732


🏁 Script executed:

# Look for Svelte directive related enums and types
rg "SvelteDirective|bind:|class:|on:" --type rs -B 1 -A 1 | head -150

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

# Check the parse_directive_text_expression function
rg "fn parse_directive_text_expression" -A 30

Repository: biomejs/biome

Length of output: 2703


🏁 Script executed:

rg "SvelteOn|on:" xtask/codegen/html.ungram -A 2 -B 2

Repository: biomejs/biome

Length of output: 1540


🏁 Script executed:

# Look for any event handler or on: handling in the codebase
rg "on:" crates/biome_service/src/file_handlers/html.rs -B 3 -A 3

Repository: biomejs/biome

Length of output: 6620


🏁 Script executed:

# Check the directive_ext implementation for all directive types
rg "fn initializer" crates/biome_html_syntax/src/directive_ext.rs -A 30

Repository: biomejs/biome

Length of output: 808


🏁 Script executed:

# Look for how shorthand directives are defined in the grammar
rg "SvelteBindDirective|SvelteClassDirective" xtask/codegen/html.ungram -B 2 -A 5

Repository: biomejs/biome

Length of output: 647


🏁 Script executed:

# Look for SvelteDirectiveValue definition to understand optional values
rg "SvelteDirectiveValue =" xtask/codegen/html.ungram -A 5

Repository: biomejs/biome

Length of output: 207


🏁 Script executed:

# Check if there's any handling for optional directive values
rg "SvelteDirective" xtask/codegen/html.ungram | grep -i "?"

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look at actual test files to see Svelte directive usage patterns
fd "svelte" --type f -path "*/tests/*" | head -10

Repository: biomejs/biome

Length of output: 225


🏁 Script executed:

# Search for test files with Svelte directives to understand shorthand syntax
rg "class:" --type txt --type md -l 2>/dev/null | head -5

Repository: biomejs/biome

Length of output: 72


🏁 Script executed:

# Check what initializer() returns for a directive without an explicit value
rg "pub fn initializer" crates/biome_html_syntax/src/directive_ext.rs -B 2 -A 15

Repository: biomejs/biome

Length of output: 913


🏁 Script executed:

# Look for any tests that demonstrate Svelte shorthand directive usage
rg "class:active|bind:value" crates/ -B 2 -A 2 | head -40

Repository: biomejs/biome

Length of output: 3616


🏁 Script executed:

# Check if there are any other code paths that handle directives without initializers
rg "AnySvelteDirective" crates/biome_service/src/file_handlers/html.rs -B 3 -A 8

Repository: biomejs/biome

Length of output: 1527


Shorthand directives without explicit initializers are not parsed. The code requires directive.initializer() to be Some(), but Svelte shorthand syntax like bind:value or class:active (without ={...}) have optional initializers and thus won't be processed. This means variables in shorthand directives won't be flagged as used, potentially causing false noUnusedVariables warnings. Confirm whether shorthand support is intentional or if the initializer check needs adjusting.

(The note about on: handlers is accurate—they're indeed not in AnySvelteDirective and Svelte 5 runes mode uses regular attributes instead.)

🤖 Prompt for AI Agents
In `@crates/biome_service/src/file_handlers/html.rs` around lines 766 - 785, The
loop over html_root.syntax().descendants() currently only processes directives
when directive.initializer() is Some(), skipping Svelte shorthand directives
(e.g., bind:value, class:active) that lack an initializer; update the handling
in the block that uses AnySvelteDirective::cast_ref and directive.initializer()
so that when initializer() is None you still record the shorthand as a used
identifier: derive a suitable expression/content from the directive itself (for
example using the directive name/target) and push a corresponding
(content.into(), doc_source) into nodes (reusing
embedded_file_source.with_embedding_kind and parse_directive_text_expression for
cases with an initializer), so shorthand directives are treated as usages
instead of being ignored.

Comment thread crates/biome_service/src/file_handlers/html.rs
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

This does fix all the false positives in my codebase that I was testing on! However, it might be a bit overzealous with regards to v-for and v-slot.

Comment thread .claude/skills/biome-developer/SKILL.md
Comment thread .claude/skills/biome-developer/SKILL.md Outdated
Comment thread crates/biome_html_parser/tests/quick_test.rs
@ematipico
Copy link
Copy Markdown
Member Author

This does fix all the false positives in my codebase that I was testing on! However, it might be a bit overzealous with regards to v-for and v-slot.

It requires special handling, yes. I preferred to keep it out of scope for this PR. I'll tackle it later

ematipico and others added 2 commits February 14, 2026 13:31
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@github-actions github-actions Bot removed the A-Linter Area: linter label Feb 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI 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.

2 participants