Skip to content

Commit dbe201a

Browse files
committed
Added support for JSDoc tags as decorators
1 parent 6ac127b commit dbe201a

5 files changed

Lines changed: 198 additions & 13 deletions

File tree

src/Decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class Decorator {
77
if (nameEnd === -1) {
88
nameEnd = raw.length;
99
}
10-
this.kind = DecoratorKind[raw.substring(1, nameEnd)];
10+
this.kind = DecoratorKind[raw.substring(0, nameEnd)];
1111
this.args = raw.split(" ").slice(1);
1212
}
1313
}

src/TSHelper.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,29 @@ export class TSHelper {
109109
const comments = type.symbol.getDocumentationComment(checker);
110110
const decorators =
111111
comments.filter(comment => comment.kind === "text")
112-
.map(comment => comment.text.trim().split("\n"))
112+
.map(comment => comment.text.split("\n"))
113113
.reduce((a, b) => a.concat(b), [])
114+
.map(line => line.trim())
114115
.filter(comment => comment[0] === "!");
116+
115117
const decMap = new Map<DecoratorKind, Decorator>();
118+
116119
decorators.forEach(decStr => {
117-
const dec = new Decorator(decStr);
118-
decMap.set(dec.kind, dec);
120+
const dec = new Decorator(decStr.substr(1));
121+
if (dec.kind !== undefined) {
122+
decMap.set(dec.kind, dec);
123+
} else {
124+
console.warn(`Encountered unknown decorator ${decStr}.`);
125+
}
119126
});
127+
128+
type.symbol.getJsDocTags().forEach(tag => {
129+
const dec = new Decorator(tag.name);
130+
if (dec.kind !== undefined) {
131+
decMap.set(dec.kind, dec);
132+
}
133+
});
134+
120135
return decMap;
121136
}
122137
return new Map<DecoratorKind, Decorator>();

test/src/util.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,54 @@ export function transpileAndExecute(tsStr: string): any {
6767
return executeLua(transpileString(tsStr));
6868
}
6969

70+
export function parseTypeScript(typescript: string, target: LuaTarget = LuaTarget.Lua53)
71+
: [ts.SourceFile, ts.TypeChecker] {
72+
const compilerHost = {
73+
directoryExists: () => true,
74+
fileExists: (fileName): boolean => true,
75+
getCanonicalFileName: fileName => fileName,
76+
getCurrentDirectory: () => "",
77+
getDefaultLibFileName: () => "lib.es6.d.ts",
78+
getDirectories: () => [],
79+
getNewLine: () => "\n",
80+
81+
getSourceFile: (filename, languageVersion) => {
82+
if (filename === "file.ts") {
83+
return ts.createSourceFile(filename, typescript, ts.ScriptTarget.Latest, false);
84+
}
85+
if (filename === "lib.es6.d.ts") {
86+
const libPath = path.join(path.dirname(require.resolve("typescript")), "lib.es6.d.ts");
87+
const libSource = fs.readFileSync(libPath).toString();
88+
return ts.createSourceFile(filename, libSource, ts.ScriptTarget.Latest, false);
89+
}
90+
return undefined;
91+
},
92+
93+
readFile: () => "",
94+
95+
useCaseSensitiveFileNames: () => false,
96+
// Don't write output
97+
writeFile: (name, text, writeByteOrderMark) => null,
98+
};
99+
100+
const program = ts.createProgram(["file.ts"], { luaTarget: target }, compilerHost);
101+
return [program.getSourceFile("file.ts"), program.getTypeChecker()];
102+
}
103+
104+
export function findFirstChild(node: ts.Node, predicate: (node: ts.Node) => boolean): ts.Node | undefined {
105+
for (const child of node.getChildren()) {
106+
if (predicate(child)) {
107+
return child;
108+
}
109+
110+
const childChild = findFirstChild(child, predicate);
111+
if (childChild !== undefined) {
112+
return childChild;
113+
}
114+
}
115+
return undefined;
116+
}
117+
70118
const jsonlib = fs.readFileSync("test/src/json.lua") + "\n";
71119

72120
export const minimalTestLib = jsonlib;

test/translation/ts/enumMembersOnly.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ enum TestEnum {
33
val1 = 0,
44
val2 = 2,
55
val3,
6-
val4 = "bye"
6+
val4 = "bye",
77
}
88

99
const a = TestEnum.val1;

test/unit/tshelper.spec.ts

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { Expect, Test, TestCase } from "alsatian";
2+
import { TSHelper as tsHelper } from "../../src/TSHelper";
3+
14
import * as ts from "typescript";
5+
import * as util from "../src/util";
26

3-
import { Expect, FocusTest, IgnoreTest, Test, TestCase } from "alsatian";
4-
import { TSHelper as tsEx } from "../../src/TSHelper";
7+
import { DecoratorKind } from "../../src/Decorator";
58

