Skip to content

Commit 60f43a4

Browse files
authored
Merge pull request #172 from Perryvw/metaextenstion-decorator
Refactored Decorators
2 parents d1f86b5 + 0be1311 commit 60f43a4

7 files changed

Lines changed: 208 additions & 51 deletions

File tree

src/Decorator.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export class Decorator {
2+
public kind: DecoratorKind;
3+
public args: string[];
4+
5+
constructor(raw: string) {
6+
let nameEnd = raw.indexOf(" ");
7+
if (nameEnd === -1) {
8+
nameEnd = raw.length;
9+
}
10+
this.kind = DecoratorKind[raw.substring(1, nameEnd)];
11+
this.args = raw.split(" ").slice(1);
12+
}
13+
}
14+
15+
export enum DecoratorKind {
16+
Extension = "Extension",
17+
MetaExtension = "MetaExtension",
18+
CustomConstructor = "CustomConstructor",
19+
CompileMembersOnly = "CompileMembersOnly",
20+
PureAbstract = "PureAbstract",
21+
Phantom = "Phantom",
22+
TupleReturn = "TupleReturn",
23+
NoClassOr = "NoClassOr",
24+
}

src/TSHelper.ts

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as ts from "typescript";
2+
import { Decorator, DecoratorKind } from "./Decorator";
23

