Skip to content
Open
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
63 changes: 63 additions & 0 deletions src/rules/no_implicit_declare_namespace_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
"#,
};
}

Expand Down Expand Up @@ -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 {} }"#: [
Expand Down