Skip to content

Commit

Permalink
add support for generics where missing (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegDokuka authored Nov 11, 2024
1 parent 90cf60c commit 38fd0db
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 48 deletions.
93 changes: 49 additions & 44 deletions openrewrite/src/javascript/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)),
Expand All @@ -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, ""),
Expand Down Expand Up @@ -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, ""),
Expand All @@ -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;
}

Expand Down Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -1064,6 +1060,7 @@ export class JavaScriptParserVisitor {
}

visitConstructorType(node: ts.ConstructorTypeNode) {
return this.visitUnknown(node);
}

visitTypeQuery(node: ts.TypeQueryNode) {
Expand Down Expand Up @@ -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),
Expand All @@ -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, ""),
[]
Expand All @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -2505,7 +2494,7 @@ export class JavaScriptParserVisitor {
return this.mapToContainer(nodes, this.trailingComma(nodes));
}

private mapTypeArguments(nodes: readonly ts.Node[]): JContainer<J.Expression> {
private mapTypeArguments(prefix: Space, nodes: readonly ts.Node[]): JContainer<J.Expression> {
if (nodes.length === 0) {
return JContainer.empty();
}
Expand All @@ -2517,7 +2506,7 @@ export class JavaScriptParserVisitor {
Markers.EMPTY
))
return new JContainer(
this.prefix(nodes[0]),
prefix,
args,
Markers.EMPTY
);
Expand Down Expand Up @@ -2573,8 +2562,24 @@ export class JavaScriptParserVisitor {
return node.modifiers?.filter(ts.isDecorator)?.map(this.convert<J.Annotation>) ?? [];
}

private mapTypeParameters(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer<J.TypeParameter> | null {
return null; // FIXME
private mapTypeParametersAsJContainer(node: ts.ClassDeclaration | ts.InterfaceDeclaration): JContainer<J.TypeParameter> | 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<ts.TypeParameterDeclaration>) : JRightPadded<J.TypeParameter>[] {
return typeParamsNodeArray.map(tp => this.rightPadded<J.TypeParameter>(this.visit(tp), this.suffix(tp)));
}

private findChildNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
Expand Down
38 changes: 35 additions & 3 deletions openrewrite/test/javascript/parser/class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ describe('class mapping', () => {
);
});
test('type parameter', () => {
rewriteRunWithOptions(
{expectUnknowns: true},
rewriteRun(
//language=typescript
typeScript('class A<T> {}')
typeScript('class A < T , G> {}')
);
});
test('body', () => {
Expand Down Expand Up @@ -211,6 +210,39 @@ describe('class mapping', () => {
);
});

test('class with type parameters', () => {
rewriteRun(
//language=typescript
typeScript(`
class A<T> {
}
`)
);
});

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
Expand Down
23 changes: 23 additions & 0 deletions openrewrite/test/javascript/parser/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type>(c: { new (): Type }): Type {
return new c();
}
`)
);
});
});
23 changes: 23 additions & 0 deletions openrewrite/test/javascript/parser/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? <R, T> (v1: R, v2: T): string;
}
`)
);
});

});
14 changes: 14 additions & 0 deletions openrewrite/test/javascript/parser/method.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
`)
);
});
});
13 changes: 12 additions & 1 deletion openrewrite/test/javascript/parser/qualifiedName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
});

Expand All @@ -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
Expand Down

0 comments on commit 38fd0db

Please sign in to comment.