From 47e2c593a945fc8e468f360d0fbb295d4bbc3f81 Mon Sep 17 00:00:00 2001 From: camc314 <18101008+camc314@users.noreply.github.com> Date: Tue, 5 Aug 2025 05:55:42 +0000 Subject: [PATCH] fix(estree): fix serialization of `TSImportTypeQualifier` (#12801) --- crates/oxc_ast/src/ast/ts.rs | 1 + crates/oxc_ast/src/generated/derive_estree.rs | 5 +- crates/oxc_ast/src/serialize/ts.rs | 115 ++++++++++++++++++ napi/parser/generated/deserialize/js.js | 49 +++++++- napi/parser/generated/deserialize/ts.js | 49 +++++++- npm/oxc-types/types.d.ts | 2 +- .../coverage/snapshots/estree_typescript.snap | 10 +- 7 files changed, 218 insertions(+), 13 deletions(-) diff --git a/crates/oxc_ast/src/ast/ts.rs b/crates/oxc_ast/src/ast/ts.rs index 4b019a7f7832f..c26c658a20934 100644 --- a/crates/oxc_ast/src/ast/ts.rs +++ b/crates/oxc_ast/src/ast/ts.rs @@ -1359,6 +1359,7 @@ pub struct TSImportType<'a> { pub span: Span, pub argument: TSType<'a>, pub options: Option>>, + #[estree(via = TSImportTypeQualifierConverter)] pub qualifier: Option>, pub type_arguments: Option>>, } diff --git a/crates/oxc_ast/src/generated/derive_estree.rs b/crates/oxc_ast/src/generated/derive_estree.rs index dc33695d7ab11..7423af2672cb7 100644 --- a/crates/oxc_ast/src/generated/derive_estree.rs +++ b/crates/oxc_ast/src/generated/derive_estree.rs @@ -2977,7 +2977,10 @@ impl ESTree for TSImportType<'_> { state.serialize_field("type", &JsonSafeString("TSImportType")); state.serialize_field("argument", &self.argument); state.serialize_field("options", &self.options); - state.serialize_field("qualifier", &self.qualifier); + state.serialize_field( + "qualifier", + &crate::serialize::ts::TSImportTypeQualifierConverter(self), + ); state.serialize_field("typeArguments", &self.type_arguments); state.serialize_span(self.span); state.end(); diff --git a/crates/oxc_ast/src/serialize/ts.rs b/crates/oxc_ast/src/serialize/ts.rs index 512b038d24f6a..406aaa3ca430e 100644 --- a/crates/oxc_ast/src/serialize/ts.rs +++ b/crates/oxc_ast/src/serialize/ts.rs @@ -401,3 +401,118 @@ impl ESTree for TSFunctionTypeParams<'_, '_> { Concat2(&fn_type.this_param, fn_type.params.as_ref()).serialize(serializer); } } + +/// Serializer for `qualifier` field of `TSImportType`. +/// +/// Our AST represents the qualifier as `TSImportTypeQualifier` with `IdentifierName` nodes. +/// TS-ESTree represents it as `TSQualifiedName` with `Identifier` nodes. +#[ast_meta] +#[estree( + ts_type = "TSQualifiedName | IdentifierName | null", + raw_deser = " + let qualifier = DESER[Option](POS_OFFSET.qualifier); + if (qualifier !== null) { + if (qualifier.type === 'IdentifierName') { + qualifier = { + type: 'Identifier', + decorators: [], + name: qualifier.name, + optional: false, + typeAnnotation: null, + start: qualifier.start, + end: qualifier.end, + }; + } else if (qualifier.type === 'TSImportTypeQualifiedName') { + // Convert TSImportTypeQualifiedName to TSQualifiedName + const convertQualifier = (q) => { + if (q.type === 'IdentifierName') { + return { + type: 'Identifier', + decorators: [], + name: q.name, + optional: false, + typeAnnotation: null, + start: q.start, + end: q.end, + }; + } else if (q.type === 'TSImportTypeQualifiedName') { + return { + type: 'TSQualifiedName', + left: convertQualifier(q.left), + right: { + type: 'Identifier', + decorators: [], + name: q.right.name, + optional: false, + typeAnnotation: null, + start: q.right.start, + end: q.right.end, + }, + start: q.start, + end: q.end, + }; + } + return q; + }; + qualifier = convertQualifier(qualifier); + } + } + qualifier + " +)] +pub struct TSImportTypeQualifierConverter<'a, 'b>(pub &'b TSImportType<'a>); + +impl ESTree for TSImportTypeQualifierConverter<'_, '_> { + fn serialize(&self, serializer: S) { + match &self.0.qualifier { + None => None::<()>.serialize(serializer), + Some(qualifier) => { + TSImportTypeQualifierAsQualifiedName(qualifier).serialize(serializer); + } + } + } +} + +struct TSImportTypeQualifierAsQualifiedName<'a, 'b>(&'b TSImportTypeQualifier<'a>); + +impl ESTree for TSImportTypeQualifierAsQualifiedName<'_, '_> { + fn serialize(&self, serializer: S) { + match self.0 { + TSImportTypeQualifier::Identifier(ident) => { + // Convert IdentifierName to Identifier + let mut state = serializer.serialize_struct(); + state.serialize_field("type", &JsonSafeString("Identifier")); + state.serialize_field("decorators", &Vec::::new().as_slice()); + state.serialize_field("name", &ident.name); + state.serialize_field("optional", &false); + state.serialize_field("typeAnnotation", &None::<()>); + state.serialize_span(ident.span); + state.end(); + } + TSImportTypeQualifier::QualifiedName(name) => { + // Convert TSImportTypeQualifiedName to TSQualifiedName + let mut state = serializer.serialize_struct(); + state.serialize_field("type", &JsonSafeString("TSQualifiedName")); + state.serialize_field("left", &TSImportTypeQualifierAsQualifiedName(&name.left)); + state.serialize_field("right", &IdentifierAsIdentifier(&name.right)); + state.serialize_span(name.span); + state.end(); + } + } + } +} + +struct IdentifierAsIdentifier<'a, 'b>(&'b IdentifierName<'a>); + +impl ESTree for IdentifierAsIdentifier<'_, '_> { + fn serialize(&self, serializer: S) { + let mut state = serializer.serialize_struct(); + state.serialize_field("type", &JsonSafeString("Identifier")); + state.serialize_field("decorators", &Vec::::new().as_slice()); + state.serialize_field("name", &self.0.name); + state.serialize_field("optional", &false); + state.serialize_field("typeAnnotation", &None::<()>); + state.serialize_span(self.0.span); + state.end(); + } +} diff --git a/napi/parser/generated/deserialize/js.js b/napi/parser/generated/deserialize/js.js index 88bf5b7cd6215..1097de43215ac 100644 --- a/napi/parser/generated/deserialize/js.js +++ b/napi/parser/generated/deserialize/js.js @@ -1874,11 +1874,58 @@ function deserializeTSTypeQuery(pos) { } function deserializeTSImportType(pos) { + let qualifier = deserializeOptionTSImportTypeQualifier(pos + 32); + if (qualifier !== null) { + if (qualifier.type === 'IdentifierName') { + qualifier = { + type: 'Identifier', + decorators: [], + name: qualifier.name, + optional: false, + typeAnnotation: null, + start: qualifier.start, + end: qualifier.end, + }; + } else if (qualifier.type === 'TSImportTypeQualifiedName') { + // Convert TSImportTypeQualifiedName to TSQualifiedName + const convertQualifier = (q) => { + if (q.type === 'IdentifierName') { + return { + type: 'Identifier', + decorators: [], + name: q.name, + optional: false, + typeAnnotation: null, + start: q.start, + end: q.end, + }; + } else if (q.type === 'TSImportTypeQualifiedName') { + return { + type: 'TSQualifiedName', + left: convertQualifier(q.left), + right: { + type: 'Identifier', + decorators: [], + name: q.right.name, + optional: false, + typeAnnotation: null, + start: q.right.start, + end: q.right.end, + }, + start: q.start, + end: q.end, + }; + } + return q; + }; + qualifier = convertQualifier(qualifier); + } + } return { type: 'TSImportType', argument: deserializeTSType(pos + 8), options: deserializeOptionBoxObjectExpression(pos + 24), - qualifier: deserializeOptionTSImportTypeQualifier(pos + 32), + qualifier, typeArguments: deserializeOptionBoxTSTypeParameterInstantiation(pos + 48), start: deserializeU32(pos), end: deserializeU32(pos + 4), diff --git a/napi/parser/generated/deserialize/ts.js b/napi/parser/generated/deserialize/ts.js index d28ad74dc734b..9a5835c05b991 100644 --- a/napi/parser/generated/deserialize/ts.js +++ b/napi/parser/generated/deserialize/ts.js @@ -2005,11 +2005,58 @@ function deserializeTSTypeQuery(pos) { } function deserializeTSImportType(pos) { + let qualifier = deserializeOptionTSImportTypeQualifier(pos + 32); + if (qualifier !== null) { + if (qualifier.type === 'IdentifierName') { + qualifier = { + type: 'Identifier', + decorators: [], + name: qualifier.name, + optional: false, + typeAnnotation: null, + start: qualifier.start, + end: qualifier.end, + }; + } else if (qualifier.type === 'TSImportTypeQualifiedName') { + // Convert TSImportTypeQualifiedName to TSQualifiedName + const convertQualifier = (q) => { + if (q.type === 'IdentifierName') { + return { + type: 'Identifier', + decorators: [], + name: q.name, + optional: false, + typeAnnotation: null, + start: q.start, + end: q.end, + }; + } else if (q.type === 'TSImportTypeQualifiedName') { + return { + type: 'TSQualifiedName', + left: convertQualifier(q.left), + right: { + type: 'Identifier', + decorators: [], + name: q.right.name, + optional: false, + typeAnnotation: null, + start: q.right.start, + end: q.right.end, + }, + start: q.start, + end: q.end, + }; + } + return q; + }; + qualifier = convertQualifier(qualifier); + } + } return { type: 'TSImportType', argument: deserializeTSType(pos + 8), options: deserializeOptionBoxObjectExpression(pos + 24), - qualifier: deserializeOptionTSImportTypeQualifier(pos + 32), + qualifier, typeArguments: deserializeOptionBoxTSTypeParameterInstantiation(pos + 48), start: deserializeU32(pos), end: deserializeU32(pos + 4), diff --git a/npm/oxc-types/types.d.ts b/npm/oxc-types/types.d.ts index 0a9f3caeec335..7c0d1cc2426c0 100644 --- a/npm/oxc-types/types.d.ts +++ b/npm/oxc-types/types.d.ts @@ -1351,7 +1351,7 @@ export interface TSImportType extends Span { type: 'TSImportType'; argument: TSType; options: ObjectExpression | null; - qualifier: TSImportTypeQualifier | null; + qualifier: TSQualifiedName | IdentifierName | null; typeArguments: TSTypeParameterInstantiation | null; } diff --git a/tasks/coverage/snapshots/estree_typescript.snap b/tasks/coverage/snapshots/estree_typescript.snap index 9735fded021a6..199a1b62582d0 100644 --- a/tasks/coverage/snapshots/estree_typescript.snap +++ b/tasks/coverage/snapshots/estree_typescript.snap @@ -2,18 +2,10 @@ commit: 81c95189 estree_typescript Summary: AST Parsed : 8575/8575 (100.00%) -Positive Passed: 8568/8575 (99.92%) -Mismatch: tasks/coverage/typescript/tests/cases/compiler/importTypeTypeofClassStaticLookup.ts - +Positive Passed: 8572/8575 (99.97%) Mismatch: tasks/coverage/typescript/tests/cases/conformance/jsx/jsxReactTestSuite.tsx Mismatch: tasks/coverage/typescript/tests/cases/conformance/jsx/tsxReactEmitEntities.tsx Mismatch: tasks/coverage/typescript/tests/cases/conformance/jsx/tsxReactEmitNesting.tsx -Mismatch: tasks/coverage/typescript/tests/cases/conformance/types/import/importTypeAmbient.ts - -Mismatch: tasks/coverage/typescript/tests/cases/conformance/types/import/importTypeGenericTypes.ts - -Mismatch: tasks/coverage/typescript/tests/cases/conformance/types/import/importTypeLocal.ts -