diff --git a/build_lualib.ts b/build_lualib.ts index cca39f055..253fa59d5 100644 --- a/build_lualib.ts +++ b/build_lualib.ts @@ -8,11 +8,15 @@ import { LuaLib } from "./src/LuaLib"; const options: tstl.CompilerOptions = { skipLibCheck: true, types: [], + target: ts.ScriptTarget.ESNext, + lib: ["lib.esnext.d.ts"], + + outDir: path.join(__dirname, "./dist/lualib"), + rootDir: path.join(__dirname, "./src/lualib"), + luaLibImport: tstl.LuaLibImportKind.None, luaTarget: tstl.LuaTarget.Lua51, noHeader: true, - outDir: path.join(__dirname, "./dist/lualib"), - rootDir: path.join(__dirname, "./src/lualib"), }; // TODO: Check diagnostics diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 525bfc46c..a42ec07dd 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -27,6 +27,7 @@ export enum LuaLibFeature { FunctionCall = "FunctionCall", Index = "Index", InstanceOf = "InstanceOf", + InstanceOfObject = "InstanceOfObject", Iterator = "Iterator", Map = "Map", NewIndex = "NewIndex", @@ -51,6 +52,7 @@ export enum LuaLibFeature { const luaLibDependencies: {[lib in LuaLibFeature]?: LuaLibFeature[]} = { ArrayFlat: [LuaLibFeature.ArrayConcat], ArrayFlatMap: [LuaLibFeature.ArrayConcat], + InstanceOf: [LuaLibFeature.Symbol], Iterator: [LuaLibFeature.Symbol], ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol], Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index e782f96c9..75392d1ad 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2658,14 +2658,18 @@ export class LuaTransformer { ); case ts.SyntaxKind.InstanceOfKeyword: - const decorators = tsHelper.getCustomDecorators( - this.checker.getTypeAtLocation(expression.right), - this.checker - ); + const rhsType = this.checker.getTypeAtLocation(expression.right); + const decorators = tsHelper.getCustomDecorators(rhsType, this.checker); + if (decorators.has(DecoratorKind.Extension) || decorators.has(DecoratorKind.MetaExtension)) { // Cannot use instanceof on extension classes throw TSTLErrors.InvalidInstanceOfExtension(expression); } + + if (tsHelper.isStandardLibraryType(rhsType, "ObjectConstructor", this.program)) { + return this.transformLuaLibFunction(LuaLibFeature.InstanceOfObject, expression, lhs); + } + return this.transformLuaLibFunction(LuaLibFeature.InstanceOf, expression, lhs, rhs); case ts.SyntaxKind.CommaToken: @@ -4533,11 +4537,6 @@ export class LuaTransformer { } private importLuaLibFeature(feature: LuaLibFeature): void { - // Add additional lib requirements - if (feature === LuaLibFeature.Map || feature === LuaLibFeature.Set) { - this.luaLibFeatureSet.add(LuaLibFeature.InstanceOf); - } - this.luaLibFeatureSet.add(feature); } diff --git a/src/lualib/InstanceOf.ts b/src/lualib/InstanceOf.ts index b59674943..6f6dd1c3c 100644 --- a/src/lualib/InstanceOf.ts +++ b/src/lualib/InstanceOf.ts @@ -7,6 +7,15 @@ interface LuaObject { } function __TS__InstanceOf(this: void, obj: LuaObject, classTbl: LuaClass): boolean { + if (typeof classTbl !== "object") { + // tslint:disable-next-line: no-string-throw + throw "Right-hand side of 'instanceof' is not an object"; + } + + if (classTbl[Symbol.hasInstance] !== undefined) { + return !!classTbl[Symbol.hasInstance](obj); + } + if (obj !== undefined) { let luaClass = obj.constructor; while (luaClass !== undefined) { diff --git a/src/lualib/InstanceOfObject.ts b/src/lualib/InstanceOfObject.ts new file mode 100644 index 000000000..681e243c5 --- /dev/null +++ b/src/lualib/InstanceOfObject.ts @@ -0,0 +1,4 @@ +function __TS__InstanceOfObject(this: void, value: unknown): boolean { + const valueType = type(value); + return valueType === "table" || valueType === "function"; +} diff --git a/src/lualib/Symbol.ts b/src/lualib/Symbol.ts index 5c7bf0a7c..ef110af61 100644 --- a/src/lualib/Symbol.ts +++ b/src/lualib/Symbol.ts @@ -17,4 +17,5 @@ function __TS__Symbol(description?: string | number): symbol { Symbol = { iterator: __TS__Symbol('Symbol.iterator'), + hasInstance: __TS__Symbol('Symbol.hasInstance'), } as any; diff --git a/src/lualib/declarations/global.d.ts b/src/lualib/declarations/global.d.ts new file mode 100644 index 000000000..6d88460ed --- /dev/null +++ b/src/lualib/declarations/global.d.ts @@ -0,0 +1,4 @@ +declare function type( + this: void, + value: any +): "nil" | "number" | "string" | "boolean" | "table" | "function" | "thread" | "userdata"; diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts index e52337405..8088994b0 100644 --- a/test/unit/typechecking.spec.ts +++ b/test/unit/typechecking.spec.ts @@ -50,7 +50,7 @@ test("instanceof", () => { "class myClass {} let inst = new myClass(); return inst instanceof myClass;", ); - expect(result).toBeTruthy(); + expect(result).toBe(true); }); test("instanceof inheritance", () => { @@ -60,7 +60,7 @@ test("instanceof inheritance", () => { let inst = new childClass(); return inst instanceof myClass; `); - expect(result).toBeTruthy(); + expect(result).toBe(true); }); test("instanceof inheritance false", () => { @@ -73,15 +73,33 @@ test("instanceof inheritance false", () => { expect(result).toBe(false); }); +test("{} instanceof Object", () => { + const result = util.transpileAndExecute("return {} instanceof Object;"); + + expect(result).toBe(true); +}); + +test("function instanceof Object", () => { + const result = util.transpileAndExecute("return (() => {}) instanceof Object;"); + + expect(result).toBe(true); +}); + test("null instanceof Object", () => { - const result = util.transpileAndExecute("return (null) instanceof Object;"); + const result = util.transpileAndExecute("return (null as any) instanceof Object;"); expect(result).toBe(false); }); +test("instanceof undefined", () => { + expect(() => { + util.transpileAndExecute("return {} instanceof (undefined as any);"); + }).toThrow("Right-hand side of 'instanceof' is not an object"); +}); + test("null instanceof Class", () => { const result = util.transpileAndExecute( - "class myClass {} return (null) instanceof myClass;", + "class myClass {} return (null as any) instanceof myClass;", ); expect(result).toBe(false); @@ -108,5 +126,23 @@ test("instanceof export", () => { "result", ); - expect(result).toBeTruthy(); + expect(result).toBe(true); +}); + +test("instanceof Symbol.hasInstance", () => { + const result = util.transpileAndExecute(` + class myClass { + static [Symbol.hasInstance]() { + return false; + } + } + + const inst = new myClass(); + const isInstanceOld = inst instanceof myClass; + myClass[Symbol.hasInstance] = () => true; + const isInstanceNew = inst instanceof myClass; + return isInstanceOld !== isInstanceNew; + `); + + expect(result).toBe(true); });