Skip to content

Commit fed72c7

Browse files
fix(html): track Vue template assignment references (#11156)
1 parent 2fa0a62 commit fed72c7

5 files changed

Lines changed: 90 additions & 4 deletions

File tree

.changeset/fifty-baboons-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#11129](https://github.com/biomejs/biome/issues/11129): [`noUnusedVariables`](https://biomejs.dev/linter/rules/no-unused-variables/) no longer reports Vue bindings as unused when they are assigned through automatically unwrapped template refs.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!-- should generate diagnostics -->
2+
<script setup lang="ts">
3+
const isNewSheetOpen = defineModel<boolean>("isNewSheetOpen");
4+
const actuallyUnused = true;
5+
</script>
6+
7+
<template>
8+
<button @click="() => (isNewSheetOpen = true)">button</button>
9+
</template>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid-vue-assignment-target.vue
4+
---
5+
# Input
6+
```vue
7+
<!-- should generate diagnostics -->
8+
<script setup lang="ts">
9+
const isNewSheetOpen = defineModel<boolean>("isNewSheetOpen");
10+
const actuallyUnused = true;
11+
</script>
12+
13+
<template>
14+
<button @click="() => (isNewSheetOpen = true)">button</button>
15+
</template>
16+
17+
```
18+
19+
# Diagnostics
20+
```
21+
invalid-vue-assignment-target.vue:4:7 lint/correctness/noUnusedVariables FIXABLE ━━━━━━━━━━━━━━━━━
22+
23+
! This variable actuallyUnused is unused.
24+
25+
2 <script setup lang="ts">
26+
3 │ const isNewSheetOpen = defineModel<boolean>("isNewSheetOpen");
27+
> 4 │ const actuallyUnused = true;
28+
│ ^^^^^^^^^^^^^^
29+
5 │ </script>
30+
6 │
31+
32+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
33+
34+
i Unsafe fix: If this is intentional, prepend actuallyUnused with an underscore.
35+
36+
1 1 │
37+
2 2 │ const isNewSheetOpen = defineModel<boolean>("isNewSheetOpen");
38+
3 │ - const·actuallyUnused·=·true;
39+
3 │ + const·_actuallyUnused·=·true;
40+
4 4 │
41+
42+
43+
```

crates/biome_workspace_db/src/embedded/references.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,21 @@ mod tests {
282282
));
283283
}
284284

285+
#[test]
286+
fn is_value_reference_used_finds_vue_assignment_targets() {
287+
let db = TestDb::new();
288+
let path = parse_vue_source_with_js_snippet(&db, "() => isNewSheetOpen = true");
289+
290+
assert!(is_value_reference_used(
291+
&db,
292+
InternedReference::new(&db, path.clone(), token_text("isNewSheetOpen"))
293+
));
294+
assert!(!is_value_reference_used(
295+
&db,
296+
InternedReference::new(&db, path, token_text("Missing"))
297+
));
298+
}
299+
285300
#[test]
286301
fn is_reference_used_classifies_type_references() {
287302
let db = TestDb::new();

crates/biome_workspace_db/src/embedded/visitor.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use biome_js_syntax::{
1414
AnyJsIdentifierUsage, AnyJsModuleItem, AnyJsObjectAssignmentPatternMember,
1515
AnyJsObjectBindingPatternMember, AnyJsObjectMember, AnyJsRoot, AnyJsStatement,
1616
AnyTsIdentifierBinding, AnyTsType, JsAssignmentExpression, JsCallExpression, JsExport,
17-
JsImport, JsModuleItemList, JsReferenceIdentifier, JsStaticMemberExpression,
18-
JsSvelteDeclarationRoot, JsSvelteSnippetRoot, JsVariableStatement, JsxReferenceIdentifier,
17+
JsIdentifierAssignment, JsImport, JsModuleItemList, JsReferenceIdentifier,
18+
JsStaticMemberExpression, JsSvelteDeclarationRoot, JsSvelteSnippetRoot, JsVariableStatement,
19+
JsxReferenceIdentifier,
1920
};
2021
use biome_languages::html::HtmlVariant;
2122
use biome_languages::javascript::{JsEmbeddingKind, SvelteEmbeddingKind};
@@ -171,7 +172,7 @@ fn collect_embedded_references(
171172
// one module and share a top-level scope, so a binding used only in
172173
// the other block must still count as used.
173174
if !js_file_source.is_embedded_source() || is_svelte {
174-
builder.visit_non_source_snippet(&snippet.parsed(db).tree());
175+
builder.visit_non_source_snippet(&snippet.parsed(db).tree(), &js_file_source);
175176
}
176177
}
177178

@@ -1139,7 +1140,7 @@ impl EmbeddedReferencesBuilder {
11391140
self.type_references.push((range, text));
11401141
}
11411142

1142-
fn visit_non_source_snippet(&mut self, root: &AnyJsRoot) {
1143+
fn visit_non_source_snippet(&mut self, root: &AnyJsRoot, file_source: &JsFileSource) {
11431144
let preorder = root.syntax().preorder();
11441145

11451146
for event in preorder {
@@ -1149,6 +1150,10 @@ impl EmbeddedReferencesBuilder {
11491150
self.visit_jsx_reference_identifier(reference);
11501151
} else if let Some(reference) = JsReferenceIdentifier::cast_ref(&node) {
11511152
self.visit_reference_identifier(reference);
1153+
} else if let Some(assignment) = JsIdentifierAssignment::cast_ref(&node)
1154+
&& matches!(file_source.as_embedding_kind(), JsEmbeddingKind::Vue { .. })
1155+
{
1156+
self.visit_identifier_assignment(assignment);
11521157
} else if let Some(member) = JsStaticMemberExpression::cast_ref(&node) {
11531158
self.visit_static_member_expression(member);
11541159
}
@@ -1296,6 +1301,15 @@ impl EmbeddedReferencesBuilder {
12961301
Some(())
12971302
}
12981303

1304+
fn visit_identifier_assignment(&mut self, assignment: JsIdentifierAssignment) -> Option<()> {
1305+
let name_token = assignment.name_token().ok()?;
1306+
self.register_reference(
1307+
name_token.text_trimmed_range(),
1308+
name_token.token_text_trimmed(),
1309+
);
1310+
Some(())
1311+
}
1312+
12991313
fn visit_static_member_expression(&mut self, member: JsStaticMemberExpression) -> Option<()> {
13001314
let object = member.object().ok()?;
13011315
if let Some(reference) = object.as_js_reference_identifier() {

0 commit comments

Comments
 (0)