X Tutup
Skip to content

Commit bf9f609

Browse files
TheLartiansPerryvw
authored andcommitted
Allow throwing arbitrary types and add builtin error classes (#724)
* allow throwing arbitrary types and add builtin error classes * track Scope in transformClassExpression * allow subclassing Error * Update test/unit/error.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * use custom traceback in errors * return full stack on fallback * use builder API for tests * cleanup * add error name to type and reduce dependencies * test subclasses for stack * test string representation of subclassed errors * fix throwing as function test * style changes * formatting * add sourceMapTraceback option to test * add 'This error serves to prevent false positives from .expectNoExecutionError()' * Update test/unit/error.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * rollback to use debug.traceback * Update src/lualib/Error.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * fix throwing strings and update tests * Update src/lualib/Error.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * test extending from error with custom toString() * update identifiers test for new exception handling * use constructor assignment for message property * print stack for uncaught errors * Update test/unit/error.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * Update test/unit/error.spec.ts Co-Authored-By: ark120202 <ark120202@gmail.com> * rollback toString() support * add more test cases * formatting * set __tostring metamethod in constructor * update tests * remove generic from ErrorType * add info string to tests * style changes * style changes
1 parent a858d85 commit bf9f609

File tree

9 files changed

+186
-24
lines changed

9 files changed

+186
-24
lines changed

src/LuaLib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum LuaLibFeature {
2626
ClassIndex = "ClassIndex",
2727
ClassNewIndex = "ClassNewIndex",
2828
Decorate = "Decorate",
29+
Error = "Error",
2930
FunctionApply = "FunctionApply",
3031
FunctionBind = "FunctionBind",
3132
FunctionCall = "FunctionCall",
@@ -64,6 +65,7 @@ export enum LuaLibFeature {
6465
const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = {
6566
ArrayFlat: [LuaLibFeature.ArrayConcat],
6667
ArrayFlatMap: [LuaLibFeature.ArrayConcat],
68+
Error: [LuaLibFeature.FunctionCall],
6769
InstanceOf: [LuaLibFeature.Symbol],
6870
Iterator: [LuaLibFeature.Symbol],
6971
ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol],

src/LuaTransformer.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,10 @@ export class LuaTransformer {
613613
// Get type that is extended
614614
const extendsType = tsHelper.getExtendedType(statement, this.checker);
615615

616+
if (extendsType) {
617+
this.checkForLuaLibType(extendsType);
618+
}
619+
616620
if (!(isExtension || isMetaExtension) && extendsType) {
617621
// Non-extensions cannot extend extension classes
618622
const extendsDecorators = tsHelper.getCustomDecorators(extendsType, this.checker);
@@ -2843,20 +2847,17 @@ export class LuaTransformer {
28432847
}
28442848

28452849
public transformThrowStatement(statement: ts.ThrowStatement): StatementVisitResult {
2846-
if (statement.expression === undefined) {
2847-
throw TSTLErrors.InvalidThrowExpression(statement);
2848-
}
2850+
const parameters: tstl.Expression[] = [];
28492851

2850-
const type = this.checker.getTypeAtLocation(statement.expression);
2851-
if (tsHelper.isStringType(type, this.checker, this.program)) {
2852-
const error = tstl.createIdentifier("error");
2853-
return tstl.createExpressionStatement(
2854-
tstl.createCallExpression(error, [this.transformExpression(statement.expression)]),
2855-
statement
2856-
);
2857-
} else {
2858-
throw TSTLErrors.InvalidThrowExpression(statement.expression);
2852+
if (statement.expression) {
2853+
parameters.push(this.transformExpression(statement.expression));
2854+
parameters.push(tstl.createNumericLiteral(0));
28592855
}
2856+
2857+
return tstl.createExpressionStatement(
2858+
tstl.createCallExpression(tstl.createIdentifier("error"), parameters),
2859+
statement
2860+
);
28602861
}
28612862

28622863
public transformContinueStatement(statement: ts.ContinueStatement): StatementVisitResult {
@@ -3679,7 +3680,10 @@ export class LuaTransformer {
36793680
className = tstl.createAnonymousIdentifier();
36803681
}
36813682

3683+
this.pushScope(ScopeType.Function);
36823684
const classDeclaration = this.transformClassDeclaration(expression, className);
3685+
this.popScope();
3686+
36833687
return this.createImmediatelyInvokedFunctionExpression(
36843688
this.statementVisitResultToArray(classDeclaration),
36853689
className,
@@ -4205,6 +4209,7 @@ export class LuaTransformer {
42054209

42064210
const expressionType = this.checker.getTypeAtLocation(expression.expression);
42074211
if (tsHelper.isStandardLibraryType(expressionType, undefined, this.program)) {
4212+
this.checkForLuaLibType(expressionType);
42084213
const result = this.transformGlobalFunctionCall(expression);
42094214
if (result) {
42104215
return result;
@@ -5503,7 +5508,8 @@ export class LuaTransformer {
55035508

55045509
protected checkForLuaLibType(type: ts.Type): void {
55055510
if (type.symbol) {
5506-
switch (this.checker.getFullyQualifiedName(type.symbol)) {
5511+
const name = this.checker.getFullyQualifiedName(type.symbol);
5512+
switch (name) {
55075513
case "Map":
55085514
this.importLuaLibFeature(LuaLibFeature.Map);
55095515
return;
@@ -5517,6 +5523,10 @@ export class LuaTransformer {
55175523
this.importLuaLibFeature(LuaLibFeature.WeakSet);
55185524
return;
55195525
}
5526+
5527+
if (tsHelper.isBuiltinErrorTypeName(name)) {
5528+
this.importLuaLibFeature(LuaLibFeature.Error);
5529+
}
55205530
}
55215531
}
55225532

src/TSHelper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ const defaultArrayCallMethodNames = new Set<string>([
3434
"flatMap",
3535
]);
3636

37+
// TODO [2019-09-27/Perry]: Refactor lualib detection to consistent map
38+
const builtinErrorTypeNames = new Set([
39+
"Error",
40+
"ErrorConstructor",
41+
"RangeError",
42+
"RangeErrorConstructor",
43+
"ReferenceError",
44+
"ReferenceErrorConstructor",
45+
"SyntaxError",
46+
"SyntaxErrorConstructor",
47+
"TypeError",
48+
"TypeErrorConstructor",
49+
"URIError",
50+
"URIErrorConstructor",
51+
]);
52+
3753
export function getExtendedTypeNode(
3854
node: ts.ClassLikeDeclarationBase,
3955
checker: ts.TypeChecker
@@ -1052,3 +1068,7 @@ export function formatPathToLuaPath(filePath: string): string {
10521068
}
10531069
return filePath.replace(/\.\//g, "").replace(/\//g, ".");
10541070
}
1071+
1072+
export function isBuiltinErrorTypeName(name: string): boolean {
1073+
return builtinErrorTypeNames.has(name);
1074+
}

src/TSTLErrors.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ export const InvalidPropertyCall = (node: ts.Node) =>
5454
export const InvalidElementCall = (node: ts.Node) =>
5555
new TranspileError(`Tried to transpile a non-element call as an element call.`, node);
5656

57-
export const InvalidThrowExpression = (node: ts.Node) =>
58-
new TranspileError(`Invalid throw expression, only strings can be thrown.`, node);
59-
6057
export const ForbiddenStaticClassPropertyName = (node: ts.Node, name: string) =>
6158
new TranspileError(`Cannot use "${name}" as a static class property or method name.`, node);
6259

src/lualib/Error.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interface ErrorType {
2+
name: string;
3+
new (...args: any[]): Error;
4+
}
5+
6+
function __TS__GetErrorStack(constructor: Function): string {
7+
let level = 1;
8+
while (true) {
9+
const info = debug.getinfo(level, "f");
10+
level += 1;
11+
if (!info) {
12+
// constructor is not in call stack
13+
level = 1;
14+
break;
15+
} else if (info.func === constructor) {
16+
break;
17+
}
18+
}
19+
20+
return debug.traceback(undefined, level);
21+
}
22+
23+
function __TS__WrapErrorToString<T extends Error>(getDescription: (this: T) => string): (this: T) => string {
24+
return function(this: Error): string {
25+
const description = getDescription.call(this);
26+
const caller = debug.getinfo(3, "f");
27+
if (_VERSION === "Lua 5.1" || (caller && caller.func !== error)) {
28+
return description;
29+
} else {
30+
return `${description}\n${this.stack}`;
31+
}
32+
};
33+
}
34+
35+
function __TS__InitErrorClass(Type: ErrorType, name: string): any {
36+
Type.name = name;
37+
return setmetatable(Type, {
38+
__call: (_self: any, message: string) => new Type(message),
39+
});
40+
}
41+
42+
Error = __TS__InitErrorClass(
43+
class implements Error {
44+
public name = "Error";
45+
public stack: string;
46+
47+
constructor(public message = "") {
48+
this.stack = __TS__GetErrorStack((this.constructor as any).new);
49+
const metatable = getmetatable(this);
50+
if (!metatable.__errorToStringPatched) {
51+
metatable.__errorToStringPatched = true;
52+
metatable.__tostring = __TS__WrapErrorToString(metatable.__tostring);
53+
}
54+
}
55+
56+
public toString(): string {
57+
return this.message !== "" ? `${this.name}: ${this.message}` : this.name;
58+
}
59+
},
60+
"Error"
61+
);
62+
63+
for (const errorName of ["RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]) {
64+
globalThis[errorName] = __TS__InitErrorClass(
65+
class extends Error {
66+
public name = errorName;
67+
},
68+
errorName
69+
);
70+
}

src/lualib/declarations/debug.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
/** @noSelfInFile */
22

3+
declare const _VERSION: string;
4+
declare function error(...args: any[]): never;
5+
36
declare namespace debug {
47
function traceback(...args: any[]): string;
8+
9+
interface FunctionInfo<T extends Function = Function> {
10+
func: T;
11+
name?: string;
12+
namewhat: "global" | "local" | "method" | "field" | "";
13+
source: string;
14+
short_src: string;
15+
linedefined: number;
16+
lastlinedefined: number;
17+
what: "Lua" | "C" | "main";
18+
currentline: number;
19+
nups: number;
20+
}
21+
22+
function getinfo(i: number, what?: string): Partial<FunctionInfo>;
523
}

src/lualib/declarations/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare function type(
1010
value: any
1111
): "nil" | "number" | "string" | "boolean" | "table" | "function" | "thread" | "userdata";
1212
declare function setmetatable<T extends object>(table: T, metatable: any): T;
13+
declare function getmetatable<T extends object>(table: T): any;
1314
declare function rawget<T, K extends keyof T>(table: T, key: K): T[K];
1415
declare function rawset<T, K extends keyof T>(table: T, key: K, val: T[K]): void;
1516
/** @tupleReturn */

test/unit/error.spec.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as TSTLErrors from "../../src/TSTLErrors";
21
import * as util from "../util";
32

43
test("throwString", () => {
@@ -7,12 +6,6 @@ test("throwString", () => {
76
`.expectToEqual(new util.ExecutionError("Some Error"));
87
});
98

10-
test("throwError", () => {
11-
util.testFunction`
12-
throw Error("Some Error")
13-
`.expectToHaveDiagnosticOfError(TSTLErrors.InvalidThrowExpression(util.nodeStub));
14-
});
15-
169
test.skip.each([0, 1, 2])("re-throw (%p)", i => {
1710
util.testFunction`
1811
const i: number = ${i};
@@ -292,3 +285,54 @@ test("return from nested finally", () => {
292285
`;
293286
expect(util.transpileAndExecute(code)).toBe("finally AB");
294287
});
288+
289+
test.each([
290+
`"error string"`,
291+
`42`,
292+
`3.141`,
293+
`true`,
294+
`false`,
295+
`undefined`,
296+
`{ x: "error object" }`,
297+
`() => "error function"`,
298+
])("throw and catch %s", error => {
299+
util.testFunction`
300+
try {
301+
throw ${error};
302+
} catch (error) {
303+
if (typeof error == 'function') {
304+
return error();
305+
} else {
306+
return error;
307+
}
308+
}
309+
`.expectToMatchJsResult();
310+
});
311+
312+
const builtinErrors = ["Error", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"];
313+
314+
test.each([...builtinErrors, ...builtinErrors.map(type => `new ${type}`)])("%s properties", errorType => {
315+
util.testFunction`
316+
const error = ${errorType}();
317+
return { name: error.name, message: error.message, string: error.toString() };
318+
`.expectToMatchJsResult();
319+
});
320+
321+
test.each([...builtinErrors, "CustomError"])("get stack from %s", errorType => {
322+
const stack = util.testFunction`
323+
class CustomError extends Error {
324+
public name = "CustomError";
325+
}
326+
327+
let stack: string | undefined;
328+
329+
function innerFunction() { stack = new ${errorType}().stack; }
330+
function outerFunction() { innerFunction(); }
331+
outerFunction();
332+
333+
return stack;
334+
`.getLuaExecutionResult();
335+
336+
expect(stack).toMatch("innerFunction");
337+
expect(stack).toMatch("outerFunction");
338+
});

test/unit/identifiers.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => {
356356
const error = "foobar";
357357
throw error;`;
358358

359-
expect(() => util.transpileAndExecute(code)).toThrow(/^LUA ERROR: .+ foobar$/);
359+
expect(() => util.transpileAndExecute(code)).toThrow(/^LUA ERROR: foobar$/);
360360
});
361361

362362
test("variable (assert)", () => {

0 commit comments

Comments
 (0)
X Tutup