From 86ed14294d806341dd13f17660eacaac1bf801d7 Mon Sep 17 00:00:00 2001 From: Lolleko Date: Fri, 17 Aug 2018 17:26:16 +0200 Subject: [PATCH 1/2] Refactored Decorators Added CustomConstructor decorator Added rename functionality to !Extension Tests for added decorators --- src/Decorator.ts | 44 ++++++++++++ src/TSHelper.ts | 53 ++++---------- src/Transpiler.ts | 75 +++++++++++++++++--- test/translation/lua/classExtension3.lua | 4 ++ test/translation/ts/classExtension3.ts | 9 +++ test/unit/decoratorCustomConstructor.spec.ts | 42 +++++++++++ test/unit/decoratorMetaExtension.spec.ts | 51 +++++++++++++ 7 files changed, 227 insertions(+), 51 deletions(-) create mode 100644 src/Decorator.ts create mode 100644 test/translation/lua/classExtension3.lua create mode 100644 test/translation/ts/classExtension3.ts create mode 100644 test/unit/decoratorCustomConstructor.spec.ts create mode 100644 test/unit/decoratorMetaExtension.spec.ts diff --git a/src/Decorator.ts b/src/Decorator.ts new file mode 100644 index 000000000..8266557b9 --- /dev/null +++ b/src/Decorator.ts @@ -0,0 +1,44 @@ +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 class DecoratorCollection { + private decorators: Map; + + constructor(rawCommentLines: string[]) { + this.decorators = new Map(); + rawCommentLines.forEach(raw => { + const dec = new Decorator(raw); + this.decorators.set(dec.kind, dec); + }); + } + + public hasDecorator(kind: DecoratorKind): boolean { + return this.decorators.has(kind); + } + + public getDecorator(kind: DecoratorKind): Decorator { + return this.decorators.get(kind); + } +} + +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..176ef12c4 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -1,4 +1,5 @@ import * as ts from "typescript"; +import { DecoratorCollection, 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.hasDecorator(DecoratorKind.PureAbstract)) { return superType; } } @@ -68,57 +70,28 @@ 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) + .hasDecorator(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): DecoratorCollection { 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] === "!"); + return new DecoratorCollection(decorators); } - return false; + return new DecoratorCollection([]); } // Search up until finding a node satisfying the callback diff --git a/src/Transpiler.ts b/src/Transpiler.ts index f3a810f6d..b45a101ad 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.hasDecorator(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) + .hasDecorator(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.hasDecorator(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.hasDecorator(DecoratorKind.CustomConstructor)) { + const customDecorator = classDecorators.getDecorator(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.hasDecorator(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.hasDecorator(DecoratorKind.Extension); + + const isMetaExtension = decorators.hasDecorator(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.getDecorator(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.hasDecorator(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..edcdd3872 --- /dev/null +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -0,0 +1,51 @@ +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(); + ` + ); + console.log(lua); + console.log("\n"); + console.log("\n"); + console.log("\n"); + 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."); + } +} From 0be1311fb66c4f564dcd65398e66da4622474588 Mon Sep 17 00:00:00 2001 From: Lolleko Date: Fri, 17 Aug 2018 21:28:15 +0200 Subject: [PATCH 2/2] Changed getCustomDecorators return from collection to map --- src/Decorator.ts | 20 -------------------- src/TSHelper.ts | 17 +++++++++++------ src/Transpiler.ts | 20 ++++++++++---------- test/unit/decoratorMetaExtension.spec.ts | 4 ---- 4 files changed, 21 insertions(+), 40 deletions(-) diff --git a/src/Decorator.ts b/src/Decorator.ts index 8266557b9..e0b3b6a00 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -12,26 +12,6 @@ export class Decorator { } } -export class DecoratorCollection { - private decorators: Map; - - constructor(rawCommentLines: string[]) { - this.decorators = new Map(); - rawCommentLines.forEach(raw => { - const dec = new Decorator(raw); - this.decorators.set(dec.kind, dec); - }); - } - - public hasDecorator(kind: DecoratorKind): boolean { - return this.decorators.has(kind); - } - - public getDecorator(kind: DecoratorKind): Decorator { - return this.decorators.get(kind); - } -} - export enum DecoratorKind { Extension = "Extension", MetaExtension = "MetaExtension", diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 176ef12c4..3027ecc44 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import { DecoratorCollection, DecoratorKind } from "./Decorator"; +import { Decorator, DecoratorKind } from "./Decorator"; export class TSHelper { @@ -34,7 +34,7 @@ export class TSHelper { if (clause.token === ts.SyntaxKind.ExtendsKeyword) { const superType = checker.getTypeAtLocation(clause.types[0]); const decorators = this.getCustomDecorators(superType, checker); - if (!decorators.hasDecorator(DecoratorKind.PureAbstract)) { + if (!decorators.has(DecoratorKind.PureAbstract)) { return superType; } } @@ -75,13 +75,13 @@ export class TSHelper { const type = checker.getTypeAtLocation(node.expression); return this.getCustomDecorators(type, checker) - .hasDecorator(DecoratorKind.TupleReturn); + .has(DecoratorKind.TupleReturn); } else { return false; } } - public static getCustomDecorators(type: ts.Type, checker: ts.TypeChecker): DecoratorCollection { + public static getCustomDecorators(type: ts.Type, checker: ts.TypeChecker): Map { if (type.symbol) { const comments = type.symbol.getDocumentationComment(checker); const decorators = @@ -89,9 +89,14 @@ export class TSHelper { .map(comment => comment.text.trim().split("\n")) .reduce((a, b) => a.concat(b), []) .filter(comment => comment[0] === "!"); - return new DecoratorCollection(decorators); + const decMap = new Map(); + decorators.forEach(decStr => { + const dec = new Decorator(decStr); + decMap.set(dec.kind, dec); + }); + return decMap; } - return new DecoratorCollection([]); + return new Map(); } // Search up until finding a node satisfying the callback diff --git a/src/Transpiler.ts b/src/Transpiler.ts index b45a101ad..5108276de 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -346,7 +346,7 @@ 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 (decorators.hasDecorator(DecoratorKind.Phantom) && node.body) { + if (decorators.has(DecoratorKind.Phantom) && node.body) { return this.transpileNode(node.body); } @@ -379,7 +379,7 @@ export abstract class LuaTranspiler { const type = this.checker.getTypeAtLocation(node); const membersOnly = tsHelper.getCustomDecorators(type, this.checker) - .hasDecorator(DecoratorKind.CompileMembersOnly); + .has(DecoratorKind.CompileMembersOnly); if (!membersOnly) { const name = this.transpileIdentifier(node.name); @@ -690,7 +690,7 @@ export abstract class LuaTranspiler { this.checker.getTypeAtLocation(declaration), this.checker ); - isTupleReturn = decorators.hasDecorator(DecoratorKind.TupleReturn); + isTupleReturn = decorators.has(DecoratorKind.TupleReturn); } if (isTupleReturn && ts.isArrayLiteralExpression(node.expression)) { return "return " + node.expression.elements.map(elem => this.transpileExpression(elem)).join(","); @@ -1045,8 +1045,8 @@ export abstract class LuaTranspiler { this.checkForLuaLibType(type); - if (classDecorators.hasDecorator(DecoratorKind.CustomConstructor)) { - const customDecorator = classDecorators.getDecorator(DecoratorKind.CustomConstructor); + if (classDecorators.has(DecoratorKind.CustomConstructor)) { + const customDecorator = classDecorators.get(DecoratorKind.CustomConstructor); if (!customDecorator.args[0]) { throw new TranspileError("!CustomConstructor requires one argument", node); } @@ -1268,7 +1268,7 @@ export abstract class LuaTranspiler { const decorators = tsHelper.getCustomDecorators(type, this.checker); // Do not output path for member only enums - if (decorators.hasDecorator(DecoratorKind.CompileMembersOnly)) { + if (decorators.has(DecoratorKind.CompileMembersOnly)) { return property; } @@ -1561,9 +1561,9 @@ export abstract class LuaTranspiler { const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(node), this.checker); // Find out if this class is extension of existing class - const isExtension = decorators.hasDecorator(DecoratorKind.Extension); + const isExtension = decorators.has(DecoratorKind.Extension); - const isMetaExtension = decorators.hasDecorator(DecoratorKind.MetaExtension); + const isMetaExtension = decorators.has(DecoratorKind.MetaExtension); if (isExtension && isMetaExtension) { throw new TranspileError( @@ -1607,7 +1607,7 @@ export abstract class LuaTranspiler { } if (isExtension) { - const extensionNameArg = decorators.getDecorator(DecoratorKind.Extension).args[0]; + const extensionNameArg = decorators.get(DecoratorKind.Extension).args[0]; if (extensionNameArg) { className = extensionNameArg; } else if (extendsType) { @@ -1658,7 +1658,7 @@ export abstract class LuaTranspiler { let noClassOr = false; if (extendsType) { const decorators = tsHelper.getCustomDecorators(extendsType, this.checker); - noClassOr = decorators.hasDecorator(DecoratorKind.NoClassOr); + noClassOr = decorators.has(DecoratorKind.NoClassOr); } let result = ""; diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts index edcdd3872..abfe2a22c 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -23,10 +23,6 @@ export class DecoratorMetaExtension { return debug.getregistry()["_LOADED"].test(); ` ); - console.log(lua); - console.log("\n"); - console.log("\n"); - console.log("\n"); const result = util.executeLua(lua); // Assert Expect(result).toBe(5);