69
enum TestEnum {
710
testA = 1,
@@ -15,8 +18,8 @@ export class TSHelperTests {
1518
@TestCase(-1, "unknown")
1619
@TestCase(TestEnum.testA | TestEnum.testB, "unknown")
1720
@Test("EnumName")
18-
public testEnumName(inp, expected) {
19-
const result = tsEx.enumName(inp, TestEnum);
21+
public testEnumName(inp, expected): void {
22+
const result = tsHelper.enumName(inp, TestEnum);
2023

2124
Expect(result).toEqual(expected);
2225
}
@@ -26,14 +29,133 @@ export class TSHelperTests {
2629
@TestCase(TestEnum.testA | TestEnum.testC, ["testA", "testC"])
2730
@TestCase(TestEnum.testA | TestEnum.testB | TestEnum.testC, ["testA", "testB", "testC"])
2831
@Test("EnumNames")
29-
public testEnumNames(inp, expected) {
30-
const result = tsEx.enumNames(inp, TestEnum);
32+
public testEnumNames(inp, expected): void {
33+
const result = tsHelper.enumNames(inp, TestEnum);
3134

3235
Expect(result).toEqual(expected);
3336
}
3437

3538
@Test("IsFileModuleNull")
36-
public isFileModuleNull() {
37-
Expect(tsEx.isFileModule(null)).toEqual(false);
39+
public isFileModuleNull(): void {
40+
Expect(tsHelper.isFileModule(null)).toEqual(false);
41+
}
42+
43+
@Test("GetCustomDecorators single")
44+
public GetCustomDecoratorsSingle(): void {
45+
const source = `/** !CompileMembersOnly */
46+
enum TestEnum {
47+
val1 = 0,
48+
val2 = 2,
49+
val3,
50+
val4 = "bye",
51+
}
52+
53+
const a = TestEnum.val1;`;
54+
55+
const [sourceFile, typeChecker] = util.parseTypeScript(source);
56+
const identifier = util.findFirstChild(sourceFile, ts.isIdentifier);
57+
const enumType = typeChecker.getTypeAtLocation(identifier);
58+
59+
const decorators = tsHelper.getCustomDecorators(enumType, typeChecker);
60+
61+
Expect(decorators.size).toBe(1);
62+
Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy();
63+
}
64+
65+
@Test("GetCustomDecorators multiple")
66+
public GetCustomDecoratorsMultiple(): void {
67+
const source = `/** !CompileMembersOnly
68+
* !Phantom */
69+
enum TestEnum {
70+
val1 = 0,
71+
val2 = 2,
72+
val3,
73+
val4 = "bye",
74+
}
75+
76+
const a = TestEnum.val1;`;
77+
78+
const [sourceFile, typeChecker] = util.parseTypeScript(source);
79+
const identifier = util.findFirstChild(sourceFile, ts.isIdentifier);
80+
const enumType = typeChecker.getTypeAtLocation(identifier);
81+
82+
const decorators = tsHelper.getCustomDecorators(enumType, typeChecker);
83+
84+
Expect(decorators.size).toBe(2);
85+
Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy();
86+
Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy();
87+
}
88+
89+
@Test("GetCustomDecorators single jsdoc")
90+
public GetCustomDecoratorsSingleJSDoc(): void {
91+
const source = `/** @CompileMembersOnly */
92+
enum TestEnum {
93+
val1 = 0,
94+
val2 = 2,
95+
val3,
96+
val4 = "bye",
97+
}
98+
99+
const a = TestEnum.val1;`;
100+
101+
const [sourceFile, typeChecker] = util.parseTypeScript(source);
102+
const identifier = util.findFirstChild(sourceFile, ts.isIdentifier);
103+
const enumType = typeChecker.getTypeAtLocation(identifier);
104+
105+
const decorators = tsHelper.getCustomDecorators(enumType, typeChecker);
106+
107+
Expect(decorators.size).toBe(1);
108+
Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy();
109+
}
110+
111+
@Test("GetCustomDecorators multiple jsdoc")
112+
public GetCustomDecoratorsMultipleJSDoc(): void {
113+
const source = `/** @Phantom
114+
* @CompileMembersOnly */
115+
enum TestEnum {
116+
val1 = 0,
117+
val2 = 2,
118+
val3,
119+
val4 = "bye",
120+
}
121+
122+
const a = TestEnum.val1;`;
123+
124+
const [sourceFile, typeChecker] = util.parseTypeScript(source);
125+
const identifier = util.findFirstChild(sourceFile, ts.isIdentifier);
126+
const enumType = typeChecker.getTypeAtLocation(identifier);
127+
128+
const decorators = tsHelper.getCustomDecorators(enumType, typeChecker);
129+
130+
Expect(decorators.size).toBe(2);
131+
Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy();
132+
Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy();
133+
}
134+
135+
@Test("GetCustomDecorators multiple default jsdoc")
136+
public GetCustomDecoratorsMultipleDefaultJSDoc(): void {
137+
const source = `/**
138+
* @description
139+
* @param abc def
140+
* @Phantom
141+
* @CompileMembersOnly */
142+
enum TestEnum {
143+
val1 = 0,
144+
val2 = 2,
145+
val3,
146+
val4 = "bye",
147+
}
148+
149+
const a = TestEnum.val1;`;
150+
151+
const [sourceFile, typeChecker] = util.parseTypeScript(source);
152+
const identifier = util.findFirstChild(sourceFile, ts.isIdentifier);
153+
const enumType = typeChecker.getTypeAtLocation(identifier);
154+
155+
const decorators = tsHelper.getCustomDecorators(enumType, typeChecker);
156+
157+
Expect(decorators.size).toBe(2);
158+
Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy();
159+
Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy();
38160
}
39161
}

0 commit comments

Comments
 (0)