Skip to content

Commit dd2aef0

Browse files
committed
fix(semantic): incorrect SymbolFlags of TSModuleDeclaration (#10350)
Based on TypeScript's [implementation](https://github.com/microsoft/TypeScript/blob/15392346d05045742e653eab5c87538ff2a3c863/src/compiler/binder.ts#L2384-L2393) to correct `SymbolFlags` of `TSModuleDeclaration`. The `SymbolFlags::NamespaceModule` and `SymbolFlags::ValueModule` have a significant difference, `NamespaceModule`: can only be referenced as a type. `ValueModule`: can only be referenced as a value. Let's take an example to see: ```ts namespace NamespaceModule { export type A = string } namespace ValueModule { export const A = 0; } ``` The following code is the JS output of the above example. ```js "use strict"; var ValueModule; (function (ValueModule) { ValueModule.A = 0; })(ValueModule || (ValueModule = {})); ``` Only `ValueModule` will be preserved and transformed. That means whether a `TSModuleDeclaration` needs to be transformed or removed directly, we can determine by its `SymbolFlags`. We can use it to simplify the current `TSModuleDeclaration` transformation later.
1 parent c37f048 commit dd2aef0

File tree

18 files changed

+1403
-838
lines changed

18 files changed

+1403
-838
lines changed

crates/oxc_ast/src/ast_impl/js.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,14 @@ impl<'a> Declaration<'a> {
10301030
Declaration::TSInterfaceDeclaration(decl) => Some(&decl.id),
10311031
Declaration::TSEnumDeclaration(decl) => Some(&decl.id),
10321032
Declaration::TSImportEqualsDeclaration(decl) => Some(&decl.id),
1033-
_ => None,
1033+
Declaration::TSModuleDeclaration(decl) => {
1034+
if let TSModuleDeclarationName::Identifier(ident) = &decl.id {
1035+
Some(ident)
1036+
} else {
1037+
None
1038+
}
1039+
}
1040+
Declaration::VariableDeclaration(_) => None,
10341041
}
10351042
}
10361043

crates/oxc_semantic/src/binder.rs

Lines changed: 266 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
33
use std::ptr;
44

5+
use oxc_allocator::{Address, GetAddress};
56
use oxc_ast::{AstKind, ast::*};
67
use oxc_ecmascript::{BoundNames, IsSimpleParameterList};
78
use oxc_syntax::{
9+
node::NodeId,
810
scope::{ScopeFlags, ScopeId},
911
symbol::SymbolFlags,
1012
};
@@ -382,27 +384,284 @@ impl<'a> Binder<'a> for TSEnumMember<'a> {
382384
}
383385

384386
impl<'a> Binder<'a> for TSModuleDeclaration<'a> {
385-
fn bind(&self, builder: &mut SemanticBuilder) {
387+
fn bind(&self, builder: &mut SemanticBuilder<'a>) {
386388
// do not bind `global` for `declare global { ... }`
387389
if self.kind.is_global() {
388390
return;
389391
}
390392
let TSModuleDeclarationName::Identifier(id) = &self.id else { return };
393+
let instantiated =
394+
get_module_instance_state(builder, self, builder.current_node_id).is_instantiated();
395+
let (includes, excludes) = if instantiated {
396+
(SymbolFlags::ValueModule, SymbolFlags::ValueModuleExcludes)
397+
} else {
398+
(SymbolFlags::NameSpaceModule, SymbolFlags::NamespaceModuleExcludes)
399+
};
391400

392401
// At declaration time a module has no value declaration it is only when a value declaration
393402
// is made inside a the scope of a module that the symbol is modified
394403
let ambient = if self.declare { SymbolFlags::Ambient } else { SymbolFlags::None };
395-
let symbol_id = builder.declare_symbol(
396-
id.span,
397-
&id.name,
398-
SymbolFlags::NameSpaceModule | ambient,
399-
SymbolFlags::None,
400-
);
404+
let symbol_id = builder.declare_symbol(id.span, &id.name, includes | ambient, excludes);
401405

402406
id.set_symbol_id(symbol_id);
403407
}
404408
}
405409

