diff --git a/src/rules/no_implicit_declare_namespace_export.rs b/src/rules/no_implicit_declare_namespace_export.rs index a4cac1c1..42545af0 100644 --- a/src/rules/no_implicit_declare_namespace_export.rs +++ b/src/rules/no_implicit_declare_namespace_export.rs @@ -33,6 +33,18 @@ impl LintRule for NoImplicitDeclareNamespaceExport { } } +/// A file is an ES module when it has at least one top-level `import`/`export` +/// statement (every `ModuleDecl` is such a statement). +fn file_is_module(program: ast_view::Program) -> bool { + match program { + ast_view::Program::Module(module) => module + .body + .iter() + .any(|item| matches!(item, ast_view::ModuleItem::ModuleDecl(_))), + ast_view::Program::Script(_) => false, + } +} + struct NoImplicitDeclareNamespaceExportHandler; impl Handler for NoImplicitDeclareNamespaceExportHandler { @@ -41,6 +53,25 @@ impl Handler for NoImplicitDeclareNamespaceExportHandler { module_decl: &ast_view::TsModuleDecl, ctx: &mut Context, ) { + // `declare global { ... }` is a module-augmentation form: TypeScript + // explicitly disallows `export {}` inside it ("Exports and export + // assignments are not permitted in module augmentations"), so emitting + // the implicit-export hint here would suggest a fix that doesn't + // compile. See denoland/deno#33268. + if module_decl.inner.global { + return; + } + // `declare module "foo" { ... }` is also a module augmentation — and so + // likewise rejects `export {}` (TS2669) — but only when the surrounding + // file is itself a module (has a top-level import/export). In a plain + // ambient script it is a real ambient module declaration where members are + // implicitly exported and `export {}` is valid, so the rule still fires. + if module_decl.inner.declare + && matches!(&module_decl.id, ast_view::TsModuleName::Str(_)) + && file_is_module(ctx.program()) + { + return; + } if module_decl.inner.declare { if let Some(ast_view::TsNamespaceBody::TsModuleBlock(block)) = module_decl.body @@ -136,6 +167,28 @@ declare namespace bar { r#" declare namespace empty {} "#, + // `declare global` cannot use `export {}` inside it, so the rule + // must not flag implicit-export usage in that block. + // See denoland/deno#33268. + r#" +declare global { + const asdf = 1; +} + "#, + r#" +declare global { + interface Window { foo: string } +} + "#, + // `declare module "foo" { ... }` in a *module* file (note the top-level + // `export {}`) is a module augmentation, so `export {};` inside it is + // likewise rejected by TypeScript and the rule must not fire. + r#" +export {}; +declare module "foo" { + const x: number; +} + "#, }; } @@ -166,6 +219,16 @@ declare namespace empty {} ], }; + // `declare module "foo"` in a *script* file (no top-level import/export) is + // an ambient module declaration, not an augmentation, so members are + // implicitly exported and the rule should still fire. + assert_lint_err! { + NoImplicitDeclareNamespaceExport, + r#"declare module "foo" { type X = 1; }"#: [ + { col: 0, message: MESSAGE, hint: HINT } + ], + }; + assert_lint_err! { NoImplicitDeclareNamespaceExport, r#"declare namespace foo { class X {} }"#: [