diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index 20158462..c2354f0f 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -7640,7 +7640,7 @@ inline std::optional try_call_index_extensible(lua_State* L, const char* ke } template -inline std::optional try_call_parent_index_fallback(lua_State* L, const char* key) +inline std::optional try_call_parent_index_extensibles(lua_State* L, const char* key) { LUABRIDGE_ASSERT(lua_istable(L, -1)); @@ -7677,6 +7677,40 @@ inline std::optional try_call_parent_index_fallback(lua_State* L, const cha } } + lua_pop(L, 1); + } + + lua_pop(L, 1); + return std::nullopt; +} + +template +inline std::optional try_call_parent_index_fallbacks(lua_State* L, const char* key) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + if (key == nullptr) + return std::nullopt; + + lua_rawgetp_x(L, -1, getParentKey()); + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + lua_rawgetp_x(L, -1, getIndexFallbackKey()); if (lua_iscfunction(L, -1)) { @@ -7727,15 +7761,24 @@ inline int index_metamethod(lua_State* L) for (;;) { + const Options options = get_class_options(L, -1); + + // For static __index: the static fallback takes priority over registered static + // property getters so that a user-defined static __index fallback can shadow + // static properties. + // For instance __index with allowOverridingMethods: the instance fallback takes + // priority over class-table Lua methods, enabling Lua-side method overrides via + // __newindex. if constexpr (IsObject) { - - if (auto result = try_call_index_fallback(L)) - return *result; + if (options.test(extensibleClass | allowOverridingMethods)) + { + if (auto result = try_call_index_fallback(L)) + return *result; + } } else { - if (auto result = try_call_static_index_fallback(L)) return *result; } @@ -7773,7 +7816,6 @@ inline int index_metamethod(lua_State* L) LUABRIDGE_ASSERT(lua_isnil(L, -1)); lua_pop(L, 1); - const Options options = get_class_options(L, -1); if (options.test(extensibleClass | allowOverridingMethods)) { if (auto result = try_call_index_extensible(L, key)) @@ -7866,7 +7908,21 @@ inline int index_metamethod(lua_State* L) return *result; } - if (auto result = try_call_parent_index_fallback(L, key)) + if (auto result = try_call_parent_index_extensibles(L, key)) + return *result; + + if constexpr (IsObject) + { + if (auto result = try_call_index_fallback(L)) + return *result; + } + else + { + if (auto result = try_call_static_index_fallback(L)) + return *result; + } + + if (auto result = try_call_parent_index_fallbacks(L, key)) return *result; lua_pop(L, 1); @@ -8262,7 +8318,7 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* } template -inline std::optional try_call_parent_newindex(lua_State* L) +inline std::optional try_call_parent_newindex_setters(lua_State* L) { LUABRIDGE_ASSERT(lua_istable(L, -1)); @@ -8312,6 +8368,37 @@ inline std::optional try_call_parent_newindex(lua_State* L) lua_pop(L, 1); } + lua_pop(L, 1); + } + + lua_pop(L, 1); + return std::nullopt; +} + +template +inline std::optional try_call_parent_newindex_fallbacks(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + lua_rawgetp_x(L, -1, getParentKey()); + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + lua_rawgetp_x(L, -1, getNewIndexFallbackKey()); if (lua_iscfunction(L, -1)) { @@ -8367,6 +8454,9 @@ inline int newindex_metamethod(lua_State* L) lua_pop(L, 1); + if (auto result = try_call_parent_newindex_setters(L)) + return *result; + if constexpr (IsObject) { if (auto result = try_call_newindex_fallback(L)) @@ -8392,27 +8482,7 @@ inline int newindex_metamethod(lua_State* L) } } - lua_rawgetp_x(L, -1, getPropsetKey()); - if (lua_istable(L, -1)) - { - lua_pushvalue(L, 2); - lua_rawget(L, -2); - lua_remove(L, -2); - - if (lua_iscfunction(L, -1)) - { - lua_remove(L, -2); - if constexpr (IsObject) - lua_pushvalue(L, 1); - lua_pushvalue(L, 3); - lua_call(L, IsObject ? 2 : 1, 0); - return 0; - } - } - - lua_pop(L, 1); - - if (auto result = try_call_parent_newindex(L)) + if (auto result = try_call_parent_newindex_fallbacks(L)) return *result; if constexpr (IsObject) @@ -9454,36 +9524,44 @@ void push_class_property_getter(lua_State* L, T (U::*value), const char* debugna lua_pushcclosure_x(L, &property_getter::call, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)() const, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)() const, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)() const noexcept, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)() const noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)(lua_State*) const, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)(lua_State*) const, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)(lua_State*) const noexcept, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)(lua_State*) const noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); @@ -9609,36 +9687,44 @@ void push_class_property_setter(lua_State* L, T U::*value, const char* debugname lua_pushcclosure_x(L, &property_setter::call, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T), const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T), const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T) noexcept, const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T) noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T, lua_State*), const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T, lua_State*), const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T, lua_State*) noexcept, const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T, lua_State*) noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index d7c3fe56..116e9b20 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -390,8 +390,14 @@ inline std::optional try_call_index_extensible(lua_State* L, const char* ke return std::nullopt; } +/** + * @brief Scan the flattened parent list for an extensible class getter. + * + * Only extensible checks are performed; __index fallbacks are intentionally ignored so that a + * registered getter anywhere in the hierarchy always takes priority over any fallback. + */ template -inline std::optional try_call_parent_index_fallback(lua_State* L, const char* key) +inline std::optional try_call_parent_index_extensibles(lua_State* L, const char* key) { LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt @@ -428,6 +434,46 @@ inline std::optional try_call_parent_index_fallback(lua_State* L, const cha } } + lua_pop(L, 1); // Stack: mt, parent list + } + + lua_pop(L, 1); // Stack: mt + return std::nullopt; +} + +/** + * @brief Scan the flattened parent list for a __index fallback. + * + * Only fallbacks are checked; extensible getters are intentionally ignored because they + * have already been searched by try_call_parent_index_extensibles. + */ +template +inline std::optional try_call_parent_index_fallbacks(lua_State* L, const char* key) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt + + if (key == nullptr) + return std::nullopt; + + lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent list | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, parent list, parent mt + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + lua_rawgetp_x(L, -1, getIndexFallbackKey()); // Stack: mt, parent list, parent mt, ifb | nil if (lua_iscfunction(L, -1)) { @@ -479,15 +525,24 @@ inline int index_metamethod(lua_State* L) for (;;) { + const Options options = get_class_options(L, -1); // Stack: mt + + // For static __index: the static fallback takes priority over registered static + // property getters so that a user-defined static __index fallback can shadow + // static properties. + // For instance __index with allowOverridingMethods: the instance fallback takes + // priority over class-table Lua methods, enabling Lua-side method overrides via + // __newindex. if constexpr (IsObject) { - // Repeat the lookup in the index fallback - if (auto result = try_call_index_fallback(L)) - return *result; + if (options.test(extensibleClass | allowOverridingMethods)) + { + if (auto result = try_call_index_fallback(L)) + return *result; + } } else { - // Repeat the lookup in the static index fallback if (auto result = try_call_static_index_fallback(L)) return *result; } @@ -527,7 +582,6 @@ inline int index_metamethod(lua_State* L) lua_pop(L, 1); // Stack: mt // Repeat the lookup in the index extensible, for method overrides - const Options options = get_class_options(L, -1); // Stack: mt if (options.test(extensibleClass | allowOverridingMethods)) { if (auto result = try_call_index_extensible(L, key)) @@ -624,7 +678,25 @@ inline int index_metamethod(lua_State* L) return *result; } - if (auto result = try_call_parent_index_fallback(L, key)) + // Before consulting any __index fallback, scan the entire parent hierarchy for a + // matching extensible getter. This ensures that a registered getter anywhere in the + // inheritance chain always takes priority over a __index fallback defined at a + // narrower scope (e.g. an intermediate or leaf class). + if (auto result = try_call_parent_index_extensibles(L, key)) + return *result; + + if constexpr (IsObject) + { + if (auto result = try_call_index_fallback(L)) + return *result; + } + else + { + if (auto result = try_call_static_index_fallback(L)) + return *result; + } + + if (auto result = try_call_parent_index_fallbacks(L, key)) return *result; lua_pop(L, 1); // Stack: - @@ -1045,8 +1117,14 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* return 0; } +/** + * @brief Scan the flattened parent list for a property setter matching the key (stack[2]). + * + * Only setters are checked; __newindex fallbacks are intentionally ignored so that a + * registered property anywhere in the hierarchy always takes priority over any fallback. + */ template -inline std::optional try_call_parent_newindex(lua_State* L) +inline std::optional try_call_parent_newindex_setters(lua_State* L) { LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt @@ -1096,6 +1174,43 @@ inline std::optional try_call_parent_newindex(lua_State* L) lua_pop(L, 1); // Stack: mt, parent list, parent mt } + lua_pop(L, 1); // Stack: mt, parent list + } + + lua_pop(L, 1); // Stack: mt + return std::nullopt; +} + +/** + * @brief Scan the flattened parent list for a __newindex fallback. + * + * Only fallbacks are checked; property setters are intentionally ignored because they + * have already been searched by try_call_parent_newindex_setters. + */ +template +inline std::optional try_call_parent_newindex_fallbacks(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt + + lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent list | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, parent list, parent mt + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + lua_rawgetp_x(L, -1, getNewIndexFallbackKey()); // Stack: mt, parent list, parent mt, nifb | nil if (lua_iscfunction(L, -1)) { @@ -1152,6 +1267,13 @@ inline int newindex_metamethod(lua_State* L) lua_pop(L, 1); // Stack: mt + // Before consulting any __newindex fallback, scan the entire parent hierarchy for a + // matching property setter. This ensures that a registered property anywhere in the + // inheritance chain always takes priority over a __newindex fallback defined at a + // narrower scope (e.g. an intermediate or leaf class). + if (auto result = try_call_parent_newindex_setters(L)) + return *result; + if constexpr (IsObject) { if (auto result = try_call_newindex_fallback(L)) @@ -1178,28 +1300,7 @@ inline int newindex_metamethod(lua_State* L) } } - // Try in the propget key - lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, propset table (ps) - if (lua_istable(L, -1)) - { - lua_pushvalue(L, 2); // Stack: mt, ps, field name - lua_rawget(L, -2); // Stack: mt, ps, setter | nil - lua_remove(L, -2); // Stack: mt, setter | nil - - if (lua_iscfunction(L, -1)) // Stack: mt, setter - { - lua_remove(L, -2); // Stack: setter - if constexpr (IsObject) - lua_pushvalue(L, 1); // Stack: setter, table | userdata - lua_pushvalue(L, 3); // Stack: setter, table | userdata, new value - lua_call(L, IsObject ? 2 : 1, 0); // Stack: - - return 0; - } - } - - lua_pop(L, 1); // Stack: mt - - if (auto result = try_call_parent_newindex(L)) + if (auto result = try_call_parent_newindex_fallbacks(L)) return *result; if constexpr (IsObject) @@ -2396,36 +2497,44 @@ void push_class_property_getter(lua_State* L, T (U::*value), const char* debugna lua_pushcclosure_x(L, &property_getter::call, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)() const, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)() const, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)() const noexcept, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)() const noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)(lua_State*) const, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)(lua_State*) const, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)(lua_State*) const noexcept, const char* debugname) +template +void push_class_property_getter(lua_State* L, T (B::*getter)(lua_State*) const noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using GetType = decltype(getter); new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); @@ -2559,36 +2668,44 @@ void push_class_property_setter(lua_State* L, T U::*value, const char* debugname lua_pushcclosure_x(L, &property_setter::call, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T), const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T), const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T) noexcept, const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T) noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T, lua_State*), const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T, lua_State*), const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); } -template -void push_class_property_setter(lua_State* L, void (C::*setter)(T, lua_State*) noexcept, const char* debugname) +template +void push_class_property_setter(lua_State* L, void (B::*setter)(T, lua_State*) noexcept, const char* debugname) { + static_assert(std::is_same_v || std::is_base_of_v); + using SetType = decltype(setter); new (lua_newuserdata_x(L, sizeof(SetType))) SetType(setter); diff --git a/Tests/Source/IssueTests.cpp b/Tests/Source/IssueTests.cpp index 032fa252..77954475 100644 --- a/Tests/Source/IssueTests.cpp +++ b/Tests/Source/IssueTests.cpp @@ -4,6 +4,9 @@ #include "TestBase.h" +#include +#include + struct IssueTests : TestBase { }; @@ -266,3 +269,257 @@ TEST_F(IssueTests, IssueMainThread) luabridge::LuaRef test = luabridge::getGlobal(L, "test"); EXPECT_TRUE(test.call()); } + +// ============================================================================= +// Issue 242: try_call_parent_newindex must scan all parent setters before +// considering any __newindex fallback. +// ============================================================================= + +namespace { + +struct Issue242Base +{ + int x = 0; + std::map extra; + + int getX() const { return x; } + void setX(int v) { x = v; } +}; + +struct Issue242Middle : Issue242Base +{ + // Intermediate class that adds a __newindex fallback. + // fallbackWritten tracks whether the fallback was called. + bool fallbackWritten = false; + + luabridge::LuaRef onNewIndex(const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L) + { + fallbackWritten = true; + extra[key.tostring()] = value.unsafe_cast(); + return value; + } + + luabridge::LuaRef onIndex(const luabridge::LuaRef& key, lua_State* L) + { + auto it = extra.find(key.tostring()); + if (it != extra.end()) + { + auto res = luabridge::push(L, it->second); + (void)res; + return luabridge::LuaRef::fromStack(L); + } + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } +}; + +struct Issue242Derived : Issue242Middle +{ + // Most-derived class; inherits everything from Middle and Base. +}; + +} // namespace + +// Scenario 1 (from the issue): Base has a setter for 'x'; Derived has a __newindex +// fallback. Setting 'x' on a Derived instance must invoke Base's setter, NOT the +// fallback. +TEST_F(IssueTests, Issue242_BaseSetterTakesPriorityOverDerivedNewIndexFallback) +{ + Issue242Middle obj; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + // Writing 'x' must reach the registered setter, not the fallback. + ASSERT_TRUE(runLua("obj.x = 99")); + EXPECT_EQ(99, obj.x); + EXPECT_FALSE(obj.fallbackWritten); + + // Reading 'x' back must return the value set via the C++ property. + ASSERT_TRUE(runLua("result = obj.x")); + EXPECT_EQ(99, result()); +} + +// Scenario 2: deep hierarchy — A has a setter; B (intermediate) has a __newindex +// fallback; C (most-derived) has neither. Setting 'x' on a C instance must invoke +// A's setter, skipping B's fallback. +TEST_F(IssueTests, Issue242_DeepHierarchyBaseSetterTakesPriorityOverIntermediateNewIndexFallback) +{ + Issue242Derived obj; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass() + .deriveClass("Derived") + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + ASSERT_TRUE(runLua("obj.x = 42")); + EXPECT_EQ(42, obj.x); + EXPECT_FALSE(obj.fallbackWritten); +} + +// Scenario 3: the __newindex fallback is still invoked for keys that have no +// registered setter anywhere in the hierarchy. +TEST_F(IssueTests, Issue242_NewIndexFallbackCalledForUnregisteredProperties) +{ + Issue242Middle obj; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + // 'y' has no setter in the hierarchy → fallback must be called. + ASSERT_TRUE(runLua("obj.y = 7")); + EXPECT_TRUE(obj.fallbackWritten); + EXPECT_EQ(7, obj.extra["y"]); + // 'x' is still handled by the C++ setter. + ASSERT_TRUE(runLua("obj.x = 5")); + EXPECT_EQ(5, obj.x); +} + +// Scenario 4: a setter defined on the derived class itself still takes priority +// over a __newindex fallback on the same class. +TEST_F(IssueTests, Issue242_DerivedOwnSetterTakesPriorityOverItsOwnNewIndexFallback) +{ + Issue242Middle obj; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .endClass() + .deriveClass("Middle") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + ASSERT_TRUE(runLua("obj.x = 11")); + EXPECT_EQ(11, obj.x); + EXPECT_FALSE(obj.fallbackWritten); +} + +// ============================================================================= +// Issue 242 (index): try_call_parent_index must scan all parent getters before +// considering any __index fallback. +// ============================================================================= + +// Scenario 1: Base has a getter for 'x'; Middle has a __index fallback. +// Reading 'x' on a Middle instance must invoke Base's getter, NOT the fallback. +TEST_F(IssueTests, Issue242_BaseGetterTakesPriorityOverDerivedIndexFallback) +{ + Issue242Middle obj; + obj.x = 55; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + // Reading 'x' must reach the registered getter, not the __index fallback. + ASSERT_TRUE(runLua("result = obj.x")); + EXPECT_EQ(55, result()); +} + +// Scenario 2: deep hierarchy — A has a getter; B (intermediate) has a __index +// fallback; C (most-derived) has neither. Reading 'x' on a C instance must invoke +// A's getter, skipping B's fallback. +TEST_F(IssueTests, Issue242_DeepHierarchyBaseGetterTakesPriorityOverIntermediateIndexFallback) +{ + Issue242Derived obj; + obj.x = 77; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass() + .deriveClass("Derived") + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + ASSERT_TRUE(runLua("result = obj.x")); + EXPECT_EQ(77, result()); +} + +// Scenario 3: the __index fallback is still invoked for keys that have no +// registered getter anywhere in the hierarchy. +TEST_F(IssueTests, Issue242_IndexFallbackCalledForUnregisteredProperties) +{ + Issue242Middle obj; + obj.extra["y"] = 42; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .endClass() + .deriveClass("Middle") + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + // 'y' has no getter in the hierarchy → fallback must be called. + ASSERT_TRUE(runLua("result = obj.y")); + EXPECT_EQ(42, result()); + + // 'x' is still handled by the C++ getter. + obj.x = 99; + ASSERT_TRUE(runLua("result = obj.x")); + EXPECT_EQ(99, result()); +} + +// Scenario 4: a getter defined on the derived class itself still takes priority +// over a __index fallback on the same class. +TEST_F(IssueTests, Issue242_DerivedOwnGetterTakesPriorityOverItsOwnIndexFallback) +{ + Issue242Middle obj; + obj.x = 33; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .endClass() + .deriveClass("Middle") + .addProperty("x", &Issue242Base::getX, &Issue242Base::setX) + .addIndexMetaMethod(&Issue242Middle::onIndex) + .addNewIndexMetaMethod(&Issue242Middle::onNewIndex) + .endClass(); + + luabridge::setGlobal(L, &obj, "obj"); + + ASSERT_TRUE(runLua("result = obj.x")); + EXPECT_EQ(33, result()); +}