diff --git a/src/Compiler.ts b/src/Compiler.ts index 73f2c01f4..7d01f0ea9 100644 --- a/src/Compiler.ts +++ b/src/Compiler.ts @@ -155,10 +155,11 @@ function emitFilesAndReportErrors(program: ts.Program): number { } export function createTranspiler(checker: ts.TypeChecker, - options: ts.CompilerOptions, + options: CompilerOptions, sourceFile: ts.SourceFile): LuaTranspiler { let luaTargetTranspiler: LuaTranspiler; - switch (options.luaTarget) { + const target = options.luaTarget ? options.luaTarget.toLowerCase() : ""; + switch (target) { case LuaTarget.Lua51: luaTargetTranspiler = new LuaTranspiler51(checker, options, sourceFile); break; diff --git a/src/Errors.ts b/src/Errors.ts index 7a32f3926..9c0319904 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -34,6 +34,9 @@ export class TSTLErrors { public static InvalidExtensionMetaExtension = (node: ts.Node) => new TranspileError(`Cannot use both '!Extension' and '!MetaExtension' decorators on the same class.`, node) + public static InvalidNewExpressionOnExtension = (node: ts.Node) => + new TranspileError(`Cannot construct classes with decorator '!Extension' or '!MetaExtension'.`, node) + public static InvalidPropertyCall = (node: ts.Node) => new TranspileError(`Tried to transpile a non-property call as property call.`, node) diff --git a/src/Transpiler.ts b/src/Transpiler.ts index 1ba744de7..3a428a1fb 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -15,10 +15,11 @@ export enum LuaTarget { Lua51 = "5.1", Lua52 = "5.2", Lua53 = "5.3", - LuaJIT = "JIT", + LuaJIT = "jit", } export enum LuaLibFeature { + ArrayConcat = "ArrayConcat", ArrayEvery = "ArrayEvery", ArrayFilter = "ArrayFilter", ArrayForEach = "ArrayForEach", @@ -315,14 +316,25 @@ export abstract class LuaTranspiler { const imports = node.importClause.namedBindings; + const requireKeyword = "require"; + if (ts.isNamedImports(imports)) { const fileImportTable = path.basename(importPathWithoutQuotes) + this.importCount; const resolvedImportPath = this.getImportPath(importPathWithoutQuotes); - let result = `local ${fileImportTable} = require(${resolvedImportPath})\n`; + let result = `local ${fileImportTable} = ${requireKeyword}(${resolvedImportPath})\n`; this.importCount++; - imports.elements.forEach(element => { + const filteredElements = imports.elements.filter(e => { + const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(e), this.checker); + return !decorators.has(DecoratorKind.Extension) && !decorators.has(DecoratorKind.MetaExtension); + }); + + if (filteredElements.length === 0) { + return ""; + } + + filteredElements.forEach(element => { const nameText = this.transpileIdentifier(element.name); if (element.propertyName) { const propertyText = this.transpileIdentifier(element.propertyName); @@ -335,7 +347,7 @@ export abstract class LuaTranspiler { return result; } else if (ts.isNamespaceImport(imports)) { const resolvedImportPath = this.getImportPath(importPathWithoutQuotes); - return `local ${this.transpileIdentifier(imports.name)} = require(${resolvedImportPath})\n`; + return `local ${this.transpileIdentifier(imports.name)} = ${requireKeyword}(${resolvedImportPath})\n`; } else { throw TSTLErrors.UnsupportedImportType(imports); } @@ -786,7 +798,9 @@ export abstract class LuaTranspiler { case ts.SyntaxKind.TypeOfExpression: return this.transpileTypeOfExpression(node as ts.TypeOfExpression); case ts.SyntaxKind.EmptyStatement: - return ""; + return ""; + case ts.SyntaxKind.SpreadElement: + return this.transpileSpreadElement(node as ts.SpreadElement); default: throw TSTLErrors.UnsupportedKind("expression", node.kind, node); } @@ -1029,6 +1043,10 @@ export abstract class LuaTranspiler { this.checkForLuaLibType(type); + if (classDecorators.has(DecoratorKind.Extension) || classDecorators.has(DecoratorKind.MetaExtension)) { + throw TSTLErrors.InvalidNewExpressionOnExtension(node); + } + if (classDecorators.has(DecoratorKind.CustomConstructor)) { const customDecorator = classDecorators.get(DecoratorKind.CustomConstructor); if (!customDecorator.args[0]) { @@ -1186,6 +1204,8 @@ export abstract class LuaTranspiler { const caller = this.transpileExpression(expression.expression); const expressionName = this.transpileIdentifier(expression.name); switch (expressionName) { + case "concat": + return this.transpileLuaLibFunction(LuaLibFeature.ArrayConcat, caller, params); case "push": return this.transpileLuaLibFunction(LuaLibFeature.ArrayPush, caller, params); case "pop": @@ -1236,6 +1256,10 @@ export abstract class LuaTranspiler { public transpilePropertyAccessExpression(node: ts.PropertyAccessExpression): string { const property = node.name.text; + if (tsHelper.hasGetAccessor(node, this.checker)) { + return this.transpileGetAccessor(node); + } + // Check for primitive types to override const type = this.checker.getTypeAtLocation(node.expression); switch (type.flags) { @@ -1245,8 +1269,6 @@ export abstract class LuaTranspiler { case ts.TypeFlags.Object: if (tsHelper.isArrayType(type, this.checker)) { return this.transpileArrayProperty(node); - } else if (tsHelper.hasGetAccessor(node, this.checker)) { - return this.transpileGetAccessor(node); } } @@ -1366,6 +1388,10 @@ export abstract class LuaTranspiler { return escapedText; } + public transpileSpreadElement(node: ts.SpreadElement): string { + return "unpack(" + this.transpileExpression(node.expression) + ")"; + } + public transpileArrayBindingElement(name: ts.ArrayBindingElement): string { if (ts.isOmittedExpression(name)) { return "__"; @@ -1566,13 +1592,6 @@ export abstract class LuaTranspiler { let result = ""; - 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 (isMetaExtension) { if (!extendsType) { @@ -1592,6 +1611,20 @@ export abstract class LuaTranspiler { } } + if (!isExtension && !isMetaExtension) { + result += this.transpileClassCreationMethods(node, instanceFields, extendsType); + } else { + for (const f of instanceFields) { + // Get identifier + const fieldIdentifier = f.name as ts.Identifier; + const fieldName = this.transpileIdentifier(fieldIdentifier); + + const value = this.transpileExpression(f.initializer); + + result += this.indent + `${className}.${fieldName} = ${value}\n`; + } + } + // Add static declarations for (const field of staticFields) { const fieldName = this.transpileIdentifier(field.name as ts.Identifier); diff --git a/src/lualib/ArrayConcat.ts b/src/lualib/ArrayConcat.ts new file mode 100644 index 000000000..06d5206b3 --- /dev/null +++ b/src/lualib/ArrayConcat.ts @@ -0,0 +1,22 @@ +declare function pcall(func: () => any): any; +declare function type(val: any): string; + +function __TS__ArrayConcat(arr1: any[], ...args: any[]): any[] { + const out: any[] = []; + for (const val of arr1) { + out[out.length] = val; + } + for (const arg of args) { + // Hack because we don't have an isArray function + if (pcall(() => (arg as any[]).length) && type(arg) !== "string") { + const argAsArray = (arg as any[]); + for (const val of argAsArray) { + out[out.length] = val; + } + } else { + out[out.length] = arg; + } + } + + return out; +} diff --git a/src/targets/Transpiler.52.ts b/src/targets/Transpiler.52.ts index 196f63a2a..31a6f4dcb 100644 --- a/src/targets/Transpiler.52.ts +++ b/src/targets/Transpiler.52.ts @@ -63,4 +63,9 @@ export class LuaTranspiler52 extends LuaTranspiler51 { public transpileDestructingAssignmentValue(node: ts.Expression): string { return `table.unpack(${this.transpileExpression(node)})`; } + + /** @override */ + public transpileSpreadElement(node: ts.SpreadElement): string { + return "table.unpack(" + this.transpileExpression(node.expression) + ")"; + } } diff --git a/test/translation/lua/classExtension4.lua b/test/translation/lua/classExtension4.lua new file mode 100644 index 000000000..2d0c01b99 --- /dev/null +++ b/test/translation/lua/classExtension4.lua @@ -0,0 +1,4 @@ +MyClass.test = "test" +MyClass.testP = "testP" +function MyClass.myFunction(self) +end \ No newline at end of file diff --git a/test/translation/ts/classExtension1.ts b/test/translation/ts/classExtension1.ts index 922185c25..cf293483d 100644 --- a/test/translation/ts/classExtension1.ts +++ b/test/translation/ts/classExtension1.ts @@ -1,4 +1,4 @@ /** !Extension */ class MyClass { - myFunction() {} -} \ No newline at end of file + public myFunction() {} +} diff --git a/test/translation/ts/classExtension2.ts b/test/translation/ts/classExtension2.ts index 1b9d54732..109160d97 100644 --- a/test/translation/ts/classExtension2.ts +++ b/test/translation/ts/classExtension2.ts @@ -4,5 +4,5 @@ class TestClass { /** !Extension */ class MyClass extends TestClass { - myFunction() {} + public myFunction() {} } diff --git a/test/translation/ts/classExtension3.ts b/test/translation/ts/classExtension3.ts index a31129841..287c46d3f 100644 --- a/test/translation/ts/classExtension3.ts +++ b/test/translation/ts/classExtension3.ts @@ -1,9 +1,9 @@ /** !Extension RenamedTestClass */ class TestClass { - myFunction() {} + public myFunction() {} } /** !Extension RenamedMyClass */ class MyClass extends TestClass { - myFunction() {} + public myFunction() {} } diff --git a/test/translation/ts/classExtension4.ts b/test/translation/ts/classExtension4.ts new file mode 100644 index 000000000..2183c5610 --- /dev/null +++ b/test/translation/ts/classExtension4.ts @@ -0,0 +1,6 @@ +/** !Extension */ +class MyClass { + public test: string = "test"; + private testP: string = "testP"; + public myFunction() {} +} diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts index c8b5694e9..d29f51467 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -1,4 +1,4 @@ -import { Expect, Test, TestCase } from "alsatian"; +import { Expect, Test } from "alsatian"; import * as util from "../src/util"; import { TranspileError } from "../../src/Errors"; @@ -44,4 +44,20 @@ export class DecoratorMetaExtension { }).toThrowError(TranspileError, "!MetaExtension requires the extension of the metatable class."); } + + @Test("DontAllowInstantiation") + public dontAllowInstantiation(): void { + Expect(() => { + util.transpileString( + ` + declare class _LOADED; + /** !MetaExtension */ + class Ext extends _LOADED { + } + const e = new Ext(); + ` + ); + }).toThrowError(TranspileError, + "Cannot construct classes with decorator '!Extension' or '!MetaExtension'."); + } } diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 69bef8ab1..9a746027c 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -322,7 +322,7 @@ export class ExpressionTests { const identifier = ts.createIdentifier("fromCodePoint"); Expect(() => transpiler.transpileStringExpression(identifier)) .toThrowError(TranspileError, "string property fromCodePoint is/are not supported " + - "for target Lua JIT."); + "for target Lua jit."); } @Test("Unknown string expression error") @@ -331,7 +331,7 @@ export class ExpressionTests { const identifier = ts.createIdentifier("abcd"); Expect(() => transpiler.transpileStringExpression(identifier)) - .toThrowError(TranspileError, "string property abcd is/are not supported for target Lua JIT."); + .toThrowError(TranspileError, "string property abcd is/are not supported for target Lua jit."); } @Test("Unsupported array function error") diff --git a/test/unit/lualib/lualib.spec.ts b/test/unit/lualib/lualib.spec.ts index 30279827c..328e346ee 100644 --- a/test/unit/lualib/lualib.spec.ts +++ b/test/unit/lualib/lualib.spec.ts @@ -172,6 +172,33 @@ export class LuaLibArrayTests { } } + @TestCase([], []) + @TestCase([1, 2, 3], []) + @TestCase([1, 2, 3], [4]) + @TestCase([1, 2, 3], [4, 5]) + @TestCase([1, 2, 3], [4, 5]) + @TestCase([1, 2, 3], 4, [5]) + @TestCase([1, 2, 3], 4, [5, 6]) + @TestCase([1, 2, 3], 4, [5, 6], 7) + @TestCase([1, 2, 3], "test", [5, 6], 7, ["test1", "test2"]) + @TestCase([1, 2, "test"], "test", ["test1", "test2"]) + @Test("array.concat") + public concat(arr: T[], ...args: T[]) { + const argStr = args.map(arg => JSON.stringify(arg)).join(","); + // Transpile + const lua = util.transpileString( + `let concatTestTable = ${JSON.stringify(arr)}; + return JSONStringify(concatTestTable.concat(${argStr}));` + ); + + // Execute + const result = util.executeLua(lua); + + // Assert + const concatArr = arr.concat(...args); + Expect(result).toBe(JSON.stringify(concatArr)); + } + @TestCase([], "") @TestCase(["test1"], "test1") @TestCase(["test1", "test2"], "test1,test2") @@ -308,7 +335,7 @@ export class LuaLibArrayTests { // Assert Expect(result).toBe(expected); } - + @TestCase("true", 11) @TestCase("false", 13) @TestCase("a < 4", 13) diff --git a/test/unit/spreadElement.spec.ts b/test/unit/spreadElement.spec.ts new file mode 100644 index 000000000..50bf8f2dc --- /dev/null +++ b/test/unit/spreadElement.spec.ts @@ -0,0 +1,24 @@ +import { Expect, Test, TestCase } from "alsatian"; + +import { LuaTarget } from "../../src/Transpiler"; +import * as util from "../src/util"; + +export class SpreadElementTest { + + @TestCase([]) + @TestCase([1, 2, 3]) + @TestCase([1, "test", 3]) + @Test("Spread Element Push") + public spreadElementPush(inp: any[]) { + const lua = util.transpileString(`return JSONStringify([].push(...${JSON.stringify(inp)}));`); + const result = util.executeLua(lua); + Expect(result).toBe([].push(...inp)); + } + + @Test("Spread Element Lua 5.1") + public spreadElement51() { + // Cant test functional because our VM doesn't run on 5.1 + const lua = util.transpileString(`[].push(...${JSON.stringify([1, 2, 3])});`, {luaTarget: LuaTarget.Lua51}); + Expect(lua).toBe("__TS__ArrayPush({}, unpack({1,2,3}));"); + } +} diff --git a/tslint.json b/tslint.json index bb6058aba..d4505f068 100644 --- a/tslint.json +++ b/tslint.json @@ -31,9 +31,9 @@ "interface-name": false, "radix": false, "typedef": [ - true, - "call-signature", - "property-declaration" + true, + "call-signature", + "property-declaration" ] }, "rulesDirectory": []