Skip to content

Commit a689cb5

Browse files
authored
fix(noUnusedVariables): recognize merged interfaces (#11600)
1 parent 092a89f commit a689cb5

6 files changed

Lines changed: 228 additions & 5 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6644](https://github.com/biomejs/biome/issues/6644): [`noUnusedVariables`](https://biomejs.dev/linter/rules/no-unused-variables/) now recognizes all interface declarations in a TypeScript declaration-merging group when the interface is referenced.
6+
7+
The following snippet no longer triggers the rule.
8+
9+
```ts
10+
interface Things {
11+
foo: string;
12+
}
13+
14+
interface Things {
15+
bar: string;
16+
}
17+
18+
export type Key = keyof Things;
19+
20+
interface Things {
21+
baz: string;
22+
}
23+
```

‎crates/biome_js_analyze/src/lint/correctness/no_unused_variables.rs‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -475,9 +475,9 @@ fn is_implemented_overload_type_parameter(
475475
}
476476

477477
signatures.iter().any(|id| {
478-
model.binding_by_id(*id).is_some_and(|binding| {
479-
binding.syntax().text_trimmed_range() == signature_range
480-
})
478+
model
479+
.binding_by_id(*id)
480+
.is_some_and(|binding| binding.syntax().text_trimmed_range() == signature_range)
481481
})
482482
})
483483
}
@@ -735,6 +735,9 @@ fn is_declaration_merged_with_used(
735735
) -> Option<bool> {
736736
let decl = binding.declaration()?;
737737
match decl {
738+
AnyJsBindingDeclaration::TsInterfaceDeclaration(_) => {
739+
is_interface_merged_with_used(model, binding)
740+
}
738741
AnyJsBindingDeclaration::TsModuleDeclaration(_) => {
739742
is_namespace_merged_with_used_value(model, binding)
740743
}
@@ -745,6 +748,30 @@ fn is_declaration_merged_with_used(
745748
}
746749
}
747750

751+
fn is_interface_merged_with_used(
752+
model: &SemanticModel,
753+
binding: &AnyJsIdentifierBinding,
754+
) -> Option<bool> {
755+
let name_token = binding.name_token().ok()?;
756+
let name = name_token.text_trimmed();
757+
let scope = model.scope_hoisted_to(binding.syntax())?;
758+
759+
Some(scope.bindings().any(|scope_binding| {
760+
let other = scope_binding.tree();
761+
let Some(other_declaration) = other.declaration() else {
762+
return false;
763+
};
764+
matches!(
765+
other_declaration,
766+
AnyJsBindingDeclaration::TsInterfaceDeclaration(_)
767+
) && other
768+
.name_token()
769+
.is_ok_and(|other_name| other_name.text_trimmed() == name)
770+
&& (model.is_exported(&other)
771+
|| !is_unused_by_references(model, &other, other_declaration.syntax()))
772+
}))
773+
}
774+
748775
/// Returns `true` if `call` is a call to a simple identifier named `name`.
749776
fn is_call_to(call: &JsCallExpression, name: &str) -> bool {
750777
let Ok(AnyJsExpression::JsIdentifierExpression(ident)) = call.callee() else {
@@ -889,8 +916,7 @@ pub fn is_unused(model: &SemanticModel, binding: &AnyJsIdentifierBinding) -> boo
889916
let Some(declaration) = binding.declaration() else {
890917
return false;
891918
};
892-
let declaration = declaration.syntax();
893-
if !is_unused_by_references(model, binding, declaration) {
919+
if !is_unused_by_references(model, binding, declaration.syntax()) {
894920
return false;
895921
}
896922
!is_declaration_merged_with_used(model, binding).unwrap_or(false)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* should generate diagnostics */
2+
3+
interface Unused {
4+
first: string;
5+
}
6+
7+
interface Unused {
8+
second: string;
9+
}
10+
11+
const ValueOnly = 0;
12+
interface ValueOnly {
13+
prop: string;
14+
}
15+
console.log(ValueOnly);
16+
17+
interface Shadowed {
18+
outer: string;
19+
}
20+
export function useShadowed() {
21+
interface Shadowed {
22+
inner: string;
23+
}
24+
type Key = keyof Shadowed;
25+
return null as Key;
26+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalidInterfaceDeclarationMerging.ts
4+
---
5+
# Input
6+
```ts
7+
/* should generate diagnostics */
8+
9+
interface Unused {
10+
first: string;
11+
}
12+
13+
interface Unused {
14+
second: string;
15+
}
16+
17+
const ValueOnly = 0;
18+
interface ValueOnly {
19+
prop: string;
20+
}
21+
console.log(ValueOnly);
22+
23+
interface Shadowed {
24+
outer: string;
25+
}
26+
export function useShadowed() {
27+
interface Shadowed {
28+
inner: string;
29+
}
30+
type Key = keyof Shadowed;
31+
return null as Key;
32+
}
33+
34+
```
35+
36+
# Diagnostics
37+
```
38+
invalidInterfaceDeclarationMerging.ts:3:11 lint/correctness/noUnusedVariables ━━━━━━━━━━━━━━━━━━━━━━
39+
40+
! This interface Unused is unused.
41+
42+
1 │ /* should generate diagnostics */
43+
2 │
44+
> 3 │ interface Unused {
45+
│ ^^^^^^
46+
4 │ first: string;
47+
5 │ }
48+
49+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
50+
51+
52+
```
53+
54+
```
55+
invalidInterfaceDeclarationMerging.ts:7:11 lint/correctness/noUnusedVariables ━━━━━━━━━━━━━━━━━━━━━━
56+
57+
! This interface Unused is unused.
58+
59+
5 │ }
60+
6 │
61+
> 7 │ interface Unused {
62+
│ ^^^^^^
63+
8 │ second: string;
64+
9 │ }
65+
66+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
67+
68+
69+
```
70+
71+
```
72+
invalidInterfaceDeclarationMerging.ts:12:11 lint/correctness/noUnusedVariables ━━━━━━━━━━━━━━━━━━━━━
73+
74+
! This interface ValueOnly is unused.
75+
76+
11 │ const ValueOnly = 0;
77+
> 12 │ interface ValueOnly {
78+
│ ^^^^^^^^^
79+
13 │ prop: string;
80+
14 │ }
81+
82+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
83+
84+
85+
```
86+
87+
```
88+
invalidInterfaceDeclarationMerging.ts:17:11 lint/correctness/noUnusedVariables ━━━━━━━━━━━━━━━━━━━━━
89+
90+
! This interface Shadowed is unused.
91+
92+
15 │ console.log(ValueOnly);
93+
16 │
94+
> 17 │ interface Shadowed {
95+
│ ^^^^^^^^
96+
18 │ outer: string;
97+
19 │ }
98+
99+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
100+
101+
102+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* should not generate diagnostics */
2+
3+
interface Things {
4+
foo: string;
5+
}
6+
7+
interface Things {
8+
bar: string;
9+
}
10+
11+
type Key = keyof Things;
12+
13+
interface Things {
14+
baz: string;
15+
}
16+
17+
export function doStuff(key: Key) {
18+
return key;
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: validInterfaceDeclarationMerging.ts
4+
---
5+
# Input
6+
```ts
7+
/* should not generate diagnostics */
8+
9+
interface Things {
10+
foo: string;
11+
}
12+
13+
interface Things {
14+
bar: string;
15+
}
16+
17+
type Key = keyof Things;
18+
19+
interface Things {
20+
baz: string;
21+
}
22+
23+
export function doStuff(key: Key) {
24+
return key;
25+
}
26+
27+
```

0 commit comments

Comments
 (0)