diff --git a/CHANGELOG.md b/CHANGELOG.md index eceb69bb4..1311e01d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - Fixed iteration over generators stopping at first yielded `nil` value +- Fixed extending a class not keeping `toString` implementation from a super class + ## 0.33.0 - Added support for nullish coalescing `A ?? B`. diff --git a/src/lualib/ClassExtends.ts b/src/lualib/ClassExtends.ts index e297cba93..0df851629 100644 --- a/src/lualib/ClassExtends.ts +++ b/src/lualib/ClassExtends.ts @@ -16,4 +16,5 @@ function __TS__ClassExtends(this: void, target: LuaClass, base: LuaClass): void // Re-add metatable events defined by accessors with `__TS__SetDescriptor` if (typeof base.prototype.__index === "function") target.prototype.__index = base.prototype.__index; if (typeof base.prototype.__newindex === "function") target.prototype.__newindex = base.prototype.__newindex; + if (typeof base.prototype.__tostring === "function") target.prototype.__tostring = base.prototype.__tostring; } diff --git a/src/lualib/declarations/tstl.d.ts b/src/lualib/declarations/tstl.d.ts index a983ee66d..57e436ae3 100644 --- a/src/lualib/declarations/tstl.d.ts +++ b/src/lualib/declarations/tstl.d.ts @@ -10,6 +10,7 @@ interface Metatable { _descriptors?: Record; __index?: any; __newindex?: any; + __tostring?: any; } interface LuaClass extends Metatable { diff --git a/test/unit/builtins/object.spec.ts b/test/unit/builtins/object.spec.ts index 067da81f6..b4866d2e7 100644 --- a/test/unit/builtins/object.spec.ts +++ b/test/unit/builtins/object.spec.ts @@ -28,3 +28,96 @@ test.each(["[]", '[["a", 1], ["b", 2]]', '[["a", 1], ["a", 2]]', 'new Map([["foo util.testExpression`Object.fromEntries(${entries})`.expectToMatchJsResult(); } ); + +describe(".toString()", () => { + const toStringDeclaration = ` + function toString(value: object) { + const result = value.toString(); + return result === "[object Object]" || result.startsWith("table: ") ? "table" : result; + } + `; + + test("class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + return toString(new A()); + `.expectToMatchJsResult(); + }); + + test("inherited class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + class B extends A {} + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); + + test("don't affect inherited class", () => { + util.testFunction` + ${toStringDeclaration} + class A {} + + class B extends A { + public toString() { + return "B"; + } + } + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); + + test("override inherited class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + class B extends A { + public toString() { + return "B"; + } + } + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); +}); + +describe(".hasOwnProperty()", () => { + test("class field", () => { + util.testFunction` + class A { + public field = true; + } + + return new A().hasOwnProperty("field"); + `.expectToMatchJsResult(); + }); + + test("class method", () => { + util.testFunction` + class A { + public method() {} + } + + return new A().hasOwnProperty("method"); + `.expectToMatchJsResult(); + }); +}); diff --git a/test/unit/classes/classes.spec.ts b/test/unit/classes/classes.spec.ts index 52bcdd0fc..f4c14df34 100644 --- a/test/unit/classes/classes.spec.ts +++ b/test/unit/classes/classes.spec.ts @@ -352,42 +352,6 @@ test("ClassComputedMethodCall", () => { `.expectToMatchJsResult(); }); -test("ClassToString", () => { - util.testFunction` - class a { - public toString(): string { - return "instance of a"; - } - } - let inst = new a(); - return inst.toString(); - `.expectToMatchJsResult(); -}); - -test("HasOwnProperty true", () => { - util.testFunction` - class a { - public test(): void { - } - } - let inst = new a(); - inst["prop"] = 17; - return inst.hasOwnProperty("prop"); - `.expectToMatchJsResult(); -}); - -test("HasOwnProperty false", () => { - util.testFunction` - class a { - public test(): void { - } - } - let inst = new a(); - inst["prop"] = 17; - return inst.hasOwnProperty("test"); - `.expectToMatchJsResult(); -}); - test("CastClassMethodCall", () => { util.testFunction` interface result