diff --git a/src/Decorator.ts b/src/Decorator.ts new file mode 100644 index 000000000..e0b3b6a00 --- /dev/null +++ b/src/Decorator.ts @@ -0,0 +1,24 @@ +export class Decorator { + public kind: DecoratorKind; + public args: string[]; + + constructor(raw: string) { + let nameEnd = raw.indexOf(" "); + if (nameEnd === -1) { + nameEnd = raw.length; + } + this.kind = DecoratorKind[raw.substring(1, nameEnd)]; + this.args = raw.split(" ").slice(1); + } +} + +export enum DecoratorKind { + Extension = "Extension", + MetaExtension = "MetaExtension", + CustomConstructor = "CustomConstructor", + CompileMembersOnly = "CompileMembersOnly", + PureAbstract = "PureAbstract", + Phantom = "Phantom", + TupleReturn = "TupleReturn", + NoClassOr = "NoClassOr", +} diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 104379ac5..3027ecc44 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -1,4 +1,5 @@ import * as ts from "typescript"; +import { Decorator, DecoratorKind } from "./Decorator"; export class TSHelper { @@ -28,11 +29,12 @@ export class TSHelper { } public static getExtendedType(node: ts.ClassDeclaration, checker: ts.TypeChecker): ts.Type | undefined { - if (node.heritageClauses) { + if (node && node.heritageClauses) { for (const clause of node.heritageClauses) { if (clause.token === ts.SyntaxKind.ExtendsKeyword) { const superType = checker.getTypeAtLocation(clause.types[0]); - if (!this.isPureAbstractClass(superType, checker)) { + const decorators = this.getCustomDecorators(superType, checker); + if (!decorators.has(DecoratorKind.PureAbstract)) { return superType; } } @@ -68,57 +70,33 @@ export class TSHelper { return typeNode && (typeNode.kind === ts.SyntaxKind.ArrayType || typeNode.kind === ts.SyntaxKind.TupleType); } - public static isCompileMembersOnlyEnum(type: ts.Type, checker: ts.TypeChecker): boolean { - return type.symbol - && ((type.symbol.flags & ts.SymbolFlags.Enum) !== 0) - && type.symbol.getDocumentationComment(checker)[0] !== undefined - && this.hasCustomDecorator(type, checker, "!CompileMembersOnly"); - } - - public static isPureAbstractClass(type: ts.Type, checker: ts.TypeChecker): boolean { - return type.symbol - && ((type.symbol.flags & ts.SymbolFlags.Class) !== 0) - && this.hasCustomDecorator(type, checker, "!PureAbstract"); - } - - public static isExtensionClass(type: ts.Type, checker: ts.TypeChecker): boolean { - return type.symbol - && ((type.symbol.flags & ts.SymbolFlags.Class) !== 0) - && this.hasCustomDecorator(type, checker, "!Extension"); - } - - public static isPhantom(type: ts.Type, checker: ts.TypeChecker): boolean { - return type.symbol - && ((type.symbol.flags & ts.SymbolFlags.Namespace) !== 0) - && this.hasCustomDecorator(type, checker, "!Phantom"); - } - public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean { if (ts.isCallExpression(node)) { const type = checker.getTypeAtLocation(node.expression); - return this.isTupleReturnFunction(type, checker); + + return this.getCustomDecorators(type, checker) + .has(DecoratorKind.TupleReturn); } else { return false; } } - public static isTupleReturnFunction(type: ts.Type, checker: ts.TypeChecker): boolean { - return type.symbol - && ((type.symbol.flags & ts.SymbolFlags.Function) !== 0 - || (type.symbol.flags & ts.SymbolFlags.Method) !== 0) - && this.hasCustomDecorator(type, checker, "!TupleReturn"); - } - - public static hasCustomDecorator(type: ts.Type, checker: ts.TypeChecker, decorator: string): boolean { + public static getCustomDecorators(type: ts.Type, checker: ts.TypeChecker): Map { if (type.symbol) { const comments = type.symbol.getDocumentationComment(checker); const decorators = comments.filter(comment => comment.kind === "text") - .map(comment => comment.text.trim()) - .filter(comment => comment[0] === "!"); - return decorators.indexOf(decorator) > -1; + .map(comment => comment.text.trim().split("\n")) + .reduce((a, b) => a.concat(b), []) + .filter(comment => comment[0] === "!"); + const decMap = new Map(); + decorators.forEach(decStr => { + const dec = new Decorator(decStr); + decMap.set(dec.kind, dec); + }); + return decMap; } - return false; + return new Map(); } // Search up until finding a node satisfying the callback diff --git a/src/Transpiler.ts b/src/Transpiler.ts index f3a810f6d..5108276de 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -5,6 +5,7 @@ import { TSHelper as tsHelper } from "./TSHelper"; import * as fs from "fs"; import * as path from "path"; +import { DecoratorKind } from "./Decorator"; /* tslint:disable */ const packageJSON = require("../package.json"); @@ -343,8 +344,9 @@ export abstract class LuaTranspiler { } public transpileNamespace(node: ts.ModuleDeclaration): string { + const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(node), this.checker); // If phantom namespace just transpile the body as normal - if (tsHelper.isPhantom(this.checker.getTypeAtLocation(node), this.checker) && node.body) { + if (decorators.has(DecoratorKind.Phantom) && node.body) { return this.transpileNode(node.body); } @@ -376,7 +378,8 @@ export abstract class LuaTranspiler { let result = ""; const type = this.checker.getTypeAtLocation(node); - const membersOnly = tsHelper.isCompileMembersOnlyEnum(type, this.checker); + const membersOnly = tsHelper.getCustomDecorators(type, this.checker) + .has(DecoratorKind.CompileMembersOnly); if (!membersOnly) { const name = this.transpileIdentifier(node.name); @@ -681,8 +684,15 @@ export abstract class LuaTranspiler { // If parent function is a TupleReturn function // and return expression is an array literal, leave out brackets. const declaration = tsHelper.findFirstNodeAbove(node, ts.isFunctionDeclaration); - if (declaration && tsHelper.isTupleReturnFunction(this.checker.getTypeAtLocation(declaration), this.checker) - && ts.isArrayLiteralExpression(node.expression)) { + let isTupleReturn = false; + if (declaration) { + const decorators = tsHelper.getCustomDecorators( + this.checker.getTypeAtLocation(declaration), + this.checker + ); + isTupleReturn = decorators.has(DecoratorKind.TupleReturn); + } + if (isTupleReturn && ts.isArrayLiteralExpression(node.expression)) { return "return " + node.expression.elements.map(elem => this.transpileExpression(elem)).join(","); } @@ -1030,8 +1040,18 @@ export abstract class LuaTranspiler { public transpileNewExpression(node: ts.NewExpression): string { const name = this.transpileExpression(node.expression); const params = node.arguments ? this.transpileArguments(node.arguments, ts.createTrue()) : "true"; + const type = this.checker.getTypeAtLocation(node); + const classDecorators = tsHelper.getCustomDecorators(type, this.checker); - this.checkForLuaLibType(this.checker.getTypeAtLocation(node)); + this.checkForLuaLibType(type); + + if (classDecorators.has(DecoratorKind.CustomConstructor)) { + const customDecorator = classDecorators.get(DecoratorKind.CustomConstructor); + if (!customDecorator.args[0]) { + throw new TranspileError("!CustomConstructor requires one argument", node); + } + return `${customDecorator.args[0]}(${this.transpileArguments(node.arguments)})`; + } return `${name}.new(${params})`; } @@ -1246,8 +1266,9 @@ export abstract class LuaTranspiler { this.checkForLuaLibType(type); + const decorators = tsHelper.getCustomDecorators(type, this.checker); // Do not output path for member only enums - if (tsHelper.isCompileMembersOnlyEnum(type, this.checker)) { + if (decorators.has(DecoratorKind.CompileMembersOnly)) { return property; } @@ -1537,8 +1558,19 @@ export abstract class LuaTranspiler { let className = this.transpileIdentifier(node.name); + const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(node), this.checker); + // Find out if this class is extension of existing class - const isExtension = tsHelper.isExtensionClass(this.checker.getTypeAtLocation(node), this.checker); + const isExtension = decorators.has(DecoratorKind.Extension); + + const isMetaExtension = decorators.has(DecoratorKind.MetaExtension); + + if (isExtension && isMetaExtension) { + throw new TranspileError( + "Can't use both decorators '!Extension' and '!MetaExtension' on the same class.", + node + ); + } // Get type that is extended const extendsType = tsHelper.getExtendedType(node, this.checker); @@ -1554,7 +1586,7 @@ export abstract class LuaTranspiler { let result = ""; - if (!isExtension) { + if (!isExtension && !isMetaExtension) { result += this.transpileClassCreationMethods(node, instanceFields, extendsType); } else { // export empty table @@ -1562,8 +1594,25 @@ export abstract class LuaTranspiler { } // Overwrite the original className with the class we are overriding for extensions - if (isExtension && extendsType) { - className = extendsType.symbol.escapedName as string; + if (isMetaExtension) { + if (!extendsType) { + throw new TranspileError( + "!MetaExtension requires the base class to have the name of the metatable beeing extended.", + node + ); + } + const extendsName = extendsType.symbol.escapedName as string; + className = "__meta__" + extendsName; + result += `local ${className} = debug.getregistry()["${extendsName}"]\n`; + } + + if (isExtension) { + const extensionNameArg = decorators.get(DecoratorKind.Extension).args[0]; + if (extensionNameArg) { + className = extensionNameArg; + } else if (extendsType) { + className = extendsType.symbol.escapedName as string; + } } // Add static declarations @@ -1606,7 +1655,11 @@ export abstract class LuaTranspiler { extendsType: ts.Type): string { const className = this.transpileIdentifier(node.name); - const noClassOr = extendsType && tsHelper.hasCustomDecorator(extendsType, this.checker, "!NoClassOr"); + let noClassOr = false; + if (extendsType) { + const decorators = tsHelper.getCustomDecorators(extendsType, this.checker); + noClassOr = decorators.has(DecoratorKind.NoClassOr); + } let result = ""; diff --git a/test/translation/lua/classExtension3.lua b/test/translation/lua/classExtension3.lua new file mode 100644 index 000000000..7614dafea --- /dev/null +++ b/test/translation/lua/classExtension3.lua @@ -0,0 +1,4 @@ +function RenamedTestClass.myFunction(self) +end +function RenamedMyClass.myFunction(self) +end \ No newline at end of file diff --git a/test/translation/ts/classExtension3.ts b/test/translation/ts/classExtension3.ts new file mode 100644 index 000000000..a31129841 --- /dev/null +++ b/test/translation/ts/classExtension3.ts @@ -0,0 +1,9 @@ +/** !Extension RenamedTestClass */ +class TestClass { + myFunction() {} +} + +/** !Extension RenamedMyClass */ +class MyClass extends TestClass { + myFunction() {} +} diff --git a/test/unit/decoratorCustomConstructor.spec.ts b/test/unit/decoratorCustomConstructor.spec.ts new file mode 100644 index 000000000..493aa3118 --- /dev/null +++ b/test/unit/decoratorCustomConstructor.spec.ts @@ -0,0 +1,42 @@ +import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../src/util"; + +import { TranspileError } from "../../src/Transpiler"; + +export class DecoratorCustomConstructor { + + @Test("CustomCreate") + public customCreate(): void { + // Transpile + const lua = util.transpileString( + `/** !CustomConstructor Point2DCreate */ + class Point2D { + x: number; + y: number; + } + function Point2DCreate(x: number, y: number) { + return {x: x, y: y}; + } + return new Point2D(1, 2).x; + ` + ); + const result = util.executeLua(lua); + // Assert + Expect(result).toBe(1); + } + + @Test("IncorrectUsage") + public incorrectUsage(): void { + Expect(() => { + util.transpileString( + `/** !CustomConstructor */ + class Point2D { + x: number; + y: number; + } + return new Point2D(1, 2).x; + ` + ); + }).toThrowError(TranspileError, "!CustomConstructor requires one argument"); + } +} diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts new file mode 100644 index 000000000..abfe2a22c --- /dev/null +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -0,0 +1,47 @@ +import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../src/util"; + +import { TranspileError } from "../../src/Transpiler"; + +export class DecoratorMetaExtension { + + @Test("MetaExtension") + public metaExtension(): void { + // Transpile + const lua = util.transpileString( + ` + declare class _LOADED; + declare namespace debug { + function getregistry(): any; + } + /** !MetaExtension */ + class LoadedExt extends _LOADED { + public static test() { + return 5; + } + } + return debug.getregistry()["_LOADED"].test(); + ` + ); + const result = util.executeLua(lua); + // Assert + Expect(result).toBe(5); + } + + @Test("IncorrectUsage") + public incorrectUsage(): void { + Expect(() => { + util.transpileString( + ` + /** !MetaExtension */ + class LoadedExt { + public static test() { + return 5; + } + } + ` + ); + }).toThrowError(TranspileError, + "!MetaExtension requires the base class to have the name of the metatable beeing extended."); + } +}