34
export class TSHelper {
45

@@ -28,11 +29,12 @@ export class TSHelper {
2829
}
2930

3031
public static getExtendedType(node: ts.ClassDeclaration, checker: ts.TypeChecker): ts.Type | undefined {
31-
if (node.heritageClauses) {
32+
if (node && node.heritageClauses) {
3233
for (const clause of node.heritageClauses) {
3334
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
3435
const superType = checker.getTypeAtLocation(clause.types[0]);
35-
if (!this.isPureAbstractClass(superType, checker)) {
36+
const decorators = this.getCustomDecorators(superType, checker);
37+
if (!decorators.has(DecoratorKind.PureAbstract)) {
3638
return superType;
3739
}
3840
}
@@ -68,57 +70,33 @@ export class TSHelper {
6870
return typeNode && (typeNode.kind === ts.SyntaxKind.ArrayType || typeNode.kind === ts.SyntaxKind.TupleType);
6971
}
7072

71-
public static isCompileMembersOnlyEnum(type: ts.Type, checker: ts.TypeChecker): boolean {
72-
return type.symbol
73-
&& ((type.symbol.flags & ts.SymbolFlags.Enum) !== 0)
74-
&& type.symbol.getDocumentationComment(checker)[0] !== undefined
75-
&& this.hasCustomDecorator(type, checker, "!CompileMembersOnly");
76-
}
77-
78-
public static isPureAbstractClass(type: ts.Type, checker: ts.TypeChecker): boolean {
79-
return type.symbol
80-
&& ((type.symbol.flags & ts.SymbolFlags.Class) !== 0)
81-
&& this.hasCustomDecorator(type, checker, "!PureAbstract");
82-
}
83-
84-
public static isExtensionClass(type: ts.Type, checker: ts.TypeChecker): boolean {
85-
return type.symbol
86-
&& ((type.symbol.flags & ts.SymbolFlags.Class) !== 0)
87-
&& this.hasCustomDecorator(type, checker, "!Extension");
88-
}
89-
90-
public static isPhantom(type: ts.Type, checker: ts.TypeChecker): boolean {
91-
return type.symbol
92-
&& ((type.symbol.flags & ts.SymbolFlags.Namespace) !== 0)
93-
&& this.hasCustomDecorator(type, checker, "!Phantom");
94-
}
95-
9673
public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean {
9774
if (ts.isCallExpression(node)) {
9875
const type = checker.getTypeAtLocation(node.expression);
99-
return this.isTupleReturnFunction(type, checker);
76+
77+
return this.getCustomDecorators(type, checker)
78+
.has(DecoratorKind.TupleReturn);
10079
} else {
10180
return false;
10281
}
10382
}
10483

105-
public static isTupleReturnFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
106-
return type.symbol
107-
&& ((type.symbol.flags & ts.SymbolFlags.Function) !== 0
108-
|| (type.symbol.flags & ts.SymbolFlags.Method) !== 0)
109-
&& this.hasCustomDecorator(type, checker, "!TupleReturn");
110-
}
111-
112-
public static hasCustomDecorator(type: ts.Type, checker: ts.TypeChecker, decorator: string): boolean {
84+
public static getCustomDecorators(type: ts.Type, checker: ts.TypeChecker): Map<DecoratorKind, Decorator> {
11385
if (type.symbol) {
11486
const comments = type.symbol.getDocumentationComment(checker);
11587
const decorators =
11688
comments.filter(comment => comment.kind === "text")
117-
.map(comment => comment.text.trim())
118-
.filter(comment => comment[0] === "!");
119-
return decorators.indexOf(decorator) > -1;
89+
.map(comment => comment.text.trim().split("\n"))
90+
.reduce((a, b) => a.concat(b), [])
91+
.filter(comment => comment[0] === "!");
92+
const decMap = new Map<DecoratorKind, Decorator>();
93+
decorators.forEach(decStr => {
94+
const dec = new Decorator(decStr);
95+
decMap.set(dec.kind, dec);
96+
});
97+
return decMap;
12098
}
121-
return false;
99+
return new Map<DecoratorKind, Decorator>();
122100
}
123101

124102
// Search up until finding a node satisfying the callback

src/Transpiler.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TSHelper as tsHelper } from "./TSHelper";
55

66
import * as fs from "fs";
77
import * as path from "path";
8+
import { DecoratorKind } from "./Decorator";
89

910
/* tslint:disable */
1011
const packageJSON = require("../package.json");
@@ -343,8 +344,9 @@ export abstract class LuaTranspiler {
343344
}
344345

345346
public transpileNamespace(node: ts.ModuleDeclaration): string {
347+
const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(node), this.checker);
346348
// If phantom namespace just transpile the body as normal
347-
if (tsHelper.isPhantom(this.checker.getTypeAtLocation(node), this.checker) && node.body) {
349+
if (decorators.has(DecoratorKind.Phantom) && node.body) {
348350
return this.transpileNode(node.body);
349351
}
350352

@@ -376,7 +378,8 @@ export abstract class LuaTranspiler {
376378
let result = "";
377379

378380
const type = this.checker.getTypeAtLocation(node);
379-
const membersOnly = tsHelper.isCompileMembersOnlyEnum(type, this.checker);
381+
const membersOnly = tsHelper.getCustomDecorators(type, this.checker)
382+
.has(DecoratorKind.CompileMembersOnly);
380383

381384
if (!membersOnly) {
382385
const name = this.transpileIdentifier(node.name);
@@ -681,8 +684,15 @@ export abstract class LuaTranspiler {
681684
// If parent function is a TupleReturn function
682685
// and return expression is an array literal, leave out brackets.
683686
const declaration = tsHelper.findFirstNodeAbove(node, ts.isFunctionDeclaration);
684-
if (declaration && tsHelper.isTupleReturnFunction(this.checker.getTypeAtLocation(declaration), this.checker)
685-
&& ts.isArrayLiteralExpression(node.expression)) {
687+
let isTupleReturn = false;
688+
if (declaration) {
689+
const decorators = tsHelper.getCustomDecorators(
690+
this.checker.getTypeAtLocation(declaration),
691+
this.checker
692+
);
693+
isTupleReturn = decorators.has(DecoratorKind.TupleReturn);
694+
}
695+
if (isTupleReturn && ts.isArrayLiteralExpression(node.expression)) {
686696
return "return " + node.expression.elements.map(elem => this.transpileExpression(elem)).join(",");
687697
}
688698

@@ -1030,8 +1040,18 @@ export abstract class LuaTranspiler {
10301040
public transpileNewExpression(node: ts.NewExpression): string {
10311041
const name = this.transpileExpression(node.expression);
10321042
const params = node.arguments ? this.transpileArguments(node.arguments, ts.createTrue()) : "true";
1043+
const type = this.checker.getTypeAtLocation(node);
1044+
const classDecorators = tsHelper.getCustomDecorators(type, this.checker);
10331045

1034-
this.checkForLuaLibType(this.checker.getTypeAtLocation(node));
1046+
this.checkForLuaLibType(type);
1047+
1048+
if (classDecorators.has(DecoratorKind.CustomConstructor)) {
1049+
const customDecorator = classDecorators.get(DecoratorKind.CustomConstructor);
1050+
if (!customDecorator.args[0]) {
1051+
throw new TranspileError("!CustomConstructor requires one argument", node);
1052+
}
1053+
return `${customDecorator.args[0]}(${this.transpileArguments(node.arguments)})`;
1054+
}
10351055

10361056
return `${name}.new(${params})`;
10371057
}
@@ -1246,8 +1266,9 @@ export abstract class LuaTranspiler {
12461266

12471267
this.checkForLuaLibType(type);
12481268

1269+
const decorators = tsHelper.getCustomDecorators(type, this.checker);
12491270
// Do not output path for member only enums
1250-
if (tsHelper.isCompileMembersOnlyEnum(type, this.checker)) {
1271+
if (decorators.has(DecoratorKind.CompileMembersOnly)) {
12511272
return property;
12521273
}
12531274

@@ -1537,8 +1558,19 @@ export abstract class LuaTranspiler {
15371558

15381559
let className = this.transpileIdentifier(node.name);
15391560

1561+
const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(node), this.checker);
1562+
15401563
// Find out if this class is extension of existing class
1541-
const isExtension = tsHelper.isExtensionClass(this.checker.getTypeAtLocation(node), this.checker);
1564+
const isExtension = decorators.has(DecoratorKind.Extension);
1565+
1566+
const isMetaExtension = decorators.has(DecoratorKind.MetaExtension);
1567+
1568+
if (isExtension && isMetaExtension) {
1569+
throw new TranspileError(
1570+
"Can't use both decorators '!Extension' and '!MetaExtension' on the same class.",
1571+
node
1572+
);
1573+
}
15421574

15431575
// Get type that is extended
15441576
const extendsType = tsHelper.getExtendedType(node, this.checker);
@@ -1554,16 +1586,33 @@ export abstract class LuaTranspiler {
15541586

15551587
let result = "";
15561588

1557-
if (!isExtension) {
1589+
if (!isExtension && !isMetaExtension) {
15581590
result += this.transpileClassCreationMethods(node, instanceFields, extendsType);
15591591
} else {
15601592
// export empty table
15611593
this.pushExport(className, node, true);
15621594
}
15631595

15641596
// Overwrite the original className with the class we are overriding for extensions
1565-
if (isExtension && extendsType) {
1566-
className = extendsType.symbol.escapedName as string;
1597+
if (isMetaExtension) {
1598+
if (!extendsType) {
1599+
throw new TranspileError(
1600+
"!MetaExtension requires the base class to have the name of the metatable beeing extended.",
1601+
node
1602+
);
1603+
}
1604+
const extendsName = extendsType.symbol.escapedName as string;
1605+
className = "__meta__" + extendsName;
1606+
result += `local ${className} = debug.getregistry()["${extendsName}"]\n`;
1607+
}
1608+
1609+
if (isExtension) {
1610+
const extensionNameArg = decorators.get(DecoratorKind.Extension).args[0];
1611+
if (extensionNameArg) {
1612+
className = extensionNameArg;
1613+
} else if (extendsType) {
1614+
className = extendsType.symbol.escapedName as string;
1615+
}
15671616
}
15681617

15691618
// Add static declarations
@@ -1606,7 +1655,11 @@ export abstract class LuaTranspiler {
16061655
extendsType: ts.Type): string {
16071656
const className = this.transpileIdentifier(node.name);
16081657

1609-
const noClassOr = extendsType && tsHelper.hasCustomDecorator(extendsType, this.checker, "!NoClassOr");
1658+
let noClassOr = false;
1659+
if (extendsType) {
1660+
const decorators = tsHelper.getCustomDecorators(extendsType, this.checker);
1661+
noClassOr = decorators.has(DecoratorKind.NoClassOr);
1662+
}
16101663

16111664
let result = "";
16121665

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function RenamedTestClass.myFunction(self)
2+
end
3+
function RenamedMyClass.myFunction(self)
4+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** !Extension RenamedTestClass */
2+
class TestClass {
3+
myFunction() {}
4+
}
5+
6+
/** !Extension RenamedMyClass */
7+
class MyClass extends TestClass {
8+
myFunction() {}
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Expect, Test, TestCase } from "alsatian";
2+
import * as util from "../src/util";
3+
4+
import { TranspileError } from "../../src/Transpiler";
5+
6+
export class DecoratorCustomConstructor {
7+
8+
@Test("CustomCreate")
9+
public customCreate(): void {
10+
// Transpile
11+
const lua = util.transpileString(
12+
`/** !CustomConstructor Point2DCreate */
13+
class Point2D {
14+
x: number;
15+
y: number;
16+
}
17+
function Point2DCreate(x: number, y: number) {
18+
return {x: x, y: y};
19+
}
20+
return new Point2D(1, 2).x;
21+
`
22+
);
23+
const result = util.executeLua(lua);
24+
// Assert
25+
Expect(result).toBe(1);
26+
}
27+
28+
@Test("IncorrectUsage")
29+
public incorrectUsage(): void {
30+
Expect(() => {
31+
util.transpileString(
32+
`/** !CustomConstructor */
33+
class Point2D {
34+
x: number;
35+
y: number;
36+
}
37+
return new Point2D(1, 2).x;
38+
`
39+
);
40+
}).toThrowError(TranspileError, "!CustomConstructor requires one argument");
41+
}
42+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Expect, Test, TestCase } from "alsatian";
2+
import * as util from "../src/util";
3+
4+
import { TranspileError } from "../../src/Transpiler";
5+
6+
export class DecoratorMetaExtension {
7+
8+
@Test("MetaExtension")
9+
public metaExtension(): void {
10+
// Transpile
11+
const lua = util.transpileString(
12+
`
13+
declare class _LOADED;
14+
declare namespace debug {
15+
function getregistry(): any;
16+
}
17+
/** !MetaExtension */
18+
class LoadedExt extends _LOADED {
19+
public static test() {
20+
return 5;
21+
}
22+
}
23+
return debug.getregistry()["_LOADED"].test();
24+
`
25+
);
26+
const result = util.executeLua(lua);
27+
// Assert
28+
Expect(result).toBe(5);
29+
}
30+
31+
@Test("IncorrectUsage")
32+
public incorrectUsage(): void {
33+
Expect(() => {
34+
util.transpileString(
35+
`
36+
/** !MetaExtension */
37+
class LoadedExt {
38+
public static test() {
39+
return 5;
40+
}
41+
}
42+
`
43+
);
44+
}).toThrowError(TranspileError,
45+
"!MetaExtension requires the base class to have the name of the metatable beeing extended.");
46+
}
47+
}

0 commit comments

Comments
 (0)