410+
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
411+
pub enum ModuleInstanceState {
412+
NonInstantiated,
413+
Instantiated,
414+
ConstEnumOnly,
415+
}
416+
417+
impl ModuleInstanceState {
418+
fn is_instantiated(self) -> bool {
419+
self != ModuleInstanceState::NonInstantiated
420+
}
421+
}
422+
423+
/// Determines if a module is instantiated or not.
424+
///
425+
/// Based on `https://github.com/microsoft/TypeScript/blob/15392346d05045742e653eab5c87538ff2a3c863/src/compiler/binder.ts#L342-L474`
426+
fn get_module_instance_state<'a>(
427+
builder: &mut SemanticBuilder<'a>,
428+
decl: &TSModuleDeclaration<'a>,
429+
current_node_id: NodeId,
430+
) -> ModuleInstanceState {
431+
get_module_instance_state_impl(builder, decl, current_node_id, &mut Vec::new())
432+
}
433+
434+
fn get_module_instance_state_impl<'a, 'b>(
435+
builder: &mut SemanticBuilder<'a>,
436+
decl: &'b TSModuleDeclaration<'a>,
437+
current_node_id: NodeId,
438+
module_declaration_stmts: &mut Vec<&'b Statement<'a>>,
439+
) -> ModuleInstanceState {
440+
let address = Address::from_ptr(decl);
441+
442+
if let Some(state) = builder.module_instance_state_cache.get(&address) {
443+
return *state;
444+
}
445+
446+
let Some(body) = &decl.body else {
447+
// For modules without a block, we consider them instantiated
448+
return ModuleInstanceState::Instantiated;
449+
};
450+
451+
// A module is uninstantiated if it contains only specific declarations
452+
let state = match body {
453+
TSModuleDeclarationBody::TSModuleBlock(block) => {
454+
let mut child_state = ModuleInstanceState::NonInstantiated;
455+
for stmt in &block.body {
456+
module_declaration_stmts.extend(block.body.iter());
457+
child_state = get_module_instance_state_for_statement(
458+
builder,
459+
stmt,
460+
current_node_id,
461+
module_declaration_stmts,
462+
);
463+
if child_state.is_instantiated() {
464+
break;
465+
}
466+
}
467+
child_state
468+
}
469+
TSModuleDeclarationBody::TSModuleDeclaration(module) => {
470+
get_module_instance_state(builder, module, current_node_id)
471+
}
472+
};
473+
474+
builder.module_instance_state_cache.insert(address, state);
475+
state
476+
}
477+
478+
fn get_module_instance_state_for_statement<'a, 'b>(
479+
builder: &mut SemanticBuilder<'a>,
480+
stmt: &'b Statement<'a>,
481+
current_node_id: NodeId,
482+
module_declaration_stmts: &mut Vec<&'b Statement<'a>>,
483+
) -> ModuleInstanceState {
484+
let address = stmt.address();
485+
if let Some(state) = builder.module_instance_state_cache.get(&address) {
486+
return *state;
487+
}
488+
489+
let state = match stmt {
490+
// 1. interface declarations, type alias declarations
491+
Statement::TSInterfaceDeclaration(_)
492+
| Statement::TSTypeAliasDeclaration(_)
493+
// 3. non-exported import declarations
494+
| Statement::TSImportEqualsDeclaration(_) => {
495+
ModuleInstanceState::NonInstantiated
496+
}
497+
// 2. const enum declarations
498+
Statement::TSEnumDeclaration(enum_decl) => {
499+
if enum_decl.r#const {
500+
ModuleInstanceState::ConstEnumOnly
501+
} else {
502+
ModuleInstanceState::Instantiated
503+
}
504+
}
505+
Statement::ExportDefaultDeclaration(export_decl) => {
506+
if matches!(export_decl.declaration, ExportDefaultDeclarationKind::TSInterfaceDeclaration(_)) {
507+
ModuleInstanceState::NonInstantiated
508+
} else {
509+
ModuleInstanceState::Instantiated
510+
}
511+
}
512+
Statement::ExportNamedDeclaration(export_decl) if export_decl.declaration.is_some() => {
513+
match export_decl.declaration.as_ref().unwrap() {
514+
Declaration::TSModuleDeclaration(module_decl) => {
515+
get_module_instance_state_impl(builder, module_decl, current_node_id, module_declaration_stmts)
516+
}
517+
decl => if decl.is_type() {
518+
ModuleInstanceState::NonInstantiated
519+
} else {
520+
ModuleInstanceState::Instantiated
521+
}
522+
}
523+
}
524+
// 4. Export alias declarations pointing at uninstantiated modules
525+
Statement::ExportNamedDeclaration(export_decl) => {
526+
if export_decl.source.is_none() {
527+
let mut export_state = ModuleInstanceState::NonInstantiated;
528+
for specifier in &export_decl.specifiers {
529+
export_state = get_module_instance_state_for_alias_target(builder, specifier, current_node_id, module_declaration_stmts.as_slice());
530+
if export_state.is_instantiated() {
531+
break;
532+
}
533+
}
534+
export_state
535+
} else {
536+
ModuleInstanceState::Instantiated
537+
}
538+
}
539+
// 5. other module declarations
540+
Statement::TSModuleDeclaration(module_decl) => {
541+
get_module_instance_state_impl(builder, module_decl, current_node_id, module_declaration_stmts)
542+
}
543+
// Any other type of statement means the module is instantiated
544+
_ => ModuleInstanceState::Instantiated,
545+
};
546+
547+
builder.module_instance_state_cache.insert(address, state);
548+
state
549+
}
550+
551+
// `module_declaration_stmts` is stored statements that are collected from the all ModuleBlocks.
552+
// The reason we need to collect and pass in this method is that we need to check export specifiers
553+
// whether they refer to a declaration that declared in the module block or not. And we can't use
554+
// `self.nodes.node(node_id)` to get the nested module block's statements since the child ModuleBlock
555+
// AstNode hasn't created yet.
556+
fn get_module_instance_state_for_alias_target<'a>(
557+
builder: &mut SemanticBuilder<'a>,
558+
specifier: &ExportSpecifier<'a>,
559+
mut current_node_id: NodeId,
560+
module_declaration_stmts: &[&Statement<'a>],
561+
) -> ModuleInstanceState {
562+
let ModuleExportName::IdentifierReference(local) = &specifier.local else {
563+
return ModuleInstanceState::Instantiated;
564+
};
565+
566+
let name = local.name;
567+
let mut current_block_stmts = module_declaration_stmts.to_vec();
568+
loop {
569+
let mut found = false;
570+
for stmt in &current_block_stmts {
571+
match stmt {
572+
Statement::VariableDeclaration(decl) => {
573+
decl.bound_names(&mut |id| {
574+
if id.name == name {
575+
found = true;
576+
}
577+
});
578+
}
579+
match_declaration!(Statement) => {
580+
if stmt.to_declaration().id().is_some_and(|id| id.name == name) {
581+
found = true;
582+
}
583+
}
584+
Statement::ExportNamedDeclaration(decl) => match decl.declaration.as_ref() {
585+
Some(Declaration::VariableDeclaration(decl)) => {
586+
decl.bound_names(&mut |id| {
587+
if id.name == name {
588+
found = true;
589+
}
590+
});
591+
}
592+
Some(decl) => {
593+
if decl.id().is_some_and(|id| id.name == name) {
594+
found = true;
595+
}
596+
}
597+
None => {
598+
continue;
599+
}
600+
},
601+
Statement::ExportDefaultDeclaration(decl) => match &decl.declaration {
602+
ExportDefaultDeclarationKind::FunctionDeclaration(decl) => {
603+
if decl.id.as_ref().is_some_and(|id| id.name == name) {
604+
found = true;
605+
}
606+
}
607+
ExportDefaultDeclarationKind::ClassDeclaration(decl) => {
608+
if decl.id.as_ref().is_some_and(|id| id.name == name) {
609+
found = true;
610+
}
611+
}
612+
ExportDefaultDeclarationKind::TSInterfaceDeclaration(decl) => {
613+
if decl.id.name == name {
614+
found = true;
615+
}
616+
}
617+
_ => {}
618+
},
619+
_ => {}
620+
}
621+
622+
if found {
623+
if matches!(stmt, Statement::TSImportEqualsDeclaration(_)) {
624+
// Treat re-exports of import aliases as instantiated,
625+
// since they're ambiguous. This is consistent with
626+
// `export import x = mod.x` being treated as instantiated:
627+
// import x = mod.x;
628+
// export { x };
629+
return ModuleInstanceState::Instantiated;
630+
}
631+
return get_module_instance_state_for_statement(
632+
builder,
633+
stmt,
634+
current_node_id,
635+
&mut Vec::default(), // No need to check export specifier
636+
);
637+
}
638+
}
639+
640+
let Some(node) = builder.nodes.ancestors(current_node_id).skip(1).find(|node| {
641+
matches!(
642+
node.kind(),
643+
AstKind::Program(_) | AstKind::TSModuleBlock(_) | AstKind::BlockStatement(_)
644+
)
645+
}) else {
646+
break;
647+
};
648+
649+
current_node_id = node.id();
650+
current_block_stmts.clear();
651+
// Didn't find the declaration whose name matches export specifier
652+
// in the current block, so we need to check the parent block.
653+
current_block_stmts.extend(match node.kind() {
654+
AstKind::Program(program) => program.body.iter(),
655+
AstKind::TSModuleBlock(block) => block.body.iter(),
656+
AstKind::BlockStatement(block) => block.body.iter(),
657+
_ => unreachable!(),
658+
});
659+
}
660+
661+
// Not found in any of the statements
662+
ModuleInstanceState::Instantiated
663+
}
664+
406665
impl<'a> Binder<'a> for TSTypeParameter<'a> {
407666
fn bind(&self, builder: &mut SemanticBuilder) {
408667
let scope_id = if matches!(

0 commit comments

Comments
 (0)