Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Decorator.ts
Original file line number Diff line number Diff line change
@@ -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",
}
58 changes: 18 additions & 40 deletions src/TSHelper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as ts from "typescript";
import { Decorator, DecoratorKind } from "./Decorator";

export class TSHelper {

Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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<DecoratorKind, Decorator> {
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<DecoratorKind, Decorator>();
decorators.forEach(decStr => {
const dec = new Decorator(decStr);
decMap.set(dec.kind, dec);
});
return decMap;
}
return false;
return new Map<DecoratorKind, Decorator>();
}

// Search up until finding a node satisfying the callback
Expand Down
75 changes: 64 additions & 11 deletions src/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(",");
}

Expand Down Expand Up @@ -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})`;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -1554,16 +1586,33 @@ export abstract class LuaTranspiler {

let result = "";

if (!isExtension) {
if (!isExtension && !isMetaExtension) {
result += this.transpileClassCreationMethods(node, instanceFields, extendsType);
} else {
// export empty table
this.pushExport(className, node, true);
}

// 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
Expand Down Expand Up @@ -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 = "";

Expand Down
4 changes: 4 additions & 0 deletions test/translation/lua/classExtension3.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function RenamedTestClass.myFunction(self)
end
function RenamedMyClass.myFunction(self)
end
9 changes: 9 additions & 0 deletions test/translation/ts/classExtension3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** !Extension RenamedTestClass */
class TestClass {
myFunction() {}
}

/** !Extension RenamedMyClass */
class MyClass extends TestClass {
myFunction() {}
}
42 changes: 42 additions & 0 deletions test/unit/decoratorCustomConstructor.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
}
}
47 changes: 47 additions & 0 deletions test/unit/decoratorMetaExtension.spec.ts
Original file line number Diff line number Diff line change
@@ -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.");
}
}