From 9a8d1b1f5449e213115fcbfa3f12b4e9b8a79964 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Tue, 5 Nov 2024 21:26:04 +0100 Subject: [PATCH 1/2] fix(callable): __call cannot be in a nested metatable --- CHANGELOG.md | 4 ++++ lua/pl/types.lua | 20 ++++++++++++++++---- tests/test-types.lua | 10 ++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e52ab1e..a96fd10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ deprecation policy. see [CONTRIBUTING.md](CONTRIBUTING.md#release-instructions-for-a-new-version) for release instructions +## unreleased + - fix(types): callable would return false positive if `__call` was nested + [#489](https://github.com/lunarmodules/Penlight/pull/489) + ## 1.14.0 (2024-Apr-15) - fix(path): make `path.expanduser` more sturdy [#469](https://github.com/lunarmodules/Penlight/pull/469) diff --git a/lua/pl/types.lua b/lua/pl/types.lua index 35b0ccb5..ce82efa7 100644 --- a/lua/pl/types.lua +++ b/lua/pl/types.lua @@ -8,10 +8,22 @@ local math_ceil = math.ceil local assert_arg = utils.assert_arg local types = {} ---- is the object either a function or a callable object?. --- @param obj Object to check. -function types.is_callable (obj) - return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call and true +do + -- we prefer debug.getmetatable, but only if available + local gmt = (debug or {}).getmetatable or getmetatable + + --- is the object either a function or a callable object?. + -- @param obj Object to check. + function types.is_callable (obj) + if type(obj) == 'function' then + return true + end + local mt = gmt(obj) + if not mt then + return false + end + return type(rawget(mt, "__call")) == "function" + end end --- is the object of the specified type?. diff --git a/tests/test-types.lua b/tests/test-types.lua index bfb3c8fc..df2566c0 100644 --- a/tests/test-types.lua +++ b/tests/test-types.lua @@ -32,6 +32,16 @@ asserteq(types.is_integer(-10.1),false) asserteq(types.is_callable(asserteq),true) asserteq(types.is_callable(List),true) +do + local mt = setmetatable({}, { + __index = { + __call = function() end + } + }) + asserteq(type(mt.__call), "function") -- __call is looked-up through another metatable + local nc = setmetatable({}, mt) + asserteq(types.is_callable(nc), false) -- NOT callable, since __call is fetched using RAWget by Lua +end asserteq(types.is_indexable(array),true) asserteq(types.is_indexable('hello'),nil) From 5f94f1faa77744cbc55413245ceed7aae96db265 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Wed, 6 Nov 2024 09:18:07 +0100 Subject: [PATCH 2/2] verify expected results --- tests/test-types.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test-types.lua b/tests/test-types.lua index df2566c0..7cf313de 100644 --- a/tests/test-types.lua +++ b/tests/test-types.lua @@ -35,11 +35,16 @@ asserteq(types.is_callable(List),true) do local mt = setmetatable({}, { __index = { - __call = function() end + __call = function() return "ok" end } }) asserteq(type(mt.__call), "function") -- __call is looked-up through another metatable local nc = setmetatable({}, mt) + -- proof-of-pudding, let's call it. To verify Lua behaves the same on all engines + local success, result = pcall(function() return nc() end) + assert(result ~= "ok", "expected result to not be 'ok'") + asserteq(success, false) + -- real test now asserteq(types.is_callable(nc), false) -- NOT callable, since __call is fetched using RAWget by Lua end