Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ impl Symbol<'_, '_> {
.map(|scope_id| scopes.get_node_id(scope_id))
.map(|node_id| nodes.get_node(node_id))
.any(|node| match node.kind() {
AstKind::TSModuleDeclaration(namespace) => is_ambient_namespace(namespace),
AstKind::TSModuleDeclaration(namespace) => {
is_ambient_namespace_without_explicit_exports(namespace)
}
// No need to check `declare` field, as `global` is only valid in ambient context
AstKind::TSGlobalDeclaration(_) => true,
_ => false,
Expand All @@ -84,8 +86,31 @@ impl Symbol<'_, '_> {
}

#[inline]
fn is_ambient_namespace(namespace: &TSModuleDeclaration) -> bool {
namespace.declare
fn is_ambient_namespace_without_explicit_exports(namespace: &TSModuleDeclaration) -> bool {
// Must be declared (ambient context)
if !namespace.declare {
return false;
}

// If the module has explicit exports, unused types should still be checked
// For modules with string literal names (like `declare module 'foo'`), if they have
// an export statement, then only exported items are available externally
if let Some(TSModuleDeclarationBody::TSModuleBlock(block)) = &namespace.body {
let has_export = block.body.iter().any(|stmt| {
matches!(
stmt,
Statement::ExportAllDeclaration(_)
| Statement::ExportDefaultDeclaration(_)
| Statement::ExportNamedDeclaration(_)
| Statement::TSExportAssignment(_)
)
});
if has_export {
return false;
}
}

true
}

impl NoUnusedVars {
Expand All @@ -95,10 +120,7 @@ impl NoUnusedVars {
symbol: &Symbol<'_, 'a>,
namespace: &TSModuleDeclaration<'a>,
) -> bool {
if is_ambient_namespace(namespace) {
return true;
}
symbol.is_in_declared_module()
namespace.declare || symbol.is_in_declared_module()
}

/// Returns `true` if this unused variable declaration should be allowed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ impl NoUnusedVars {
| AstKind::TSTypeAliasDeclaration(_)
| AstKind::TSTypeParameter(_) => self.is_ignored_var(declared_binding),
AstKind::Function(func) => {
func.r#type.is_typescript_syntax() || self.is_ignored_var(declared_binding)
// Functions with TypeScript syntax are ignored only if they are truly ambient
// (i.e., declared or in a declared module). Functions without bodies inside
// non-declared namespaces should still be checked.
if func.r#type.is_typescript_syntax() || func.body.is_none() {
return func.declare || symbol.is_in_declared_module();
}
self.is_ignored_var(declared_binding)
}
AstKind::Class(class) => {
if class.declare
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ impl NoUnusedVars {
}
ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None, false));
}
AstKind::TSInterfaceDeclaration(_) => {
AstKind::TSInterfaceDeclaration(_) | AstKind::TSTypeAliasDeclaration(_) => {
if symbol.is_in_declared_module() {
return;
}
Expand Down
21 changes: 13 additions & 8 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,19 +1139,24 @@ fn test_namespaces() {
",
"
interface Foo {}
namespace Foo {
export const a = {};
}
namespace Foo { export const a = {}; }
const foo: Foo = Foo.a
console.log(foo)
",
"
export declare namespace Foo {
type foo = 123;
}
",
"export declare namespace Foo { interface Bar { baz: string; } }",
"
declare namespace Foo { type foo = 123; }
export { Foo }
",
"declare module 'tsdown' { function bar(): void; }",
];

let fail = vec![
"namespace N {}",
// FIXME
// "export namespace N { function foo() }",
];
let fail = vec!["namespace N {}", "export namespace N { function foo() }"];

Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
.intentionally_allow_no_fix_tests()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1115,14 +1115,14 @@ fn test() {
",
None,
),
// (
// "
// declare module 'foo' {
// type Test = 1;
// }
// ",
// None,
// ),
(
"
declare module 'foo' {
type Test = 1;
}
",
None,
),
(
"
declare module 'foo' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ source: crates/oxc_linter/src/tester.rs
· ╰── 'N' is declared here
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Function 'foo' is declared but never used.
╭─[no_unused_vars.tsx:1:31]
1 │ export namespace N { function foo() }
· ─┬─
· ╰── 'foo' is declared here
╰────
help: Consider removing this declaration.
Loading