Skip to content

Commit

Permalink
Implemented Enum declaration support (#128)
Browse files Browse the repository at this point in the history
* Implemented Enum declaration support

Implemented visitEnumDeclaration and visitEnumMember parsers.
To implement Enum members' initialisation, had to override the visitEnumValue method at JavaScriptPrinter class.
  • Loading branch information
arodionov authored Oct 23, 2024
1 parent 6032946 commit 24f27cd
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 5 deletions.
85 changes: 82 additions & 3 deletions openrewrite/src/javascript/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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)) {
Expand Down Expand Up @@ -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<J.EnumValue>[] = [];
for (let i = 0; i < childCount; i++) {
if (children[i].kind === ts.SyntaxKind.EnumMember) {
const rp = new JRightPadded(
this.convert<J.EnumValue>(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) {
Expand Down Expand Up @@ -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) {
Expand Down
169 changes: 169 additions & 0 deletions openrewrite/test/javascript/parser/enum.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}
`)
);
});
});
4 changes: 2 additions & 2 deletions openrewrite/test/javascript/parser/qualifiedName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,25 @@ public J visit(@Nullable Tree tree, PrintOutputCapture<P> p) {
}
}

@Override
public J visitEnumValue(J.EnumValue enum_, PrintOutputCapture<P> 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> p) {
beforeSyntax(annotation, Space.Location.ANNOTATION_PREFIX, p);
Expand Down

0 comments on commit 24f27cd

Please sign in to comment.