From 38fd0db58d55df184a7abbb6d0cc8f3bfb3cdefc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:11:38 +0200 Subject: [PATCH] add support for generics where missing (#140) --- openrewrite/src/javascript/parser.ts | 93 ++++++++++--------- .../test/javascript/parser/class.test.ts | 38 +++++++- .../test/javascript/parser/function.test.ts | 23 +++++ .../test/javascript/parser/interface.test.ts | 23 +++++ .../test/javascript/parser/method.test.ts | 14 +++ .../javascript/parser/qualifiedName.test.ts | 13 ++- 6 files changed, 156 insertions(+), 48 deletions(-) diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index 714b917d..c365efd3 100644 --- a/openrewrite/src/javascript/parser.ts +++ b/openrewrite/src/javascript/parser.ts @@ -393,10 +393,6 @@ export class JavaScriptParserVisitor { } visitClassDeclaration(node: ts.ClassDeclaration) { - if (node.typeParameters) { - return this.visitUnknown(node); - } - return new J.ClassDeclaration( randomId(), this.prefix(node), @@ -411,7 +407,7 @@ export class JavaScriptParserVisitor { J.ClassDeclaration.Kind.Type.Class ), node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), - this.mapTypeParameters(node), + this.mapTypeParametersAsJContainer(node), null, // FIXME primary constructor this.mapExtends(node), this.mapImplements(node), @@ -618,7 +614,7 @@ export class JavaScriptParserVisitor { this.prefix(parent), Markers.EMPTY, fieldAccess, - this.mapTypeArguments(parent.typeArguments), + this.mapTypeArguments(this.suffix(parent.typeName), parent.typeArguments), this.mapType(parent) ) } else { @@ -843,9 +839,8 @@ export class JavaScriptParserVisitor { Markers.EMPTY, [], // no decorators allowed [], // no modifiers allowed - node.typeParameters - ? new J.TypeParameters(randomId(), this.suffix(node.name), Markers.EMPTY, [], node.typeParameters.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)))) - : null, + + this.mapTypeParametersAsObject(node), this.mapTypeInfo(node), this.getOptionalUnary(node), this.mapCommaSeparatedList(this.getParameterListNodes(node)), @@ -862,9 +857,7 @@ export class JavaScriptParserVisitor { Markers.EMPTY, [], // no decorators allowed [], // no modifiers allowed - node.typeParameters - ? new J.TypeParameters(randomId(), this.suffix(node.name), Markers.EMPTY, [], node.typeParameters.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)))) - : null, + this.mapTypeParametersAsObject(node), this.mapTypeInfo(node), new J.MethodDeclaration.IdentifierWithAnnotations( node.name ? this.visit(node.name) : this.mapIdentifier(node, ""), @@ -905,9 +898,7 @@ export class JavaScriptParserVisitor { Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), - node.typeParameters - ? new J.TypeParameters(randomId(), this.suffix(node.name), Markers.EMPTY, [], node.typeParameters.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)))) - : null, + this.mapTypeParametersAsObject(node), this.mapTypeInfo(node), new J.MethodDeclaration.IdentifierWithAnnotations( node.name ? this.visit(node.name) : this.mapIdentifier(node, ""), @@ -922,7 +913,7 @@ export class JavaScriptParserVisitor { } private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration - | ts.PropertySignature | ts.MethodSignature | ts.ArrowFunction | ts.CallSignatureDeclaration | ts.GetAccessorDeclaration) { + | ts.PropertySignature | ts.MethodSignature | ts.ArrowFunction | ts.CallSignatureDeclaration | ts.GetAccessorDeclaration | ts.FunctionDeclaration) { return node.type ? new JS.TypeInfo(randomId(), this.prefix(node.getChildAt(node.getChildren().indexOf(node.type) - 1)), Markers.EMPTY, this.visit(node.type)) : null; } @@ -1004,25 +995,30 @@ export class JavaScriptParserVisitor { } visitCallSignature(node: ts.CallSignatureDeclaration) { - return new JS.ArrowFunction( + return new J.MethodDeclaration( randomId(), this.prefix(node), Markers.EMPTY, [], [], - new Lambda.Parameters( - randomId(), - this.prefix(node), - Markers.EMPTY, - true, - node.parameters.length > 0 ? - node.parameters.map(p => this.rightPadded(this.convert(p), this.suffix(p))) : - [this.rightPadded(this.newJEmpty(), this.prefix(node.getChildren().find(n => n.kind === ts.SyntaxKind.CloseParenToken)!))] // to handle the case: (/*no*/) => ... - ), + this.mapTypeParametersAsObject(node), this.mapTypeInfo(node), - Space.EMPTY, - this.newJEmpty(), - null + new J.MethodDeclaration.IdentifierWithAnnotations( + new J.Identifier( + randomId(), + Space.EMPTY/* this.prefix(node.getChildren().find(n => n.kind == ts.SyntaxKind.OpenBraceToken)!) */, + Markers.EMPTY, + [], // FIXME decorators + "", + null, + null + ), [] + ), + this.mapCommaSeparatedList(this.getParameterListNodes(node)), + null, + null, + null, + this.mapMethodType(node) ); } @@ -1064,6 +1060,7 @@ export class JavaScriptParserVisitor { } visitConstructorType(node: ts.ConstructorTypeNode) { + return this.visitUnknown(node); } visitTypeQuery(node: ts.TypeQueryNode) { @@ -1857,10 +1854,6 @@ export class JavaScriptParserVisitor { } visitFunctionDeclaration(node: ts.FunctionDeclaration) { - if (node.typeParameters) { - return this.visitUnknown(node); - } - return new J.MethodDeclaration( randomId(), this.prefix(node), @@ -1874,8 +1867,8 @@ export class JavaScriptParserVisitor { J.Modifier.Type.LanguageExtension, [] ), ...this.mapModifiers(node)], - null, // FIXME type parameters - node.type ? this.visit(node.type) : null, + this.mapTypeParametersAsObject(node), // FIXME type parameters + this.mapTypeInfo(node), new J.MethodDeclaration.IdentifierWithAnnotations( node.name ? this.visit(node.name) : this.mapIdentifier(node, ""), [] @@ -1899,10 +1892,6 @@ export class JavaScriptParserVisitor { } visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - if (node.typeParameters) { - return this.visitUnknown(node); - } - return new J.ClassDeclaration( randomId(), this.prefix(node), @@ -1917,7 +1906,7 @@ export class JavaScriptParserVisitor { J.ClassDeclaration.Kind.Type.Interface ), node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), - this.mapTypeParameters(node), + this.mapTypeParametersAsJContainer(node), null, // interface has no constructor null, // implements should be used this.mapInterfaceExtends(node), // interface extends modeled as implements @@ -2505,7 +2494,7 @@ export class JavaScriptParserVisitor { return this.mapToContainer(nodes, this.trailingComma(nodes)); } - private mapTypeArguments(nodes: readonly ts.Node[]): JContainer { + private mapTypeArguments(prefix: Space, nodes: readonly ts.Node[]): JContainer { if (nodes.length === 0) { return JContainer.empty(); } @@ -2517,7 +2506,7 @@ export class JavaScriptParserVisitor { Markers.EMPTY )) return new JContainer( - this.prefix(nodes[0]), + prefix, args, Markers.EMPTY ); @@ -2573,8 +2562,24 @@ export class JavaScriptParserVisitor { return node.modifiers?.filter(ts.isDecorator)?.map(this.convert) ?? []; } - private mapTypeParameters(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer | null { - return null; // FIXME + private mapTypeParametersAsJContainer(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer | null { + return node.typeParameters + ? JContainer.build( + this.suffix(this.findChildNode(node, ts.SyntaxKind.Identifier)!), + this.mapTypeParametersList(node.typeParameters), + Markers.EMPTY + ) + : null; + } + + private mapTypeParametersAsObject(node: ts.MethodDeclaration | ts.MethodSignature | ts.FunctionDeclaration | ts.CallSignatureDeclaration) : J.TypeParameters | null { + return node.typeParameters + ? new J.TypeParameters(randomId(), node.questionToken ? this.suffix(node.questionToken) : node.name ? this.suffix(node.name) : Space.EMPTY, Markers.EMPTY, [], node.typeParameters.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)))) + : null; + } + + private mapTypeParametersList(typeParamsNodeArray: ts.NodeArray) : JRightPadded[] { + return typeParamsNodeArray.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp))); } private findChildNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined { diff --git a/openrewrite/test/javascript/parser/class.test.ts b/openrewrite/test/javascript/parser/class.test.ts index 8c7323a0..42ff63d9 100644 --- a/openrewrite/test/javascript/parser/class.test.ts +++ b/openrewrite/test/javascript/parser/class.test.ts @@ -11,10 +11,9 @@ describe('class mapping', () => { ); }); test('type parameter', () => { - rewriteRunWithOptions( - {expectUnknowns: true}, + rewriteRun( //language=typescript - typeScript('class A {}') + typeScript('class A < T , G> {}') ); }); test('body', () => { @@ -211,6 +210,39 @@ describe('class mapping', () => { ); }); + test('class with type parameters', () => { + rewriteRun( + //language=typescript + typeScript(` + class A { + } + `) + ); + }); + + test.skip('anonymous class declaration', () => { + rewriteRun( + //language=typescript + typeScript(` + class OuterClass { + public static InnerClass = class extends Number { }; + } + 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( //language=typescript diff --git a/openrewrite/test/javascript/parser/function.test.ts b/openrewrite/test/javascript/parser/function.test.ts index dc1b3546..c6ce7a41 100644 --- a/openrewrite/test/javascript/parser/function.test.ts +++ b/openrewrite/test/javascript/parser/function.test.ts @@ -40,10 +40,33 @@ describe('function mapping', () => { typeScript('function f(a = 2 , b) {}') ); }); + test('parameter with trailing comma', () => { rewriteRun( //language=typescript typeScript('function f(a , ) {}') ); }); + + test('parameter with trailing comma', () => { + rewriteRun( + //language=typescript + typeScript(` + function /*1*/ identity /*2*/ < Type , G , C > (arg: Type) /*3*/ : G { + return arg; + } + `) + ); + }); + + test.skip('parameter with anonymous type', () => { + rewriteRun( + //language=typescript + typeScript(` + function create(c: { new (): Type }): Type { + return new c(); + } + `) + ); + }); }); diff --git a/openrewrite/test/javascript/parser/interface.test.ts b/openrewrite/test/javascript/parser/interface.test.ts index 7f242d7b..38dc624a 100644 --- a/openrewrite/test/javascript/parser/interface.test.ts +++ b/openrewrite/test/javascript/parser/interface.test.ts @@ -355,4 +355,27 @@ describe('as mapping', () => { ); }); + test('interface with generics', () => { + rewriteRun( + //language=typescript + typeScript(` + interface GenericIdentityFn< T > { + /*1231*/ < Type /*1*/ > ( arg : Type ) : T ; + /*1231*/ /*1231*/ add < Type /*1*/ , R > (arg: Type): (x: T, y: Type) => R; //Function signature + } + `) + ); + }); + + test('interface with generics', () => { + rewriteRun( + //language=typescript + typeScript(` + interface X { + find ? (v1: R, v2: T): string; + } + `) + ); + }); + }); diff --git a/openrewrite/test/javascript/parser/method.test.ts b/openrewrite/test/javascript/parser/method.test.ts index 8de8cf80..5d953c1b 100644 --- a/openrewrite/test/javascript/parser/method.test.ts +++ b/openrewrite/test/javascript/parser/method.test.ts @@ -148,4 +148,18 @@ describe('method mapping', () => { `) ); }); + + test('method with generics', () => { + rewriteRun( + //language=typescript + typeScript(` + class Handler< T1 , T2> { + test < T3 > ( input: string , t3: T3 ) /*1*/ : /*asda*/ string { + // hello world comment + return input; + } + } + `) + ); + }); }); diff --git a/openrewrite/test/javascript/parser/qualifiedName.test.ts b/openrewrite/test/javascript/parser/qualifiedName.test.ts index 30bb552e..90d2ab91 100644 --- a/openrewrite/test/javascript/parser/qualifiedName.test.ts +++ b/openrewrite/test/javascript/parser/qualifiedName.test.ts @@ -14,7 +14,7 @@ describe('empty mapping', () => { test('globalThis qualified name with generic', () => { rewriteRun( //language=typescript - typeScript('const value: globalThis.Promise< string > = null') + typeScript('const value: globalThis.Promise < string > = null') ); }); @@ -37,6 +37,17 @@ describe('empty mapping', () => { ); }); + test.skip('nested class qualified name', () => { + rewriteRun( + //language=typescript + typeScript(` + class OuterClass extends (class extends Number { }) { + } + const a: typeof OuterClass.InnerClass.prototype = 1; + `) + ); + }); + test('namespace qualified name', () => { rewriteRun( //language=typescript