Skip to content

Commit d1f5abb

Browse files
committed
fix(ast/estree): fix TS-ESTree AST for TSModuleDeclaration (#10574)
Part of #9705. Our AST represents `module X.Y.Z {}` as 3 x nested `TSModuleDeclaration`s. TS-ESTree represents it as a single `TSModuleDeclaration`, with a nested tree of `TSQualifiedName`s as `id`. Additionally, TS-ESTree skips the `body` field if the module declaration has no body e.g. `module "foo";`. Align our TS-ESTree AST with these oddities.
1 parent 3c27d0d commit d1f5abb

File tree

7 files changed

+191
-126
lines changed

7 files changed

+191
-126
lines changed

crates/oxc_ast/src/ast/ts.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,14 +1173,19 @@ pub enum TSTypePredicateName<'a> {
11731173
)]
11741174
#[derive(Debug)]
11751175
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
1176-
#[estree(add_fields(global = TSModuleDeclarationGlobal))]
1176+
#[estree(
1177+
via = TSModuleDeclarationConverter,
1178+
add_fields(global = TSModuleDeclarationGlobal),
1179+
)]
11771180
pub struct TSModuleDeclaration<'a> {
11781181
pub span: Span,
11791182
/// The name of the module/namespace being declared.
11801183
///
11811184
/// Note that for `declare global {}`, no symbol will be created for the module name.
1185+
#[estree(ts_type = "BindingIdentifier | StringLiteral | TSQualifiedName")]
11821186
pub id: TSModuleDeclarationName<'a>,
11831187
#[scope(enter_before)]
1188+
#[estree(ts_type = "TSModuleBlock | null")]
11841189
pub body: Option<TSModuleDeclarationBody<'a>>,
11851190
/// The keyword used to define this module declaration.
11861191
///
@@ -1235,6 +1240,7 @@ pub enum TSModuleDeclarationKind {
12351240
#[ast(visit)]
12361241
#[derive(Debug)]
12371242
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
1243+
#[estree(no_ts_def)]
12381244
pub enum TSModuleDeclarationName<'a> {
12391245
Identifier(BindingIdentifier<'a>) = 0,
12401246
StringLiteral(StringLiteral<'a>) = 1,
@@ -1243,12 +1249,12 @@ pub enum TSModuleDeclarationName<'a> {
12431249
#[ast(visit)]
12441250
#[derive(Debug)]
12451251
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, GetAddress, ContentEq, ESTree)]
1252+
#[estree(no_ts_def)]
12461253
pub enum TSModuleDeclarationBody<'a> {
12471254
TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 0,
12481255
TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
12491256
}
12501257

1251-
// See serializer in serialize.rs
12521258
#[ast(visit)]
12531259
#[derive(Debug)]
12541260
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, ContentEq, ESTree)]

crates/oxc_ast/src/generated/derive_estree.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3034,16 +3034,7 @@ impl ESTree for TSTypePredicateName<'_> {
30343034

30353035
impl ESTree for TSModuleDeclaration<'_> {
30363036
fn serialize<S: Serializer>(&self, serializer: S) {
3037-
let mut state = serializer.serialize_struct();
3038-
state.serialize_field("type", &JsonSafeString("TSModuleDeclaration"));
3039-
state.serialize_field("start", &self.span.start);
3040-
state.serialize_field("end", &self.span.end);
3041-
state.serialize_field("id", &self.id);
3042-
state.serialize_field("body", &self.body);
3043-
state.serialize_field("kind", &self.kind);
3044-
state.serialize_field("declare", &self.declare);
3045-
state.serialize_field("global", &crate::serialize::TSModuleDeclarationGlobal(self));
3046-
state.end();
3037+
crate::serialize::TSModuleDeclarationConverter(self).serialize(serializer)
30473038
}
30483039
}
30493040

crates/oxc_ast/src/serialize.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,135 @@ impl ESTree for ExpressionStatementDirective<'_, '_> {
11251125
}
11261126
}
11271127

