diff --git a/crates/oxc_ast/src/ast/ts.rs b/crates/oxc_ast/src/ast/ts.rs index 8e7e0f108e8e7..c352b862ff4aa 100644 --- a/crates/oxc_ast/src/ast/ts.rs +++ b/crates/oxc_ast/src/ast/ts.rs @@ -901,6 +901,7 @@ pub enum TSAccessibility { #[plural(TSClassImplementsList)] pub struct TSClassImplements<'a> { pub span: Span, + #[estree(via = TSClassImplementsExpression)] pub expression: TSTypeName<'a>, 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 2816c4ea667ee..bc58049be2d37 100644 --- a/crates/oxc_ast/src/generated/derive_estree.rs +++ b/crates/oxc_ast/src/generated/derive_estree.rs @@ -2851,7 +2851,7 @@ impl ESTree for TSClassImplements<'_> { state.serialize_field("type", &JsonSafeString("TSClassImplements")); state.serialize_field("start", &self.span.start); state.serialize_field("end", &self.span.end); - state.serialize_field("expression", &self.expression); + state.serialize_field("expression", &crate::serialize::TSClassImplementsExpression(self)); state.serialize_field("typeArguments", &self.type_arguments); state.end(); } diff --git a/crates/oxc_ast/src/serialize.rs b/crates/oxc_ast/src/serialize.rs index 4e100675f9080..07454ce150e5c 100644 --- a/crates/oxc_ast/src/serialize.rs +++ b/crates/oxc_ast/src/serialize.rs @@ -1342,6 +1342,79 @@ impl ESTree for TSTypeNameIdentifierReference<'_, '_> { } } +/// Serializer for `expression` field of `TSClassImplements`. +/// +/// Our AST represents `X.Y` in `class C implements X.Y {}` as a `TSQualifiedName`. +/// TS-ESTree represents `X.Y` as a `MemberExpression`. +/// +/// Where there are more parts e.g. `class C implements X.Y.Z {}`, the `TSQualifiedName`s (Oxc) +/// or `MemberExpression`s (TS-ESTree) are nested. +#[ast_meta] +#[estree( + ts_type = "IdentifierReference | ThisExpression | MemberExpression", + raw_deser = " + let expression = DESER[TSTypeName](POS_OFFSET.expression); + if (expression.type === 'TSQualifiedName') { + let parent = expression = { + type: 'MemberExpression', + start: expression.start, + end: expression.end, + object: expression.left, + property: expression.right, + computed: false, + optional: false, + }; + + while (parent.object.type === 'TSQualifiedName') { + const object = parent.object; + parent = parent.object = { + type: 'MemberExpression', + start: object.start, + end: object.end, + object: object.left, + property: object.right, + computed: false, + optional: false, + }; + } + } + expression + " +)] +pub struct TSClassImplementsExpression<'a, 'b>(pub &'b TSClassImplements<'a>); + +impl ESTree for TSClassImplementsExpression<'_, '_> { + #[inline] // Because it just delegates + fn serialize(&self, serializer: S) { + TSTypeNameAsMemberExpression(&self.0.expression).serialize(serializer); + } +} + +struct TSTypeNameAsMemberExpression<'a, 'b>(&'b TSTypeName<'a>); + +impl ESTree for TSTypeNameAsMemberExpression<'_, '_> { + fn serialize(&self, serializer: S) { + match self.0 { + TSTypeName::IdentifierReference(ident) => { + TSTypeNameIdentifierReference(ident).serialize(serializer); + } + TSTypeName::QualifiedName(name) => { + // Convert to `TSQualifiedName` to `MemberExpression`. + // Recursively convert `left` to `MemberExpression` too if it's a `TSQualifiedName`. + let mut state = serializer.serialize_struct(); + state.serialize_field("type", &JsonSafeString("MemberExpression")); + state.serialize_field("start", &name.span.start); + state.serialize_field("end", &name.span.end); + state.serialize_field("object", &TSTypeNameAsMemberExpression(&name.left)); + state.serialize_field("property", &name.right); + state.serialize_field("computed", &false); + state.serialize_field("optional", &false); + state.end(); + } + } + } +} + /// Serializer for `params` field of `TSCallSignatureDeclaration`. /// /// These add `this_param` to start of the `params` array. diff --git a/napi/parser/deserialize-js.js b/napi/parser/deserialize-js.js index d2e6112038e4b..e281ab2c7172f 100644 --- a/napi/parser/deserialize-js.js +++ b/napi/parser/deserialize-js.js @@ -1622,11 +1622,36 @@ function deserializeTSTypeAliasDeclaration(pos) { } function deserializeTSClassImplements(pos) { + let expression = deserializeTSTypeName(pos + 8); + if (expression.type === 'TSQualifiedName') { + let parent = expression = { + type: 'MemberExpression', + start: expression.start, + end: expression.end, + object: expression.left, + property: expression.right, + computed: false, + optional: false, + }; + + while (parent.object.type === 'TSQualifiedName') { + const object = parent.object; + parent = parent.object = { + type: 'MemberExpression', + start: object.start, + end: object.end, + object: object.left, + property: object.right, + computed: false, + optional: false, + }; + } + } return { type: 'TSClassImplements', start: deserializeU32(pos), end: deserializeU32(pos + 4), - expression: deserializeTSTypeName(pos + 8), + expression, typeArguments: deserializeOptionBoxTSTypeParameterInstantiation(pos + 24), }; } diff --git a/napi/parser/deserialize-ts.js b/napi/parser/deserialize-ts.js index cbdc294efb77d..4fcb262486767 100644 --- a/napi/parser/deserialize-ts.js +++ b/napi/parser/deserialize-ts.js @@ -1774,11 +1774,36 @@ function deserializeTSTypeAliasDeclaration(pos) { } function deserializeTSClassImplements(pos) { + let expression = deserializeTSTypeName(pos + 8); + if (expression.type === 'TSQualifiedName') { + let parent = expression = { + type: 'MemberExpression', + start: expression.start, + end: expression.end, + object: expression.left, + property: expression.right, + computed: false, + optional: false, + }; + + while (parent.object.type === 'TSQualifiedName') { + const object = parent.object; + parent = parent.object = { + type: 'MemberExpression', + start: object.start, + end: object.end, + object: object.left, + property: object.right, + computed: false, + optional: false, + }; + } + } return { type: 'TSClassImplements', start: deserializeU32(pos), end: deserializeU32(pos + 4), - expression: deserializeTSTypeName(pos + 8), + expression, typeArguments: deserializeOptionBoxTSTypeParameterInstantiation(pos + 24), }; } diff --git a/npm/oxc-types/types.d.ts b/npm/oxc-types/types.d.ts index 4c4a96830c26e..ec489c5c45dbf 100644 --- a/npm/oxc-types/types.d.ts +++ b/npm/oxc-types/types.d.ts @@ -1216,7 +1216,7 @@ export type TSAccessibility = 'private' | 'protected' | 'public'; export interface TSClassImplements extends Span { type: 'TSClassImplements'; - expression: TSTypeName; + expression: IdentifierReference | ThisExpression | MemberExpression; typeArguments: TSTypeParameterInstantiation | null; } diff --git a/tasks/coverage/snapshots/estree_typescript.snap b/tasks/coverage/snapshots/estree_typescript.snap index b75a18f5e6c37..336bae1cfc2b7 100644 --- a/tasks/coverage/snapshots/estree_typescript.snap +++ b/tasks/coverage/snapshots/estree_typescript.snap @@ -2,7 +2,7 @@ commit: 15392346 estree_typescript Summary: AST Parsed : 11245/11404 (98.61%) -Positive Passed: 11074/11404 (97.11%) +Positive Passed: 11091/11404 (97.26%) Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/ClassDeclarationWithInvalidConstOnPropertyDeclaration.ts A class member cannot have the 'const' keyword. @@ -46,18 +46,12 @@ Mismatch: tasks/coverage/typescript/tests/cases/compiler/bigintWithLib.ts Mismatch: tasks/coverage/typescript/tests/cases/compiler/bigintWithoutLib.ts -Mismatch: tasks/coverage/typescript/tests/cases/compiler/bluebirdStaticThis.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/classExpressionPropertyModifiers.ts Expected a semicolon or an implicit semicolon after a statement, but found none Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/classHeritageWithTrailingSeparator.ts Expected `{` but found `EOF` -Mismatch: tasks/coverage/typescript/tests/cases/compiler/classdecl.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/complicatedPrivacy.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/constDeclarations-errors.ts Missing initializer in const declaration @@ -78,18 +72,6 @@ Mismatch: tasks/coverage/typescript/tests/cases/compiler/controlFlowInstanceofWi Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/convertKeywordsYes.ts Classes can't have a field named 'constructor' -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declFileGenericType2.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declFileModuleContinuation.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declFileWithInternalModuleNameConflictsInExtendsClause1.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declFileWithInternalModuleNameConflictsInExtendsClause2.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declFileWithInternalModuleNameConflictsInExtendsClause3.ts - -Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitHasTypesRefOnNamespaceUse.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/declareAlreadySeen.ts declare' modifier already seen. @@ -181,8 +163,6 @@ A rest parameter cannot be optional Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/fileWithNextLine3.ts A 'return' statement can only be used within a function body. -Mismatch: tasks/coverage/typescript/tests/cases/compiler/genericClassImplementingGenericInterfaceFromAnotherModule.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/illegalModifiersOnClassElements.ts Expected a semicolon or an implicit semicolon after a statement, but found none @@ -222,8 +202,6 @@ Unexpected token Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/indexerSignatureWithRestParam.ts Unexpected token -Mismatch: tasks/coverage/typescript/tests/cases/compiler/interfaceDeclaration3.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/jsFileCompilationAbstractModifier.ts Expected a semicolon or an implicit semicolon after a statement, but found none @@ -290,8 +268,6 @@ Expected a semicolon or an implicit semicolon after a statement, but found none Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/jsFileCompilationWithoutJsExtensions.ts Expected a semicolon or an implicit semicolon after a statement, but found none -Mismatch: tasks/coverage/typescript/tests/cases/compiler/jsxElementClassTooManyParams.tsx - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/letDeclarations-invalidContexts.ts Expected a semicolon or an implicit semicolon after a statement, but found none @@ -355,13 +331,9 @@ Type parameters cannot appear on a constructor declaration serde_json::from_str(oxc_json) Error: tasks/coverage/typescript/tests/cases/compiler/parsingDeepParenthensizedExpression.ts recursion limit exceeded at line 3334 column 269 -Mismatch: tasks/coverage/typescript/tests/cases/compiler/privacyClassImplementsClauseDeclFile.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/privacyImportParseErrors.ts 'export' modifier cannot be used here. -Mismatch: tasks/coverage/typescript/tests/cases/compiler/recursiveClassReferenceTest.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/regularExpressionScanning.ts Unexpected flag a in regular expression literal @@ -396,8 +368,6 @@ Unexpected token Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/staticPrototypeProperty.ts Classes may not have a static property named prototype -Mismatch: tasks/coverage/typescript/tests/cases/compiler/strictModeReservedWordInClassDeclaration.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/thisAssignmentInNamespaceDeclaration1.ts Expected a semicolon or an implicit semicolon after a statement, but found none @@ -657,8 +627,6 @@ Expected `,` but found `Identifier` Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/externalModules/typeOnly/grammarErrors.ts Expected `from` but found `Identifier` -Mismatch: tasks/coverage/typescript/tests/cases/conformance/externalModules/typeOnly/implementsClause.ts - Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/externalModules/typeOnly/importSpecifiers_js.ts Expected `,` but found `Identifier` @@ -680,8 +648,6 @@ Expected `from` but found `=` Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/jsdoc/declarations/jsDeclarationsInterfaces.ts Unexpected token -Mismatch: tasks/coverage/typescript/tests/cases/conformance/jsx/inline/inlineJsxFactoryDeclarationsLocalTypes.tsx - Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/jsx/jsxCheckJsxNoTypeArgumentsAllowed.tsx Expected `>` but found `<`