From d95c4d286116611caf029a1f9a86ac03fbe90081 Mon Sep 17 00:00:00 2001 From: mcclure Date: Tue, 22 Sep 2020 09:36:07 -0400 Subject: [PATCH] Allow inheritance of _init from more than one level up (#289) * Allow inheritance of _init from more than one level up Currently _init is exempted from "fat inheritance", and _init inheritance works by a strange workaround where the base class exactly one level up is checked for _init at constructor time. This appears to be done so that :super() can work properly, but it means that _init cannot be inherited from a grandparent. This patch adds a _parent_with_init field to all relevant classes which points to the most recent ancestor with an _init implementation. This simplifies both __call and _class and might actually be a performance improvement (it removes a loop). * Add changelog line and tests for grandparent _init * Add tests: Invocation of a : function inherited from a grandparent; and ensure :super() fails when it ought to fail. * Prevent a stack overflow when a particular class :super() edge case is hit. This causes the "createK" test in test-class.lua to fail with a "null function" error instead of a stack overflow. * Improved fix for :super infinite loop problem (avoid second rawset) --- CHANGELOG.md | 6 ++++ lua/pl/class.lua | 36 +++++++++++---------- tests/test-class.lua | 75 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fea884..d5ecccda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.x.x (upcoming) + +## Fixes + + - In `pl.class`, `_init` can now be inherited from grandparent (or older ancestor) classes. (#289) + ## 1.8.0 (2020-08-05) ### New features diff --git a/lua/pl/class.lua b/lua/pl/class.lua index 847f442b..49246ee3 100644 --- a/lua/pl/class.lua +++ b/lua/pl/class.lua @@ -17,23 +17,29 @@ local compat -- this trickery is necessary to prevent the inheritance of 'super' and -- the resulting recursive call problems. local function call_ctor (c,obj,...) - -- nice alias for the base class ctor - local base = rawget(c,'_base') - if base then - local parent_ctor = rawget(base,'_init') - while not parent_ctor do - base = rawget(base,'_base') - if not base then break end - parent_ctor = rawget(base,'_init') + local init = rawget(c,'_init') + local parent_with_init = rawget(c,'_parent_with_init') + + if parent_with_init then + if not init then -- inheriting an init + init = rawget(parent_with_init, '_init') + parent_with_init = rawget(parent_with_init, '_parent_with_init') end - if parent_ctor then + if parent_with_init then -- super() points to one above whereever _init came from rawset(obj,'super',function(obj,...) - call_ctor(base,obj,...) + call_ctor(parent_with_init,obj,...) end) end + else + -- Without this, calling super() where none exists will sometimes loop and stack overflow + rawset(obj,'super',nil) end - local res = c._init(obj,...) - rawset(obj,'super',nil) + + local res = init(obj,...) + if parent_with_init then -- If this execution of call_ctor set a super, unset it + rawset(obj,'super',nil) + end + return res end @@ -146,6 +152,7 @@ local function _class(base,c_arg,c) c.__index = c setmetatable(c,mt) if not plain then + if base and rawget(base,'_init') then c._parent_with_init = base end -- For super and inherited init c._init = nil end @@ -160,15 +167,12 @@ local function _class(base,c_arg,c) if not obj then obj = {} end setmetatable(obj,c) - if rawget(c,'_init') then -- explicit constructor + if rawget(c,'_init') or rawget(c,'_parent_with_init') then -- constructor exists local res = call_ctor(c,obj,...) if res then -- _if_ a ctor returns a value, it becomes the object... obj = res setmetatable(obj,c) end - elseif base and rawget(base,'_init') then -- default constructor - -- make sure that any stuff from the base class is initialized! - call_ctor(base,obj,...) end if base and rawget(base,'_post_init') then diff --git a/tests/test-class.lua b/tests/test-class.lua index 43dc4dcc..86210053 100644 --- a/tests/test-class.lua +++ b/tests/test-class.lua @@ -27,6 +27,10 @@ function B:foo () self.eee = 1 end +function B:foo2 () + self.g = 8 +end + asserteq(B(),{a=1,b=2}) -- can continue this chain @@ -47,6 +51,77 @@ c = C() c:foo() asserteq(c,{a=1,b=2,c=3,eee=1}) + +-- test indirect inherit + +D = class(C) + +E = class(D) + +function E:_init () + self:super() + self.e = 4 +end + +function E:foo () + -- recommended way to call inherited version of method... + self.eeee = 5 + C.foo(self) +end + +F = class(E) + +function F:_init () + self:super() + self.f = 6 +end + +f = F() +f:foo() +f:foo2() -- Test : invocation inherits this function from all the way up in B + +asserteq(f,{a=1,b=2,c=3,eee=1,e=4,eeee=5,f=6,g=8}) + +-- Test that inappropriate calls to super() fail gracefully + +G = class() -- Class with no init + +H = class(G) -- Class with an init that wrongly calls super() + +function H:_init() + self:super() -- Notice: G has no _init +end + +I = class(H) -- Inherits the init with a bad super +J = class(I) -- Grandparent-inits the init with a bad super + +K = class(J) -- Has an init, which calls the init with a bad super + +function K:_init() + self:super() +end + +local function createG() + return G() +end + +local function createH() -- Wrapper function for pcall + return H() +end + +local function createJ() + return J() +end + +local function createK() + return K() +end + +assert(pcall(createG)) -- Should succeed +assert(not pcall(createH)) -- These three should fail +assert(not pcall(createJ)) +assert(not pcall(createK)) + --- class methods! assert(c:is_a(C)) assert(c:is_a(B))