Files
camc314 44fd320324 feat(ast)!: split TS external modules & Namespace Declarations (#25284)
## Summary

- Splits `TSModuleDeclaration` into two dedicated declaration nodes:
  - `TSExternalModuleDeclaration` for string-literal modules and module augmentations.
  - `TSNamespaceDeclaration` for identifier-based `module` and `namespace` declarations.
- Removes `TSModuleDeclaration`, `TSModuleDeclarationName`, `TSModuleDeclarationBody`, and `TSModuleDeclarationKind`.
- Makes invalid combinations of names, bodies, and declaration kinds unrepresentable.
- Ensures only namespace declarations introduce a normal binding.
- Updates all AST consumers, generators, traversal, formatting, code generation, semantic analysis, lint rules, transforms, and serialization.
- Keeps normal and eager-raw ESTree output compatible: both declarations remain `TSModuleDeclaration` nodes.
- Allows experimental lazy raw deserialization to expose the two new node types directly.

## Breaking Change

The Rust AST changes from:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 38,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 39,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 40,
}

pub struct TSModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: TSModuleDeclarationName<'a>,
    pub body: Option<TSModuleDeclarationBody<'a>>,
    pub kind: TSModuleDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

to:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSExternalModuleDeclaration(Box<'a, TSExternalModuleDeclaration<'a>>) = 38,
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 39,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 40,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 41,
}

pub struct TSExternalModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: StringLiteral<'a>,
    pub body: Option<Box<'a, TSModuleBlock<'a>>>,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub struct TSNamespaceDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: BindingIdentifier<'a>,
    pub body: TSNamespaceDeclarationBody<'a>,
    pub kind: TSNamespaceDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub enum TSNamespaceDeclarationBody<'a> {
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 0,
    TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
}

pub enum TSNamespaceDeclarationKind {
    Module = 0,
    Namespace = 1,
}
```

The corresponding inherited `Statement` variants, `AstKind` variants, builders, and visitor methods have also changed.

## Why?

`TSModuleDeclaration` previously represented two declaration families with different syntax and semantics:

```ts
declare module "foo" {}
declare module "*.css";

module Foo {}
namespace Foo {}
namespace Foo.Bar {}
```

The shared representation allowed invalid states such as:

- A namespace with a string-literal name.
- An external module with an identifier name.
- An identifier namespace without a body.
- A dotted external module.
- An external module containing another module declaration as its body.
- A declaration whose `kind` disagreed with its name.

Consumers also had to inspect `id` before determining whether the node introduced a binding or represented an external module.

The two declaration families have stronger independent invariants:

### External modules

- Always have a string-literal name.
- Never introduce a normal binding identifier.
- May omit their body.
- Cannot use dotted names.
- Use module-body import and export rules.

### Namespace declarations

- Always have a binding identifier.
- Always have a body.
- May contain nested namespace declarations for dotted names.
- Explicitly preserve the `module` or `namespace` spelling.
- Introduce a namespace/module symbol.
- Use namespace-body restrictions.

Dedicated nodes encode these differences directly.

## Declaration forms

External modules use `TSExternalModuleDeclaration`:

```ts
declare module "foo" {}
declare module "*.css";
module "foo" {}
```

Identifier-based declarations use `TSNamespaceDeclaration`:

```ts
module Foo {}
namespace Foo {}
```

Dotted namespaces are represented as nested namespace declarations:

```ts
namespace Foo.Bar {}
```

```rust
TSNamespaceDeclaration {
    id: Foo,
    body: TSNamespaceDeclarationBody::TSNamespaceDeclaration(
        TSNamespaceDeclaration {
            id: Bar,
            body: TSNamespaceDeclarationBody::TSModuleBlock(...),
            // ...
        }
    ),
    // ...
}
```

Only `TSExternalModuleDeclaration::body` is optional.

## ESTree compatibility

Normal serialization and eager raw deserialization continue to emit the existing ESTree shape:

```ts
interface TSModuleDeclaration {
  type: "TSModuleDeclaration";
  id: BindingIdentifier | StringLiteral | TSQualifiedName;
  body: TSModuleBlock | null;
  kind: "module" | "namespace";
  declare: boolean;
  global: false;
}
```

Dotted namespaces continue to serialize as a single `TSModuleDeclaration` with a `TSQualifiedName`.

Experimental lazy raw deserialization is not compatibility-preserving. It now exposes `TSExternalModuleDeclaration` and `TSNamespaceDeclaration` directly, with separate constructors, layouts, and visitor type IDs.

## How to migrate

Code that previously matched all forms through one variant:

```rust
if let Declaration::TSModuleDeclaration(declaration) = declaration {
    // ...
}
```

should now match the required declaration family:

```rust
match declaration {
    Declaration::TSExternalModuleDeclaration(declaration) => {
        // String-literal external module
    }
    Declaration::TSNamespaceDeclaration(declaration) => {
        // Identifier-based module or namespace
    }
    _ => {}
}
```

The same applies to `Statement` and `AstKind`:

```rust
Statement::TSExternalModuleDeclaration(declaration)
Statement::TSNamespaceDeclaration(declaration)

AstKind::TSExternalModuleDeclaration(declaration)
AstKind::TSNamespaceDeclaration(declaration)
```

Common field migrations:

| Previous representation | New representation |
| --- | --- |
| `TSModuleDeclarationName::StringLiteral(id)` | `TSExternalModuleDeclaration::id` |
| `TSModuleDeclarationName::Identifier(id)` | `TSNamespaceDeclaration::id` |
| `body: None` | Only valid on `TSExternalModuleDeclaration` |
| `TSModuleDeclarationBody::TSModuleBlock` | External `body` or `TSNamespaceDeclarationBody::TSModuleBlock` |
| `TSModuleDeclarationBody::TSModuleDeclaration` | `TSNamespaceDeclarationBody::TSNamespaceDeclaration` |
| `TSModuleDeclarationKind::Module` | External module, or `TSNamespaceDeclarationKind::Module` |
| `TSModuleDeclarationKind::Namespace` | `TSNamespaceDeclarationKind::Namespace` |
| Inspect `id` to determine binding behavior | Match `TSNamespaceDeclaration` |
| `declaration.id()` for string modules | Now returns `None` |
| `visit_ts_module_declaration` | `visit_ts_external_module_declaration` or `visit_ts_namespace_declaration` |

When constructing nodes, use the generated `TSExternalModuleDeclaration` or `TSNamespaceDeclaration` builders rather than recreating the previous shared representation.

fixes https://github.com/oxc-project/backlog/issues/218
2026-08-05 14:37:35 +00:00
..
…