From fd7793749601ac5a43fe359f9c8e62dcdab6bb89 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Sat, 21 Feb 2026 20:11:42 -0500 Subject: [PATCH 1/3] fix(linter/no-unused-vars): allow unused type params in ambient module blocks --- .../rules/eslint/no_unused_vars/allowed.rs | 24 ++++++++++++++++++- .../rules/eslint/no_unused_vars/tests/oxc.rs | 19 ++++++++++++++- .../eslint_no_unused_vars@oxc-namespaces.snap | 11 ++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs index 41af3c758edbf..7b159ec7930f0 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -152,7 +152,29 @@ impl NoUnusedVars { symbol: &Symbol<'_, '_>, declaration_id: NodeId, ) -> bool { - matches!(symbol.nodes().parent_kind(declaration_id), AstKind::TSMappedType(_)) + let nodes = symbol.nodes(); + let scoping = symbol.scoping(); + + if matches!(nodes.parent_kind(declaration_id), AstKind::TSMappedType(_)) { + return true; + } + + // type parameters used within type declarations in ambient ts module + // blocks are required for declaration merging to work, since signatures + // must match. + let Some(parent_scope_id) = scoping.scope_parent_id(symbol.scope_id()) else { + return false; + }; + let scope_flags = scoping.scope_flags(parent_scope_id); + if scope_flags.is_ts_module_block() { + // get declaration node for the parent scope + let parent_node_id = scoping.get_node_id(parent_scope_id); + if let AstKind::TSModuleDeclaration(namespace) = nodes.get_node(parent_node_id).kind() { + return namespace.declare; + } + } + + false } /// Returns `true` if this unused parameter should be allowed (i.e. not diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index 8c3036606098c..1d109bce3e9b5 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -1246,9 +1246,26 @@ fn test_namespaces() { export { Foo } ", "declare module 'tsdown' { function bar(): void; }", + " + declare module 'vitest' { + interface Matchers { + toBeFoo(value: unknown): unknown; + } + } + ", ]; - let fail = vec!["namespace N {}", "export namespace N { function foo() }"]; + let fail = vec![ + "namespace N {}", + "export namespace N { function foo() }", + " + export namespace NonAmbientModuleDeclaration { + export interface Matchers extends MatcherOverride { + toBeFoo(value: unknown): unknown; + } + } + ", + ]; Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail) .intentionally_allow_no_fix_tests() diff --git a/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap b/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap index 771ef1e993f2c..b6e9731daa7e9 100644 --- a/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap +++ b/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap @@ -1,7 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs --- - ⚠ eslint(no-unused-vars): Variable 'N' is declared but never used. ╭─[no_unused_vars.tsx:1:11] 1 │ namespace N {} @@ -17,3 +16,13 @@ source: crates/oxc_linter/src/tester.rs · ╰── 'foo' is declared here ╰──── help: Consider removing this declaration. + + ⚠ eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'. + ╭─[no_unused_vars.tsx:3:39] + 2 │ export namespace NonAmbientModuleDeclaration { + 3 │ export interface Matchers extends MatcherOverride { + · ┬ + · ╰── 'T' is declared here + 4 │ toBeFoo(value: unknown): unknown; + ╰──── + help: Consider removing this declaration. From b0017c9469867c03b5c24f008cf68b615245b059 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 25 Feb 2026 13:03:56 +0000 Subject: [PATCH 2/3] improve test coverage --- .../rules/eslint/no_unused_vars/tests/oxc.rs | 2 ++ .../eslint_no_unused_vars@oxc-namespaces.snap | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index 1d109bce3e9b5..bd47bbbd8a7ee 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -1265,6 +1265,8 @@ fn test_namespaces() { } } ", + "declare module 'bun:test' { type Matchers2 = {} }", + "declare module 'bun:test' { class MyClass {} }", ]; Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail) diff --git a/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap b/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap index b6e9731daa7e9..6a2918e8a3bfa 100644 --- a/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap +++ b/crates/oxc_linter/src/snapshots/eslint_no_unused_vars@oxc-namespaces.snap @@ -1,6 +1,7 @@ --- source: crates/oxc_linter/src/tester.rs --- + ⚠ eslint(no-unused-vars): Variable 'N' is declared but never used. ╭─[no_unused_vars.tsx:1:11] 1 │ namespace N {} @@ -26,3 +27,27 @@ source: crates/oxc_linter/src/tester.rs 4 │ toBeFoo(value: unknown): unknown; ╰──── help: Consider removing this declaration. + + ⚠ eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'. + ╭─[no_unused_vars.tsx:1:44] + 1 │ declare module 'bun:test' { type Matchers2 = {} } + · ┬ + · ╰── 'T' is declared here + ╰──── + help: Consider removing this declaration. + + ⚠ eslint(no-unused-vars): Class 'MyClass' is declared but never used. + ╭─[no_unused_vars.tsx:1:35] + 1 │ declare module 'bun:test' { class MyClass {} } + · ───┬─── + · ╰── 'MyClass' is declared here + ╰──── + help: Consider removing this declaration. + + ⚠ eslint(no-unused-vars): Variable 'T' is declared but never used. Unused variables should start with a '_'. + ╭─[no_unused_vars.tsx:1:43] + 1 │ declare module 'bun:test' { class MyClass {} } + · ┬ + · ╰── 'T' is declared here + ╰──── + help: Consider removing this declaration. From 0af9fce870801cc56ca6a8466e18fefc2c7c47d5 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 25 Feb 2026 18:43:02 +0000 Subject: [PATCH 3/3] u --- .../src/rules/eslint/no_unused_vars/allowed.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs index 7b159ec7930f0..048c2d835a444 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -159,6 +159,20 @@ impl NoUnusedVars { return true; } + let is_interface_type_parameter = match nodes.parent_kind(declaration_id) { + AstKind::TSInterfaceDeclaration(_) => true, + AstKind::TSTypeParameterDeclaration(_) => { + matches!( + nodes.parent_kind(nodes.parent_id(declaration_id)), + AstKind::TSInterfaceDeclaration(_) + ) + } + _ => false, + }; + if !is_interface_type_parameter { + return false; + } + // type parameters used within type declarations in ambient ts module // blocks are required for declaration merging to work, since signatures // must match.