Skip to content

Commit 50eb160

Browse files
DonIsaaccamc314
andauthored
fix(linter/no-unused-vars): allow unused type params in ambient module blocks (#19615)
## What This PR Does Fixes a bug in `eslint/no-unused-vars` where type parameters used in merged interfaces were marked as unused. ### Example ```ts declare module 'bun:test' { // `T` is marked as unused, but removing it causes a type error. // This PR flags this param as allowed. interface Matchers<T> { toBeFoo(value: unknown): unknown; } } ``` --------- Co-authored-by: Cameron Clark <cameron.clark@hey.com>
1 parent 1dd0d21 commit 50eb160

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,43 @@ impl NoUnusedVars {
152152
symbol: &Symbol<'_, '_>,
153153
declaration_id: NodeId,
154154
) -> bool {
155-
matches!(symbol.nodes().parent_kind(declaration_id), AstKind::TSMappedType(_))
155+
let nodes = symbol.nodes();
156+
let scoping = symbol.scoping();
157+
158+
if matches!(nodes.parent_kind(declaration_id), AstKind::TSMappedType(_)) {
159+
return true;
160+
}
161+
162+
let is_interface_type_parameter = match nodes.parent_kind(declaration_id) {
163+
AstKind::TSInterfaceDeclaration(_) => true,
164+
AstKind::TSTypeParameterDeclaration(_) => {
165+
matches!(
166+
nodes.parent_kind(nodes.parent_id(declaration_id)),
167+
AstKind::TSInterfaceDeclaration(_)
168+
)
169+
}
170+
_ => false,
171+
};
172+
if !is_interface_type_parameter {
173+
return false;
174+
}
175+
176+
// type parameters used within type declarations in ambient ts module
177+
// blocks are required for declaration merging to work, since signatures
178+
// must match.
179+
let Some(parent_scope_id) = scoping.scope_parent_id(symbol.scope_id()) else {
180+
return false;
181+
};
182+
let scope_flags = scoping.scope_flags(parent_scope_id);
183+
if scope_flags.is_ts_module_block() {
184+
// get declaration node for the parent scope
185+
let parent_node_id = scoping.get_node_id(parent_scope_id);
186+
if let AstKind::TSModuleDeclaration(namespace) = nodes.get_node(parent_node_id).kind() {
187+
return namespace.declare;
188+
}
189+
}
190+
191+
false
156192
}
157193

158194
/// Returns `true` if this unused parameter should be allowed (i.e. not

crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1246,9 +1246,28 @@ fn test_namespaces() {
12461246
export { Foo }
12471247
",
12481248
"declare module 'tsdown' { function bar(): void; }",
1249+
"
1250+
declare module 'vitest' {
1251+
interface Matchers<T> {
1252+
toBeFoo(value: unknown): unknown;
1253+
}
1254+
}
1255+
",
12491256
];
12501257

1251-
let fail = vec!["namespace N {}", "export namespace N { function foo() }"];
1258+
let fail = vec![
1259+
"namespace N {}",
1260+
"export namespace N { function foo() }",
1261+
"
1262+
export namespace NonAmbientModuleDeclaration {
1263+
export interface Matchers<T> extends MatcherOverride {
1264+
toBeFoo(value: unknown): unknown;
1265+
}
1266+
}
1267+
",
1268+
"declare module 'bun:test' { type Matchers2<T> = {} }",
1269+
"declare module 'bun:test' { class MyClass<T> {} }",
1270+
];
12521271

12531272
Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
12541273
.intentionally_allow_no_fix_tests()

crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,37 @@ source: crates/oxc_linter/src/tester.rs
1717
· ╰── 'foo' is declared here
1818
╰────
1919
help: Consider removing this declaration.
20+
21+
eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'.
22+
╭─[no_unused_vars.tsx:3:39]
23+
2export namespace NonAmbientModuleDeclaration {
24+
3export interface Matchers<T> extends MatcherOverride {
25+
· ┬
26+
· ╰── 'T' is declared here
27+
4 │ toBeFoo(value: unknown): unknown;
28+
╰────
29+
help: Consider removing this declaration.
30+
31+
⚠ eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'.
32+
╭─[no_unused_vars.tsx:1:44]
33+
1 │ declare module 'bun:test' { type Matchers2<T> = {} }
34+
· ┬
35+
· ╰── 'T' is declared here
36+
╰────
37+
help: Consider removing this declaration.
38+
39+
⚠ eslint(no-unused-vars): Class 'MyClass' is declared but never used.
40+
╭─[no_unused_vars.tsx:1:35]
41+
1 │ declare module 'bun:test' { class MyClass<T> {} }
42+
· ───┬───
43+
· ╰── 'MyClass' is declared here
44+
╰────
45+
help: Consider removing this declaration.
46+
47+
⚠ eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'.
48+
╭─[no_unused_vars.tsx:1:43]
49+
1 │ declare module 'bun:test' { class MyClass<T> {} }
50+
· ┬
51+
· ╰── 'T' is declared here
52+
╰────
53+
help: Consider removing this declaration.

0 commit comments

Comments
 (0)