1128+
/// Converter for `TSModuleDeclaration`.
1129+
///
1130+
/// Our AST represents `module X.Y.Z {}` as 3 x nested `TSModuleDeclaration`s.
1131+
/// TS-ESTree represents it as a single `TSModuleDeclaration`,
1132+
/// with a nested tree of `TSQualifiedName`s as `id`.
1133+
#[ast_meta]
1134+
#[estree(raw_deser = "
1135+
const kind = DESER[TSModuleDeclarationKind](POS_OFFSET.kind),
1136+
global = kind === 'global',
1137+
start = DESER[u32](POS_OFFSET.span.start),
1138+
end = DESER[u32](POS_OFFSET.span.end),
1139+
declare = DESER[bool](POS_OFFSET.declare);
1140+
let id = DESER[TSModuleDeclarationName](POS_OFFSET.id),
1141+
body = DESER[Option<TSModuleDeclarationBody>](POS_OFFSET.body);
1142+
1143+
// Flatten `body`, and nest `id`
1144+
if (body !== null && body.type === 'TSModuleDeclaration') {
1145+
id = {
1146+
type: 'TSQualifiedName',
1147+
start: body.id.start,
1148+
end: id.end,
1149+
left: body.id,
1150+
right: id,
1151+
};
1152+
body = Object.hasOwn(body, 'body') ? body.body : null;
1153+
}
1154+
1155+
// Skip `body` field if `null`
1156+
const node = body === null
1157+
? { type: 'TSModuleDeclaration', start, end, id, kind, declare, global }
1158+
: { type: 'TSModuleDeclaration', start, end, id, body, kind, declare, global };
1159+
node
1160+
")]
1161+
pub struct TSModuleDeclarationConverter<'a, 'b>(pub &'b TSModuleDeclaration<'a>);
1162+
1163+
impl ESTree for TSModuleDeclarationConverter<'_, '_> {
1164+
fn serialize<S: Serializer>(&self, serializer: S) {
1165+
let module = self.0;
1166+
1167+
let mut state = serializer.serialize_struct();
1168+
state.serialize_field("type", &JsonSafeString("TSModuleDeclaration"));
1169+
state.serialize_field("start", &module.span.start);
1170+
state.serialize_field("end", &module.span.end);
1171+
1172+
match &module.body {
1173+
Some(TSModuleDeclarationBody::TSModuleDeclaration(inner_module)) => {
1174+
// Nested modules e.g. `module X.Y.Z {}`.
1175+
// Collect all IDs in a `Vec`, in order they appear (i.e. [`X`, `Y`, `Z`]).
1176+
// Also get the inner `TSModuleBlock`.
1177+
let mut parts = Vec::with_capacity(4);
1178+
1179+
let TSModuleDeclarationName::Identifier(id) = &module.id else { unreachable!() };
1180+
parts.push(id);
1181+
1182+
let mut body = None;
1183+
let mut inner_module = inner_module.as_ref();
1184+
loop {
1185+
let TSModuleDeclarationName::Identifier(id) = &inner_module.id else {
1186+
unreachable!()
1187+
};
1188+
parts.push(id);
1189+
1190+
match &inner_module.body {
1191+
Some(TSModuleDeclarationBody::TSModuleDeclaration(inner_inner_module)) => {
1192+
inner_module = inner_inner_module.as_ref();
1193+
}
1194+
Some(TSModuleDeclarationBody::TSModuleBlock(block)) => {
1195+
body = Some(block.as_ref());
1196+
break;
1197+
}
1198+
None => break,
1199+
}
1200+
}
1201+
1202+
// Serialize `parts` as a nested tree of `TSQualifiedName`s
1203+
state.serialize_field("id", &TSModuleDeclarationIdParts(&parts));
1204+
1205+
// Skip `body` field if it's `None`
1206+
if let Some(body) = body {
1207+
state.serialize_field("body", body);
1208+
}
1209+
}
1210+
Some(TSModuleDeclarationBody::TSModuleBlock(block)) => {
1211+
// No nested modules.
1212+
// Serialize as usual, with `id` being either a `BindingIdentifier` or `StringLiteral`.
1213+
state.serialize_field("id", &module.id);
1214+
state.serialize_field("body", block);
1215+
}
1216+
None => {
1217+
// No body. Skip `body` field.
1218+
state.serialize_field("id", &module.id);
1219+
}
1220+
}
1221+
1222+
state.serialize_field("kind", &module.kind);
1223+
state.serialize_field("declare", &module.declare);
1224+
state.serialize_field("global", &crate::serialize::TSModuleDeclarationGlobal(module));
1225+
state.end();
1226+
}
1227+
}
1228+
1229+
struct TSModuleDeclarationIdParts<'a, 'b>(&'b [&'b BindingIdentifier<'a>]);
1230+
1231+
impl ESTree for TSModuleDeclarationIdParts<'_, '_> {
1232+
fn serialize<S: Serializer>(&self, serializer: S) {
1233+
let parts = self.0;
1234+
assert!(!parts.is_empty());
1235+
1236+
let span_start = parts[0].span.start;
1237+
let (&last, rest) = parts.split_last().unwrap();
1238+
1239+
let mut state = serializer.serialize_struct();
1240+
state.serialize_field("type", &JsonSafeString("TSQualifiedName"));
1241+
state.serialize_field("start", &span_start);
1242+
state.serialize_field("end", &last.span.end);
1243+
1244+
if rest.len() == 1 {
1245+
// Only one part remaining (e.g. `X`). Serialize as `Identifier`.
1246+
state.serialize_field("left", &rest[0]);
1247+
} else {
1248+
// Multiple parts remaining (e.g. `X.Y`). Recurse to serialize as `TSQualifiedName`.
1249+
state.serialize_field("left", &TSModuleDeclarationIdParts(rest));
1250+
}
1251+
1252+
state.serialize_field("right", last);
1253+
state.end();
1254+
}
1255+
}
1256+
11281257
/// Serializer for `global` field of `TSModuleDeclaration`.
11291258
#[ast_meta]
11301259
#[estree(ts_type = "boolean", raw_deser = "THIS.kind === 'global'")]

napi/parser/deserialize-js.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,17 +1761,31 @@ function deserializeTSTypePredicate(pos) {
17611761
}
17621762

17631763
function deserializeTSModuleDeclaration(pos) {
1764-
const kind = deserializeTSModuleDeclarationKind(pos + 80);
1765-
return {
1766-
type: 'TSModuleDeclaration',
1767-
start: deserializeU32(pos),
1768-
end: deserializeU32(pos + 4),
1769-
id: deserializeTSModuleDeclarationName(pos + 8),
1770-
body: deserializeOptionTSModuleDeclarationBody(pos + 64),
1771-
kind,
1772-
declare: deserializeBool(pos + 81),
1773-
global: kind === 'global',
1774-
};
1764+
const kind = deserializeTSModuleDeclarationKind(pos + 80),
1765+
global = kind === 'global',
1766+
start = deserializeU32(pos),
1767+
end = deserializeU32(pos + 4),
1768+
declare = deserializeBool(pos + 81);
1769+
let id = deserializeTSModuleDeclarationName(pos + 8),
1770+
body = deserializeOptionTSModuleDeclarationBody(pos + 64);
1771+
1772+
// Flatten `body`, and nest `id`
1773+
if (body !== null && body.type === 'TSModuleDeclaration') {
1774+
id = {
1775+
type: 'TSQualifiedName',
1776+
start: body.id.start,
1777+
end: id.end,
1778+
left: body.id,
1779+
right: id,
1780+
};
1781+
body = Object.hasOwn(body, 'body') ? body.body : null;
1782+
}
1783+
1784+
// Skip `body` field if `null`
1785+
const node = body === null
1786+
? { type: 'TSModuleDeclaration', start, end, id, kind, declare, global }
1787+
: { type: 'TSModuleDeclaration', start, end, id, body, kind, declare, global };
1788+
return node;
17751789
}
17761790

17771791
function deserializeTSModuleBlock(pos) {

napi/parser/deserialize-ts.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,17 +1913,31 @@ function deserializeTSTypePredicate(pos) {
19131913
}
19141914

19151915
function deserializeTSModuleDeclaration(pos) {
1916-
const kind = deserializeTSModuleDeclarationKind(pos + 80);
1917-
return {
1918-
type: 'TSModuleDeclaration',
1919-
start: deserializeU32(pos),
1920-
end: deserializeU32(pos + 4),
1921-
id: deserializeTSModuleDeclarationName(pos + 8),
1922-
body: deserializeOptionTSModuleDeclarationBody(pos + 64),
1923-
kind,
1924-
declare: deserializeBool(pos + 81),
1925-
global: kind === 'global',
1926-
};
1916+
const kind = deserializeTSModuleDeclarationKind(pos + 80),
1917+
global = kind === 'global',
1918+
start = deserializeU32(pos),
1919+
end = deserializeU32(pos + 4),
1920+
declare = deserializeBool(pos + 81);
1921+
let id = deserializeTSModuleDeclarationName(pos + 8),
1922+
body = deserializeOptionTSModuleDeclarationBody(pos + 64);
1923+
1924+
// Flatten `body`, and nest `id`
1925+
if (body !== null && body.type === 'TSModuleDeclaration') {
1926+
id = {
1927+
type: 'TSQualifiedName',
1928+
start: body.id.start,
1929+
end: id.end,
1930+
left: body.id,
1931+
right: id,
1932+
};
1933+
body = Object.hasOwn(body, 'body') ? body.body : null;
1934+
}
1935+
1936+
// Skip `body` field if `null`
1937+
const node = body === null
1938+
? { type: 'TSModuleDeclaration', start, end, id, kind, declare, global }
1939+
: { type: 'TSModuleDeclaration', start, end, id, body, kind, declare, global };
1940+
return node;
19271941
}
19281942

19291943
function deserializeTSModuleBlock(pos) {

npm/oxc-types/types.d.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,19 +1316,15 @@ export type TSTypePredicateName = IdentifierName | TSThisType;
13161316

13171317
export interface TSModuleDeclaration extends Span {
13181318
type: 'TSModuleDeclaration';
1319-
id: TSModuleDeclarationName;
1320-
body: TSModuleDeclarationBody | null;
1319+
id: BindingIdentifier | StringLiteral | TSQualifiedName;
1320+
body: TSModuleBlock | null;
13211321
kind: TSModuleDeclarationKind;
13221322
declare: boolean;
13231323
global: boolean;
13241324
}
13251325

13261326
export type TSModuleDeclarationKind = 'global' | 'module' | 'namespace';
13271327

1328-
export type TSModuleDeclarationName = BindingIdentifier | StringLiteral;
1329-
1330-
export type TSModuleDeclarationBody = TSModuleDeclaration | TSModuleBlock;
1331-
13321328
export interface TSModuleBlock extends Span {
13331329
type: 'TSModuleBlock';
13341330
body: Array<Directive | Statement>;

0 commit comments

Comments
 (0)