From 9401d69ceabdee0f5b6239b477d89a5d5ee3a9ad Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Mon, 28 Oct 2024 15:06:42 +0100 Subject: [PATCH] Implemented interface declaration support Not implemented yet: - type parameters - call signature (arrow function) - indexable type --- openrewrite/src/javascript/parser.ts | 120 ++++++- .../test/javascript/parser/interface.test.ts | 297 ++++++++++++++++++ 2 files changed, 406 insertions(+), 11 deletions(-) create mode 100644 openrewrite/test/javascript/parser/interface.test.ts diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index 546de5e7..8fde9e24 100644 --- a/openrewrite/src/javascript/parser.ts +++ b/openrewrite/src/javascript/parser.ts @@ -246,7 +246,7 @@ export class JavaScriptParserVisitor { ); } - private mapModifiers(node: ts.VariableDeclarationList | ts.VariableStatement | ts.ClassDeclaration | ts.PropertyDeclaration | ts.FunctionDeclaration | ts.ParameterDeclaration | ts.MethodDeclaration | ts.EnumDeclaration) { + private mapModifiers(node: ts.VariableDeclarationList | ts.VariableStatement | ts.ClassDeclaration | ts.PropertyDeclaration | ts.FunctionDeclaration | ts.ParameterDeclaration | ts.MethodDeclaration | ts.EnumDeclaration | ts.InterfaceDeclaration | ts.PropertySignature ) { if (ts.isVariableStatement(node)) { return [new J.Modifier( randomId(), @@ -258,10 +258,12 @@ export class JavaScriptParserVisitor { )]; } else if (ts.isClassDeclaration(node)) { return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; - } else if (ts.isEnumDeclaration(node)) { + } else if (ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node)) { return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; } else if (ts.isPropertyDeclaration(node)) { return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; + } else if (ts.isPropertySignature(node)) { + return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; } else if (ts.isFunctionDeclaration(node) || ts.isParameter(node) || ts.isMethodDeclaration(node)) { return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; } else if (ts.isVariableDeclarationList(node)) { @@ -419,12 +421,12 @@ export class JavaScriptParserVisitor { return null; } - private mapImplements(node: ts.ClassDeclaration): JContainer | null { + private mapImplements(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer | null { if (node.heritageClauses == undefined || node.heritageClauses.length == 0) { return null; } for (let heritageClause of node.heritageClauses) { - if (heritageClause.token == ts.SyntaxKind.ImplementsKeyword) { + if ((heritageClause.token == ts.SyntaxKind.ImplementsKeyword) || (heritageClause.token == ts.SyntaxKind.ExtendsKeyword)) { const _implements: JRightPadded[] = []; for (let type of heritageClause.types) { _implements.push(this.rightPadded(this.visit(type), this.suffix(type))); @@ -463,6 +465,10 @@ export class JavaScriptParserVisitor { return this.mapIdentifier(node, 'undefined'); } + visitVoidKeyword(node: ts.Node) { + return this.mapIdentifier(node, 'void'); + } + visitFalseKeyword(node: ts.FalseLiteral) { return this.mapLiteral(node, false); } @@ -657,7 +663,28 @@ export class JavaScriptParserVisitor { } visitPropertySignature(node: ts.PropertySignature) { - return this.visitUnknown(node); + return new J.VariableDeclarations( + randomId(), + this.prefix(node), + Markers.EMPTY, + [], // no decorators allowed + this.mapModifiers(node), + this.mapTypeInfo(node), + null, + [], + [this.rightPadded( + new J.VariableDeclarations.NamedVariable( + randomId(), + this.prefix(node.name), + Markers.EMPTY, + this.visit(node.name), + [], + null, + this.mapVariableType(node) + ), + Space.EMPTY + )] + ); } visitPropertyDeclaration(node: ts.PropertyDeclaration) { @@ -686,7 +713,26 @@ export class JavaScriptParserVisitor { } visitMethodSignature(node: ts.MethodSignature) { - return this.visitUnknown(node); + return new J.MethodDeclaration( + randomId(), + this.prefix(node), + 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.mapTypeInfo(node), + new J.MethodDeclaration.IdentifierWithAnnotations( + node.name ? this.visit(node.name) : this.mapIdentifier(node, ""), + [] + ), + this.mapCommaSeparatedList(this.getParameterListNodes(node)), + null, + null, + null, + this.mapMethodType(node) + ); } visitMethodDeclaration(node: ts.MethodDeclaration) { @@ -712,7 +758,7 @@ export class JavaScriptParserVisitor { ); } - private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration) { + private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration | ts.PropertySignature | ts.MethodSignature) { return node.type ? new JS.TypeInfo(randomId(), this.prefix(node.getChildAt(node.getChildren().indexOf(node.type) - 1)), Markers.EMPTY, this.visit(node.type)) : null; } @@ -760,7 +806,17 @@ export class JavaScriptParserVisitor { } visitFunctionType(node: ts.FunctionTypeNode) { - return this.visitUnknown(node); + return new JS.FunctionType( + randomId(), + this.prefix(node), + Markers.EMPTY, + new JContainer( + this.prefix(node), + node.parameters.map(p => this.rightPadded(this.visit(p), this.suffix(p))), + Markers.EMPTY), + this.prefix(node.getChildren().find(v => v.kind === ts.SyntaxKind.EqualsGreaterThanToken)!), + this.convert(node.type), + null); } visitConstructorType(node: ts.ConstructorTypeNode) { @@ -1534,7 +1590,7 @@ export class JavaScriptParserVisitor { ); } - private getParameterListNodes(node: ts.FunctionDeclaration | ts.MethodDeclaration) { + private getParameterListNodes(node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.MethodSignature) { const children = node.getChildren(this.sourceFile); for (let i = 0; i < children.length; i++) { if (children[i].kind == ts.SyntaxKind.OpenParenToken) { @@ -1545,7 +1601,43 @@ export class JavaScriptParserVisitor { } visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - return this.visitUnknown(node); + if (node.typeParameters) { + return this.visitUnknown(node); + } + + return new J.ClassDeclaration( + randomId(), + this.prefix(node), + Markers.EMPTY, + [], // interface has no decorators + 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.Interface + ), + node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), + this.mapTypeParameters(node), + null, // interface has no constructor + null, // implements should be used + this.mapImplements(node), // interface extends modeled as implements + null, + new J.Block( + randomId(), + this.prefix(node.getChildren().find(v => v.kind === ts.SyntaxKind.OpenBraceToken)!), + Markers.EMPTY, + new JRightPadded(false, Space.EMPTY, Markers.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, + (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? Markers.build([this.convertToken(te.getLastToken())!]) : Markers.EMPTY + )), + this.prefix(node.getLastToken()!) + ), + this.mapType(node) + ); } visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration) { @@ -2139,7 +2231,7 @@ export class JavaScriptParserVisitor { return node.modifiers?.filter(ts.isDecorator)?.map(this.convert) ?? []; } - private mapTypeParameters(node: ts.ClassDeclaration): JContainer | null { + private mapTypeParameters(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer | null { return null; // FIXME } @@ -2151,6 +2243,12 @@ export class JavaScriptParserVisitor { } return undefined; } + + private convertToken(token?: ts.Node) { + if (token?.kind === ts.SyntaxKind.CommaToken) return new TrailingComma(randomId(), Space.EMPTY); + if (token?.kind === ts.SyntaxKind.SemicolonToken) return new Semicolon(randomId()); + return null; + } } function prefixFromNode(node: ts.Node, sourceFile: ts.SourceFile): Space { diff --git a/openrewrite/test/javascript/parser/interface.test.ts b/openrewrite/test/javascript/parser/interface.test.ts new file mode 100644 index 00000000..7a8f0224 --- /dev/null +++ b/openrewrite/test/javascript/parser/interface.test.ts @@ -0,0 +1,297 @@ +import {connect, disconnect, rewriteRun, typeScript} from '../testHarness'; + +describe('as mapping', () => { + beforeAll(() => connect()); + afterAll(() => disconnect()); + + test('empty interface', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Empty {} + `) + ); + }); + + test('interface with export modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + export interface Empty { + greet(name: string, surname: string): void; + } + `) + ); + }); + + test('interface with declare modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + declare interface Empty { + greet(name: string, surname: string): void; + } + `) + ); + }); + + test('interface with extends', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Animal { + name: string; + } + + interface Dog extends Animal { + breed: string; + } + `) + ); + }); + + test('interface with extending multiple interfaces', () => { + rewriteRun( + //language=typescript + typeScript(` + interface HasLegs { + count: string; + } + + interface Animal { + name: string; + } + + interface Dog extends Animal, HasLegs { + breed: string; + } + `) + ); + }); + + test('interface with properties', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + name: string + age: number + } + `) + ); + }); + + test('interface with properties with semicolons', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + name: string; + age: number; + } + `) + ); + }); + + test('interface with properties with coma', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + name: string, + age: number, + } + `) + ); + }); + + test('interface with properties with semicolons and comma', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + name: string, + age: number; + } + `) + ); + }); + + test('interface with properties with semicolons, comma and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + /*a*/ name /*b*/: /*c*/ string /*d*/ ; /*e*/ + age: number /*f*/ + } + `) + ); + }); + + test('interface with methods', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + greet(): void; + age(): number; + name(): string, + greet_name: (name: string) => string; + } + `) + ); + }); + + test('interface with methods and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + /*a*/ greet() /*b*/ : /*c*/ void /*d*/; + age(): number; /*e*/ + name(): string /*f*/ + greet_name/*g*/: /*h*/(/*i*/name/*j*/: /*k*/string/*l*/) /*m*/=> /*n*/string /*o*/; + + } + `) + ); + }); + + test('interface with properties and methods', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + greet(name: string): void + name: string + name(): string; + age: number + age(): number; + } + `) + ); + }); + + test('interface with properties and methods with modifiers ', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + greet(name: string): void + readonly name: string + } + `) + ); + }); + + test.skip('interface with properties and methods with optional ', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + greet?(name: string): void + readonly name?: string + } + `) + ); + }); + + test('interface with properties, methods and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Person { + /*a*/ greet(/*1*/name/*2*/: /*3*/string/*4*/, /*5*/ surname: string/*6*/): /*b*/ void /*c*/ + name /*d*/ : string + name(/*11*/name/*22*/: /*33*/string/*44*/): string; + age: /*e*/ number /*f*/ + age(): number; + } + `) + ); + }); + + test('interface with function type', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + add(): (x: number, y: number) => number; + } + `) + ); + }); + + test('interface with function type and zero param', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + produce(): () => number; + } + `) + ); + }); + + test('interface with function type and several return types', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + consume(): () => number /*a*/ | /*b*/ void; + } + `) + ); + }); + + test('interface with function type and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + /*a*/ add/*b*/(/*c*/)/*d*/ : /*e*/ (/*f*/ x/*g */:/*h*/ number /*i*/, /*j*/y /*k*/: /*l*/ number/*m*/)/*n*/ => /*o*/ number /*p*/; + } + `) + ); + }); + + test.skip('interface with call signature', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + greet: (name: string) => string; + (x: number, y: number): number; + } + `) + ); + }); + + test.skip('interface with indexable type', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Add { + [index: number]: string + } + `) + ); + }); + + test.skip('interface with hybrid types', () => { + rewriteRun( + //language=typescript + typeScript(` + interface Counter { + (start: number): string; // Call signature + interval: number; // Property + reset(): void; // Method + [index: number]: string // Indexable + add(): (x: number, y: number) => number; //Function signature + } + `) + ); + }); + +});