From 53024dc96c87e33b024382f5b41c5f3638de5184 Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Mon, 18 Nov 2024 10:41:53 +0100 Subject: [PATCH] Implemented support for ClassExpression Implemented ClassExpression via JS.StatementExpression as an ClassDeclaration wrapper. Refactored visitQualifiedName and generalized visitTypeReference --- openrewrite/src/javascript/parser.ts | 80 +++++++++++------- .../test/javascript/parser/arrow.test.ts | 56 +++++++++++++ .../test/javascript/parser/class.test.ts | 83 +++++++++++++++---- .../test/javascript/parser/function.test.ts | 11 +++ 4 files changed, 186 insertions(+), 44 deletions(-) diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index b6062fb8..96fa3bdf 100644 --- a/openrewrite/src/javascript/parser.ts +++ b/openrewrite/src/javascript/parser.ts @@ -406,7 +406,7 @@ export class JavaScriptParserVisitor { null, new J.Block( randomId(), - this.prefix(node.getChildren().find(v => v.kind === ts.SyntaxKind.OpenBraceToken)!), + this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), Markers.EMPTY, new JRightPadded(false, Space.EMPTY, Markers.EMPTY), node.members.map((ce : ts.ClassElement) => new JRightPadded( @@ -420,7 +420,7 @@ export class JavaScriptParserVisitor { ); } - private mapExtends(node: ts.ClassDeclaration): JLeftPadded | null { + private mapExtends(node: ts.ClassDeclaration | ts.ClassExpression): JLeftPadded | null { if (node.heritageClauses == undefined || node.heritageClauses.length == 0) { return null; } @@ -452,7 +452,7 @@ export class JavaScriptParserVisitor { return null; } - private mapImplements(node: ts.ClassDeclaration): JContainer | null { + private mapImplements(node: ts.ClassDeclaration | ts.ClassExpression): JContainer | null { if (node.heritageClauses == undefined || node.heritageClauses.length == 0) { return null; } @@ -590,28 +590,14 @@ export class JavaScriptParserVisitor { } visitQualifiedName(node: ts.QualifiedName) { - const fieldAccess = new J.FieldAccess( + return new J.FieldAccess( randomId(), this.prefix(node), Markers.EMPTY, this.visit(node.left), - new JLeftPadded(this.suffix(node.left), this.convert(node.right), Markers.EMPTY), + this.leftPadded(this.suffix(node.left), this.convert(node.right)), this.mapType(node) ); - - const parent = node.parent as ts.TypeReferenceNode; - if (parent.typeArguments) { - return new J.ParameterizedType( - randomId(), - this.prefix(parent), - Markers.EMPTY, - fieldAccess, - this.mapTypeArguments(this.suffix(parent.typeName), parent.typeArguments), - this.mapType(parent) - ) - } else { - return fieldAccess; - } } visitComputedPropertyName(node: ts.ComputedPropertyName) { @@ -1051,11 +1037,14 @@ export class JavaScriptParserVisitor { visitTypeReference(node: ts.TypeReferenceNode) { if (node.typeArguments) { - // Temporary check for supported constructions with type arguments - if (ts.isQualifiedName(node.typeName)) { - return this.visit(node.typeName); - } - return this.visitUnknown(node); + return new J.ParameterizedType( + randomId(), + this.prefix(node), + Markers.EMPTY, + this.visit(node.typeName), + this.mapTypeArguments(this.suffix(node.typeName), node.typeArguments), + this.mapType(node) + ) } return this.visit(node.typeName); } @@ -1642,7 +1631,42 @@ export class JavaScriptParserVisitor { } visitClassExpression(node: ts.ClassExpression) { - return this.visitUnknown(node); + return new JS.StatementExpression( + randomId(), + new J.ClassDeclaration( + randomId(), + this.prefix(node), + Markers.EMPTY, + [], //this.mapDecorators(node), + [], //this.mapModifiers(node), + new J.ClassDeclaration.Kind( + randomId(), + node.modifiers ? this.suffix(node.modifiers[node.modifiers.length - 1]) : this.prefix(node), + Markers.EMPTY, + [], + J.ClassDeclaration.Kind.Type.Class + ), + node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), + this.mapTypeParametersAsJContainer(node), + null, // FIXME primary constructor + this.mapExtends(node), + this.mapImplements(node), + null, + new J.Block( + randomId(), + this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), + Markers.EMPTY, + this.rightPadded(false, Space.EMPTY), + node.members.map(ce => new JRightPadded( + this.convert(ce), + ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : Space.EMPTY, + ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? Markers.build([new Semicolon(randomId())]) : Markers.EMPTY + )), + this.prefix(node.getLastToken()!) + ), + this.mapType(node) + ) + ) } visitOmittedExpression(node: ts.OmittedExpression) { @@ -2024,9 +2048,9 @@ export class JavaScriptParserVisitor { null, new J.Block( randomId(), - this.prefix(node.getChildren().find(v => v.kind === ts.SyntaxKind.OpenBraceToken)!), + this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), Markers.EMPTY, - new JRightPadded(false, Space.EMPTY, Markers.EMPTY), + this.rightPadded(false, Space.EMPTY), node.members.map(te => new JRightPadded( this.convert(te), (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : Space.EMPTY, @@ -2737,7 +2761,7 @@ export class JavaScriptParserVisitor { return node.modifiers?.filter(ts.isDecorator)?.map(this.convert) ?? []; } - private mapTypeParametersAsJContainer(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer | null { + private mapTypeParametersAsJContainer(node: ts.ClassDeclaration | ts.InterfaceDeclaration | ts.ClassExpression): JContainer | null { return node.typeParameters ? JContainer.build( this.suffix(this.findChildNode(node, ts.SyntaxKind.Identifier)!), diff --git a/openrewrite/test/javascript/parser/arrow.test.ts b/openrewrite/test/javascript/parser/arrow.test.ts index 91deeeca..572da7fb 100644 --- a/openrewrite/test/javascript/parser/arrow.test.ts +++ b/openrewrite/test/javascript/parser/arrow.test.ts @@ -13,6 +13,15 @@ describe('arrow mapping', () => { ); }); + test('function with empty body', () => { + rewriteRun( + //language=typescript + typeScript(` + const empty = (/*a*/) /*b*/ => {/*c*/}; + `) + ); + }); + test('function with simple body and comments', () => { rewriteRun( //language=typescript @@ -51,6 +60,53 @@ describe('arrow mapping', () => { ); }); + test('function with trailing comma', () => { + rewriteRun( + //language=typescript + typeScript(` + let sum = (x: number, y: number /*a*/, /*b*/) => x + y; + `) + ); + }); + + test('function with async modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + let sum = async (x: number, y: number ) => x + y; + `) + ); + }); + + test('basic async arrow function', () => { + rewriteRun( + //language=typescript + typeScript(` + const fetchData = async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve("Data fetched successfully!"); + }, 2000); + }); + }; + + // Using the function + fetchData().then((message) => { + console.log(message); // Outputs: Data fetched successfully! (after 2 seconds) + }); + `) + ); + }); + + test('function with async modifier and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + let sum = /*a*/async /*b*/ (/*c*/x: number, y: number /*d*/) /*e*/ => x + y; + `) + ); + }); + test('function with implicit return and comments', () => { rewriteRun( //language=typescript diff --git a/openrewrite/test/javascript/parser/class.test.ts b/openrewrite/test/javascript/parser/class.test.ts index a2bc78b5..ad704dc1 100644 --- a/openrewrite/test/javascript/parser/class.test.ts +++ b/openrewrite/test/javascript/parser/class.test.ts @@ -116,12 +116,12 @@ describe('class mapping', () => { b = 6; // method 1 - abs(): string{ + abs(x): string{ return "1"; } //method 2 - max(): number { + max(x, y /*a*/, /*b*/): number { return 2; } } /*asdasdas*/ @@ -179,7 +179,6 @@ describe('class mapping', () => { ); }); - test('class with simple ctor', () => { rewriteRun( //language=typescript @@ -220,7 +219,70 @@ describe('class mapping', () => { ); }); - test.skip('anonymous class declaration', () => { + test('anonymous class expression', () => { + rewriteRun( + //language=typescript + typeScript(` + const MyClass = class { + constructor(public name: string) { + } + }; + `) + ); + }); + + test('anonymous class expression with comments', () => { + rewriteRun( + //language=typescript + typeScript(` + const MyClass = /*a*/class/*b*/ {/*c*/ + constructor(public name: string) { + } + }; + `) + ); + }); + + test('named class expression', () => { + rewriteRun( + //language=typescript + typeScript(` + const Employee = class EmployeeClass { + constructor(public position: string, public salary: number, ) { + } + }; + `) + ); + }); + + test('class extends expression', () => { + rewriteRun( + //language=typescript + typeScript(` + class OuterClass extends (class extends Number { }) { + } + `) + ); + }); + + test.skip('class expressions inline', () => { + rewriteRun( + //language=typescript + typeScript(` + function createInstance(ClassType: new () => any) { + return new ClassType(); + } + + const instance = createInstance(class { + sayHello() { + console.log("Hello from an inline class!"); + } + }); + `) + ); + }); + + test.skip('inner class declaration with extends', () => { rewriteRun( //language=typescript typeScript(` @@ -230,18 +292,7 @@ describe('class mapping', () => { const a: typeof OuterClass.InnerClass.prototype = 1; `) ); - }); - - test.skip('nested class qualified name', () => { - rewriteRun( - //language=typescript - typeScript(` - class OuterClass extends (class extends Number { }) { - } - const a: typeof OuterClass.InnerClass.prototype = 1; - `) - ); - }); + }); test('class with optional properties, ctor and modifiers', () => { rewriteRun( diff --git a/openrewrite/test/javascript/parser/function.test.ts b/openrewrite/test/javascript/parser/function.test.ts index 476a9be1..de826ca0 100644 --- a/openrewrite/test/javascript/parser/function.test.ts +++ b/openrewrite/test/javascript/parser/function.test.ts @@ -108,6 +108,17 @@ describe('function mapping', () => { ); }); + test('function with type ref', () => { + rewriteRun( + //language=typescript + typeScript(` + function getLength(arr: Array): number { + return arr.length; + } + `) + ); + }); + test.skip('function type with parameter', () => { rewriteRun( //language=typescript