diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index b8435d6..546de5e 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) { + private mapModifiers(node: ts.VariableDeclarationList | ts.VariableStatement | ts.ClassDeclaration | ts.PropertyDeclaration | ts.FunctionDeclaration | ts.ParameterDeclaration | ts.MethodDeclaration | ts.EnumDeclaration) { if (ts.isVariableStatement(node)) { return [new J.Modifier( randomId(), @@ -258,6 +258,8 @@ export class JavaScriptParserVisitor { )]; } else if (ts.isClassDeclaration(node)) { return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []; + } else if (ts.isEnumDeclaration(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.isFunctionDeclaration(node) || ts.isParameter(node) || ts.isMethodDeclaration(node)) { @@ -1551,7 +1553,63 @@ export class JavaScriptParserVisitor { } visitEnumDeclaration(node: ts.EnumDeclaration) { - return this.visitUnknown(node); + return new J.ClassDeclaration( + randomId(), + this.prefix(node), + Markers.EMPTY, + [], // enum 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.Enum + ), + node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), + null, // enum has no type parameters + null, // enum has no constructor + null, // enum can't extend smth. + null, // enum can't implement smth. + 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), + this.convertEnumBlock(node), + this.prefix(node.getLastToken()!) + ), + this.mapType(node) + ); + } + + // EnumMembers (got from EnumDeclaration#members) have no information about Commas between them, + // so we should parse the EnumDeclaration body as a SyntaxList + // The enum body has the following structure: [EnumMember, Coma, EnumMember, ...], + // and we should manually figure out Commas positions + convertEnumBlock(enumDeclaration: ts.EnumDeclaration) { + if (enumDeclaration.members.length == 0) { + return []; + } + + // if the enum is not empty and have the following representation [ ..., '{', EnumBody, '}'] + // we access the enum body in the following way + const node = enumDeclaration.getChildren()[enumDeclaration.getChildCount() - 2]; + const children = node.getChildren(); + const childCount = children.length; + const enumMembers: JRightPadded[] = []; + for (let i = 0; i < childCount; i++) { + if (children[i].kind === ts.SyntaxKind.EnumMember) { + const rp = new JRightPadded( + this.convert(children[i]), + i + 1 < childCount ? this.prefix(children[i+1]) : Space.EMPTY, + i + 1 < childCount ? Markers.build([new TrailingComma(randomId(), Space.EMPTY)]) : Markers.EMPTY + ); + enumMembers.push(rp); + } + } + return enumMembers; } visitModuleDeclaration(node: ts.ModuleDeclaration) { @@ -1743,7 +1801,28 @@ export class JavaScriptParserVisitor { } visitEnumMember(node: ts.EnumMember) { - return this.visitUnknown(node); + return new J.EnumValue( + randomId(), + this.prefix(node), + Markers.EMPTY, + [], + node.name ? this.convert(node.name) : this.mapIdentifier(node, ""), + node.initializer ? new J.NewClass( + randomId(), + this.suffix(node.name), + Markers.EMPTY, + null, + Space.EMPTY, + null, + new JContainer( + Space.EMPTY, + [this.rightPadded(this.visit(node.initializer), Space.EMPTY)], + Markers.EMPTY + ), + null, + this.mapMethodType(node) + ) : null + ) } visitBundle(node: ts.Bundle) { diff --git a/openrewrite/test/javascript/parser/enum.test.ts b/openrewrite/test/javascript/parser/enum.test.ts new file mode 100644 index 0000000..e047794 --- /dev/null +++ b/openrewrite/test/javascript/parser/enum.test.ts @@ -0,0 +1,169 @@ +import {connect, disconnect, rewriteRun, typeScript} from '../testHarness'; + +describe('empty mapping', () => { + beforeAll(() => connect()); + afterAll(() => disconnect()); + + test('enum declaration', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + }; + `) + ); + }); + + test('enum empty declaration with modifiers', () => { + rewriteRun( + //language=typescript + typeScript(` + declare const enum Test { + }; + `) + ); + }); + + test('enum member', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + A + }; + `) + ); + }); + + test('enum member with coma', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + A , + }; + `) + ); + }); + + test('enum members', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + A, + B, + C + }; + `) + ); + }); + + test('enum with const modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + const enum Test { + A, + B, + C, + }; + `) + ); + }); + + test('enum with declare modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + declare enum Test { + A, + B, + C, + }; + `) + ); + }); + + test('enum with declare const modifier', () => { + rewriteRun( + //language=typescript + typeScript(` + declare const enum Test { + A, + B, + C, + }; + `) + ); + }); + + test('enum members with comments', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test /*xx*/ { + A /*aa*/, + /*bb*/ B /*cc*/, + C, /*dd*/ + }; + `) + ); + }); + + test('enum members with initializer', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + A = "AA", + B = 10 + } + `) + ); + }); + + test('enum mixed members with initializer', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + A = "AA", + B, + C = 10, + D = globalThis.NaN, + E = (2 + 2), + F, + } + `) + ); + }); + + test('enum members with initializer and comments', () => { + rewriteRun( + //language=typescript + typeScript(` + enum Test { + //A /*aaa*/ = /*bbb*/ "A" + A /*aaa*/ = /*bbb*/ "AA" , + B = 10 /*ccc*/ + /*ddd*/ 5 + } + `) + ); + }); + + test('enum complex members with initializer', () => { + rewriteRun( + //language=typescript + typeScript(` + const baseValue = 10; + + const enum MathConstants { + Pi = 3.14, + E = Math.E, + GoldenRatio = baseValue + 1.618, + } + `) + ); + }); +}); diff --git a/openrewrite/test/javascript/parser/qualifiedName.test.ts b/openrewrite/test/javascript/parser/qualifiedName.test.ts index 8ec05b4..fd296c5 100644 --- a/openrewrite/test/javascript/parser/qualifiedName.test.ts +++ b/openrewrite/test/javascript/parser/qualifiedName.test.ts @@ -49,12 +49,12 @@ describe('empty mapping', () => { ); }); - test.skip('enum qualified name', () => { + test('enum qualified name', () => { rewriteRun( //language=typescript typeScript(` enum Test { - A + A, B }; const val: Test.A = Test.A; diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/JavaScriptPrinter.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/JavaScriptPrinter.java index 481cb23..7ed7272 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/JavaScriptPrinter.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/JavaScriptPrinter.java @@ -449,6 +449,25 @@ public J visit(@Nullable Tree tree, PrintOutputCapture

p) { } } + @Override + public J visitEnumValue(J.EnumValue enum_, PrintOutputCapture

p) { + beforeSyntax(enum_, Space.Location.ENUM_VALUE_PREFIX, p); + visit(enum_.getName(), p); + + J.NewClass initializer = enum_.getInitializer(); + if (initializer != null) { + visitSpace(initializer.getPrefix(), Space.Location.NEW_CLASS_PREFIX, p); + p.append("="); + // there can be only one argument + Expression expression = initializer.getArguments().get(0); + visit(expression, p); + return enum_; + } + + afterSyntax(enum_, p); + return enum_; + } + @Override public J visitAnnotation(J.Annotation annotation, PrintOutputCapture

p) { beforeSyntax(annotation, Space.Location.ANNOTATION_PREFIX, p);