From 13ceb9005bc398da9de0b863113d7d024a3371b5 Mon Sep 17 00:00:00 2001 From: KorsarOfficial Date: Thu, 19 Mar 2026 00:57:42 +0400 Subject: [PATCH] fix: walk parent_class chain when looking up __clone for op_clone (#57) When cloning an object whose class does not define __clone() but an ancestor does, KPHP was not calling the inherited magic method because members.has_instance_method() only checks the class's own methods and does not walk the inheritance chain. Changed on_clone() in ConvertInvokeToFuncCallPass to use find_instance_method_by_local_name() which walks the full parent_class chain, matching PHP semantics. Also adds two regression tests: - tests/phpt/clone_keyword/105_inherited_clone_method.php - tests/phpt/clone_keyword/106_deep_inheritance_clone.php --- .../pipes/convert-invoke-to-func-call.cpp | 9 ++++-- .../105_inherited_clone_method.php | 29 +++++++++++++++++++ .../106_deep_inheritance_clone.php | 27 +++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/phpt/clone_keyword/105_inherited_clone_method.php create mode 100644 tests/phpt/clone_keyword/106_deep_inheritance_clone.php diff --git a/compiler/pipes/convert-invoke-to-func-call.cpp b/compiler/pipes/convert-invoke-to-func-call.cpp index 3b93064203..49d54c4f53 100644 --- a/compiler/pipes/convert-invoke-to-func-call.cpp +++ b/compiler/pipes/convert-invoke-to-func-call.cpp @@ -119,8 +119,11 @@ VertexPtr ConvertInvokeToFuncCallPass::on_clone(VertexAdaptor v_clone) return call_virt_clone; } - // clone $obj, when a class has the __clone() magic method, is replaced with { tmp = clone $obj; tmp->__clone(); $tmp } - if (klass->members.has_instance_method(ClassData::NAME_OF_CLONE)) { + // clone $obj, when a class (or any of its ancestors) has the __clone() magic method, + // is replaced with { tmp = clone $obj; tmp->__clone(); $tmp } + // find_instance_method_by_local_name walks the full parent_class chain, so an inherited + // __clone() from a base class is correctly invoked (fixes #57) + if (const auto *clone_method = klass->find_instance_method_by_local_name(ClassData::NAME_OF_CLONE)) { auto tmp_var = VertexAdaptor::create().set_location(v_clone); tmp_var->str_val = gen_unique_name("tmp_for_clone"); tmp_var->extra_type = op_ex_var_superlocal; @@ -130,7 +133,7 @@ VertexPtr ConvertInvokeToFuncCallPass::on_clone(VertexAdaptor v_clone) auto call_magic_clone = VertexAdaptor::create(tmp_var).set_location(v_clone); call_magic_clone->str_val = ClassData::NAME_OF_CLONE; call_magic_clone->extra_type = op_ex_func_call_arrow; - call_magic_clone->func_id = klass->members.get_instance_method(ClassData::NAME_OF_CLONE)->function; + call_magic_clone->func_id = clone_method->function; return VertexAdaptor::create(set_clone_to_tmp, call_magic_clone, tmp_var).set_location(v_clone); } diff --git a/tests/phpt/clone_keyword/105_inherited_clone_method.php b/tests/phpt/clone_keyword/105_inherited_clone_method.php new file mode 100644 index 0000000000..a5b4e81db5 --- /dev/null +++ b/tests/phpt/clone_keyword/105_inherited_clone_method.php @@ -0,0 +1,29 @@ +@ok +value = $v; + } + + public function __clone() { + $this->value *= 2; + } +} + +class Child extends Base { + // No __clone() here — must inherit Base::__clone() +} + +$obj = new Child(5); +$copy = clone $obj; + +// Base::__clone() doubles value: original stays 5, copy becomes 10 +var_dump($obj->value); // int(5) +var_dump($copy->value); // int(10) diff --git a/tests/phpt/clone_keyword/106_deep_inheritance_clone.php b/tests/phpt/clone_keyword/106_deep_inheritance_clone.php new file mode 100644 index 0000000000..7632d16bcb --- /dev/null +++ b/tests/phpt/clone_keyword/106_deep_inheritance_clone.php @@ -0,0 +1,27 @@ +@ok +log .= '+cloned'; + } +} + +class Parent_ extends GrandParent { + // no __clone +} + +class Child extends Parent_ { + // no __clone either — must still reach GrandParent::__clone +} + +$obj = new Child(); +$obj->log = 'original'; +$copy = clone $obj; + +var_dump($obj->log); // string(8) "original" +var_dump($copy->log); // string(15) "original+cloned"