diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 050e1eaf3..3c5404309 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -25,6 +25,7 @@ export enum LuaLibFeature { ClassIndex = "ClassIndex", ClassNewIndex = "ClassNewIndex", Decorate = "Decorate", + Error = "Error", FunctionApply = "FunctionApply", FunctionBind = "FunctionBind", FunctionCall = "FunctionCall", @@ -63,6 +64,7 @@ export enum LuaLibFeature { const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = { ArrayFlat: [LuaLibFeature.ArrayConcat], ArrayFlatMap: [LuaLibFeature.ArrayConcat], + Error: [LuaLibFeature.FunctionCall], InstanceOf: [LuaLibFeature.Symbol], Iterator: [LuaLibFeature.Symbol], ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol], diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 43111e5d1..edb31fda3 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -613,6 +613,10 @@ export class LuaTransformer { // Get type that is extended const extendsType = tsHelper.getExtendedType(statement, this.checker); + if (extendsType) { + this.checkForLuaLibType(extendsType); + } + if (!(isExtension || isMetaExtension) && extendsType) { // Non-extensions cannot extend extension classes const extendsDecorators = tsHelper.getCustomDecorators(extendsType, this.checker); @@ -2843,20 +2847,17 @@ export class LuaTransformer { } public transformThrowStatement(statement: ts.ThrowStatement): StatementVisitResult { - if (statement.expression === undefined) { - throw TSTLErrors.InvalidThrowExpression(statement); - } + const parameters: tstl.Expression[] = []; - const type = this.checker.getTypeAtLocation(statement.expression); - if (tsHelper.isStringType(type, this.checker, this.program)) { - const error = tstl.createIdentifier("error"); - return tstl.createExpressionStatement( - tstl.createCallExpression(error, [this.transformExpression(statement.expression)]), - statement - ); - } else { - throw TSTLErrors.InvalidThrowExpression(statement.expression); + if (statement.expression) { + parameters.push(this.transformExpression(statement.expression)); + parameters.push(tstl.createNumericLiteral(0)); } + + return tstl.createExpressionStatement( + tstl.createCallExpression(tstl.createIdentifier("error"), parameters), + statement + ); } public transformContinueStatement(statement: ts.ContinueStatement): StatementVisitResult { @@ -3679,7 +3680,10 @@ export class LuaTransformer { className = tstl.createAnonymousIdentifier(); } + this.pushScope(ScopeType.Function); const classDeclaration = this.transformClassDeclaration(expression, className); + this.popScope(); + return this.createImmediatelyInvokedFunctionExpression( this.statementVisitResultToArray(classDeclaration), className, @@ -4205,6 +4209,7 @@ export class LuaTransformer { const expressionType = this.checker.getTypeAtLocation(expression.expression); if (tsHelper.isStandardLibraryType(expressionType, undefined, this.program)) { + this.checkForLuaLibType(expressionType); const result = this.transformGlobalFunctionCall(expression); if (result) { return result; @@ -5501,7 +5506,8 @@ export class LuaTransformer { protected checkForLuaLibType(type: ts.Type): void { if (type.symbol) { - switch (this.checker.getFullyQualifiedName(type.symbol)) { + const name = this.checker.getFullyQualifiedName(type.symbol); + switch (name) { case "Map": this.importLuaLibFeature(LuaLibFeature.Map); return; @@ -5515,6 +5521,10 @@ export class LuaTransformer { this.importLuaLibFeature(LuaLibFeature.WeakSet); return; } + + if (tsHelper.isBuiltinErrorTypeName(name)) { + this.importLuaLibFeature(LuaLibFeature.Error); + } } } diff --git a/src/TSHelper.ts b/src/TSHelper.ts index c68c07298..3bac0cfda 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -33,6 +33,22 @@ const defaultArrayCallMethodNames = new Set([ "flatMap", ]); +// TODO [2019-09-27/Perry]: Refactor lualib detection to consistent map +const builtinErrorTypeNames = new Set([ + "Error", + "ErrorConstructor", + "RangeError", + "RangeErrorConstructor", + "ReferenceError", + "ReferenceErrorConstructor", + "SyntaxError", + "SyntaxErrorConstructor", + "TypeError", + "TypeErrorConstructor", + "URIError", + "URIErrorConstructor", +]); + export function getExtendedTypeNode( node: ts.ClassLikeDeclarationBase, checker: ts.TypeChecker @@ -1041,3 +1057,7 @@ export function formatPathToLuaPath(filePath: string): string { } return filePath.replace(/\.\//g, "").replace(/\//g, "."); } + +export function isBuiltinErrorTypeName(name: string): boolean { + return builtinErrorTypeNames.has(name); +} diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 89867e01d..c32318f45 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -54,9 +54,6 @@ export const InvalidPropertyCall = (node: ts.Node) => export const InvalidElementCall = (node: ts.Node) => new TranspileError(`Tried to transpile a non-element call as an element call.`, node); -export const InvalidThrowExpression = (node: ts.Node) => - new TranspileError(`Invalid throw expression, only strings can be thrown.`, node); - export const ForbiddenStaticClassPropertyName = (node: ts.Node, name: string) => new TranspileError(`Cannot use "${name}" as a static class property or method name.`, node); diff --git a/src/lualib/Error.ts b/src/lualib/Error.ts new file mode 100644 index 000000000..c91223f65 --- /dev/null +++ b/src/lualib/Error.ts @@ -0,0 +1,70 @@ +interface ErrorType { + name: string; + new (...args: any[]): Error; +} + +function __TS__GetErrorStack(constructor: Function): string { + let level = 1; + while (true) { + const info = debug.getinfo(level, "f"); + level += 1; + if (!info) { + // constructor is not in call stack + level = 1; + break; + } else if (info.func === constructor) { + break; + } + } + + return debug.traceback(undefined, level); +} + +function __TS__WrapErrorToString(getDescription: (this: T) => string): (this: T) => string { + return function(this: Error): string { + const description = getDescription.call(this); + const caller = debug.getinfo(3, "f"); + if (_VERSION === "Lua 5.1" || (caller && caller.func !== error)) { + return description; + } else { + return `${description}\n${this.stack}`; + } + }; +} + +function __TS__InitErrorClass(Type: ErrorType, name: string): any { + Type.name = name; + return setmetatable(Type, { + __call: (_self: any, message: string) => new Type(message), + }); +} + +Error = __TS__InitErrorClass( + class implements Error { + public name = "Error"; + public stack: string; + + constructor(public message = "") { + this.stack = __TS__GetErrorStack((this.constructor as any).new); + const metatable = getmetatable(this); + if (!metatable.__errorToStringPatched) { + metatable.__errorToStringPatched = true; + metatable.__tostring = __TS__WrapErrorToString(metatable.__tostring); + } + } + + public toString(): string { + return this.message !== "" ? `${this.name}: ${this.message}` : this.name; + } + }, + "Error" +); + +for (const errorName of ["RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]) { + globalThis[errorName] = __TS__InitErrorClass( + class extends Error { + public name = errorName; + }, + errorName + ); +} diff --git a/src/lualib/declarations/debug.d.ts b/src/lualib/declarations/debug.d.ts index d5bd489d4..a451fe07f 100644 --- a/src/lualib/declarations/debug.d.ts +++ b/src/lualib/declarations/debug.d.ts @@ -1,5 +1,23 @@ /** @noSelfInFile */ +declare const _VERSION: string; +declare function error(...args: any[]): never; + declare namespace debug { function traceback(...args: any[]): string; + + interface FunctionInfo { + func: T; + name?: string; + namewhat: "global" | "local" | "method" | "field" | ""; + source: string; + short_src: string; + linedefined: number; + lastlinedefined: number; + what: "Lua" | "C" | "main"; + currentline: number; + nups: number; + } + + function getinfo(i: number, what?: string): Partial; } diff --git a/src/lualib/declarations/global.d.ts b/src/lualib/declarations/global.d.ts index e27b9ecea..aafe7d562 100644 --- a/src/lualib/declarations/global.d.ts +++ b/src/lualib/declarations/global.d.ts @@ -10,6 +10,7 @@ declare function type( value: any ): "nil" | "number" | "string" | "boolean" | "table" | "function" | "thread" | "userdata"; declare function setmetatable(table: T, metatable: any): T; +declare function getmetatable(table: T): any; declare function rawget(table: T, key: K): T[K]; declare function rawset(table: T, key: K, val: T[K]): void; /** @tupleReturn */ diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 17a6bc15c..e1959abb8 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -1,4 +1,3 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; import * as util from "../util"; test("throwString", () => { @@ -7,12 +6,6 @@ test("throwString", () => { `.expectToEqual(new util.ExecutionError("Some Error")); }); -test("throwError", () => { - util.testFunction` - throw Error("Some Error") - `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidThrowExpression(util.nodeStub)); -}); - test.skip.each([0, 1, 2])("re-throw (%p)", i => { util.testFunction` const i: number = ${i}; @@ -292,3 +285,54 @@ test("return from nested finally", () => { `; expect(util.transpileAndExecute(code)).toBe("finally AB"); }); + +test.each([ + `"error string"`, + `42`, + `3.141`, + `true`, + `false`, + `undefined`, + `{ x: "error object" }`, + `() => "error function"`, +])("throw and catch %s", error => { + util.testFunction` + try { + throw ${error}; + } catch (error) { + if (typeof error == 'function') { + return error(); + } else { + return error; + } + } + `.expectToMatchJsResult(); +}); + +const builtinErrors = ["Error", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]; + +test.each([...builtinErrors, ...builtinErrors.map(type => `new ${type}`)])("%s properties", errorType => { + util.testFunction` + const error = ${errorType}(); + return { name: error.name, message: error.message, string: error.toString() }; + `.expectToMatchJsResult(); +}); + +test.each([...builtinErrors, "CustomError"])("get stack from %s", errorType => { + const stack = util.testFunction` + class CustomError extends Error { + public name = "CustomError"; + } + + let stack: string | undefined; + + function innerFunction() { stack = new ${errorType}().stack; } + function outerFunction() { innerFunction(); } + outerFunction(); + + return stack; + `.getLuaExecutionResult(); + + expect(stack).toMatch("innerFunction"); + expect(stack).toMatch("outerFunction"); +}); diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index bde4477fa..3cd3753f6 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -356,7 +356,7 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { const error = "foobar"; throw error;`; - expect(() => util.transpileAndExecute(code)).toThrow(/^LUA ERROR: .+ foobar$/); + expect(() => util.transpileAndExecute(code)).toThrow(/^LUA ERROR: foobar$/); }); test("variable (assert)", () => {