From 72423b821fd08baccf21aa803908c86d22c336f8 Mon Sep 17 00:00:00 2001 From: Ignas Rudaitis Date: Mon, 27 Jul 2026 11:48:23 +0300 Subject: [PATCH 1/7] Add test case --- tests/return-from-constructor.phpt | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/return-from-constructor.phpt diff --git a/tests/return-from-constructor.phpt b/tests/return-from-constructor.phpt new file mode 100644 index 0000000..fe8e291 --- /dev/null +++ b/tests/return-from-constructor.phpt @@ -0,0 +1,32 @@ +--TEST-- +https://github.com/antecedent/patchwork/issues/213 + +--FILE-- + +===DONE=== + +--EXPECT-- +Patchwork\Exceptions\NonNullToVoid +===DONE=== From f9be20cd5bc0f91f8064039cbff119d03451f874 Mon Sep 17 00:00:00 2001 From: Ignas Rudaitis Date: Mon, 27 Jul 2026 11:49:00 +0300 Subject: [PATCH 2/7] Treat constructors as void-typed --- src/CodeManipulation/Actions/Generic.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CodeManipulation/Actions/Generic.php b/src/CodeManipulation/Actions/Generic.php index fb0ea54..f95a598 100644 --- a/src/CodeManipulation/Actions/Generic.php +++ b/src/CodeManipulation/Actions/Generic.php @@ -78,6 +78,10 @@ function prependCodeToFunctions($code, $typedVariants = array(), $fillArgRefs = function getDeclaredReturnType(Source $s, $function) { $parenthesis = $s->next(LEFT_ROUND, $function); + $name = $s->next(T_STRING, $function); + if ($name < $parenthesis && $s->read($name) === '__construct') { + return 'void'; + } $next = $s->skip(Source::junk(), $s->match($parenthesis)); if ($s->is(T_USE, $next)) { $next = $s->skip(Source::junk(), $s->match($s->next(LEFT_ROUND, $next))); From b2bdccbb3b3b6ecbd1f284bfe6d7371c42cc3960 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 16 Aug 2026 00:00:21 +0200 Subject: [PATCH 3/7] Add test case for destruct --- tests/return-from-constructor.phpt | 2 +- tests/return-from-destructor.phpt | 32 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/return-from-destructor.phpt diff --git a/tests/return-from-constructor.phpt b/tests/return-from-constructor.phpt index fe8e291..e87e720 100644 --- a/tests/return-from-constructor.phpt +++ b/tests/return-from-constructor.phpt @@ -1,5 +1,5 @@ --TEST-- -https://github.com/antecedent/patchwork/issues/213 +Return from constructor / antecedent/patchwork#213 --FILE-- +===DONE=== + +--EXPECT-- +Patchwork\Exceptions\NonNullToVoid +===DONE=== From 6aee010b57ed3b27ea0e9583d277176ee7863d85 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 16 Aug 2026 00:07:36 +0200 Subject: [PATCH 4/7] Maybe fix ? --- src/CodeManipulation/Actions/Generic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeManipulation/Actions/Generic.php b/src/CodeManipulation/Actions/Generic.php index f95a598..2876f09 100644 --- a/src/CodeManipulation/Actions/Generic.php +++ b/src/CodeManipulation/Actions/Generic.php @@ -79,7 +79,7 @@ function getDeclaredReturnType(Source $s, $function) { $parenthesis = $s->next(LEFT_ROUND, $function); $name = $s->next(T_STRING, $function); - if ($name < $parenthesis && $s->read($name) === '__construct') { + if ($name < $parenthesis && ($s->read($name) === '__construct' || $s->read($name) === '__destruct')) { return 'void'; } $next = $s->skip(Source::junk(), $s->match($parenthesis)); From a1c3f95108f74fb940cea722ef956501b877d995 Mon Sep 17 00:00:00 2001 From: Ignas Rudaitis Date: Sun, 16 Aug 2026 10:32:03 +0300 Subject: [PATCH 5/7] Add a __destruct method to the NamedObject class used in tests --- tests/includes/NamedObject.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/includes/NamedObject.php b/tests/includes/NamedObject.php index ed075b2..d6f92a4 100644 --- a/tests/includes/NamedObject.php +++ b/tests/includes/NamedObject.php @@ -4,13 +4,17 @@ class NamedObject { private $name; - function __construct($name) + public function __construct($name) { $this->name = $name; } - function getName() + public function getName() { return $this->name; } + + public function __destruct() + { + } } From 6eb07e18977feb152de7cfb40e49b0e07c5d2b36 Mon Sep 17 00:00:00 2001 From: Ignas Rudaitis Date: Tue, 18 Aug 2026 10:12:21 +0300 Subject: [PATCH 6/7] Extend the test cases to make sure that anonymous classes are unaffected --- tests/includes/NamedObject.php | 13 +++++++++++++ tests/return-from-constructor.phpt | 25 +++++++++++++++++++++++-- tests/return-from-destructor.phpt | 25 +++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/tests/includes/NamedObject.php b/tests/includes/NamedObject.php index d6f92a4..2b7dec1 100644 --- a/tests/includes/NamedObject.php +++ b/tests/includes/NamedObject.php @@ -17,4 +17,17 @@ public function getName() public function __destruct() { } + + public static function createAnonymousSubclassInstance() + { + return new class extends NamedObject { + public function __construct() + { + } + + public function __destruct() + { + } + }; + } } diff --git a/tests/return-from-constructor.phpt b/tests/return-from-constructor.phpt index e87e720..c75c7dc 100644 --- a/tests/return-from-constructor.phpt +++ b/tests/return-from-constructor.phpt @@ -14,8 +14,14 @@ $_SERVER['PHP_SELF'] = __FILE__; require __DIR__ . "/../Patchwork.php"; require __DIR__ . "/includes/NamedObject.php"; -redefine('NamedObject::__construct', function ($name) { - return $name; +$classes = []; + +echo "Named class:\n"; + +redefine('*::__construct', function () use (&$classes) { + $class = get_class($this); + $classes[] = $class; + return $class; }); try { @@ -24,9 +30,24 @@ try { echo get_class($e), "\n"; } +echo "Anonymous subclass:\n"; + +try { + NamedObject::createAnonymousSubclassInstance(); +} catch (Exception $e) { + echo get_class($e), "\n"; +} + +echo "(has its own constructor that is not redefinable using Patchwork)\n"; + +assert($classes === ['NamedObject']); + ?> ===DONE=== --EXPECT-- +Named class: Patchwork\Exceptions\NonNullToVoid +Anonymous subclass: +(has its own constructor that is not redefinable using Patchwork) ===DONE=== diff --git a/tests/return-from-destructor.phpt b/tests/return-from-destructor.phpt index 97b907d..32474a7 100644 --- a/tests/return-from-destructor.phpt +++ b/tests/return-from-destructor.phpt @@ -14,19 +14,40 @@ $_SERVER['PHP_SELF'] = __FILE__; require __DIR__ . "/../Patchwork.php"; require __DIR__ . "/includes/NamedObject.php"; -redefine('NamedObject::__destruct', function () { - return __CLASS__; +$classes = []; + +redefine('*::__destruct', function () use (&$classes) { + $class = get_class($this); + $classes[] = $class; + return $class; }); +echo "Named class:\n"; + try { new NamedObject('foo'); } catch (Exception $e) { echo get_class($e), "\n"; } +echo "Anonymous subclass:\n"; + +try { + NamedObject::createAnonymousSubclassInstance(); +} catch (Exception $e) { + echo get_class($e), "\n"; +} + +echo "(has its own destructor that is not redefinable using Patchwork)\n"; + +assert($classes === ['NamedObject']); + ?> ===DONE=== --EXPECT-- +Named class: Patchwork\Exceptions\NonNullToVoid +Anonymous subclass: +(has its own destructor that is not redefinable using Patchwork) ===DONE=== From 1910918a96c78cdda4930c3c7b7be65956f6a807 Mon Sep 17 00:00:00 2001 From: Ignas Rudaitis Date: Tue, 18 Aug 2026 10:50:16 +0300 Subject: [PATCH 7/7] Make sure the anonymous class reports to get_declared_classes() --- tests/includes/NamedObject.php | 2 ++ tests/return-from-constructor.phpt | 9 ++++----- tests/return-from-destructor.phpt | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/includes/NamedObject.php b/tests/includes/NamedObject.php index 2b7dec1..9254b21 100644 --- a/tests/includes/NamedObject.php +++ b/tests/includes/NamedObject.php @@ -31,3 +31,5 @@ public function __destruct() }; } } + +NamedObject::createAnonymousSubclassInstance(); diff --git a/tests/return-from-constructor.phpt b/tests/return-from-constructor.phpt index c75c7dc..80e8181 100644 --- a/tests/return-from-constructor.phpt +++ b/tests/return-from-constructor.phpt @@ -38,10 +38,9 @@ try { echo get_class($e), "\n"; } -echo "(has its own constructor that is not redefinable using Patchwork)\n"; - -assert($classes === ['NamedObject']); - +assert(count($classes) === 2); +assert($classes[0] === 'NamedObject'); +assert($classes[1] !== 'NamedObject'); ?> ===DONE=== @@ -49,5 +48,5 @@ assert($classes === ['NamedObject']); Named class: Patchwork\Exceptions\NonNullToVoid Anonymous subclass: -(has its own constructor that is not redefinable using Patchwork) +Patchwork\Exceptions\NonNullToVoid ===DONE=== diff --git a/tests/return-from-destructor.phpt b/tests/return-from-destructor.phpt index 32474a7..009eeb0 100644 --- a/tests/return-from-destructor.phpt +++ b/tests/return-from-destructor.phpt @@ -38,9 +38,9 @@ try { echo get_class($e), "\n"; } -echo "(has its own destructor that is not redefinable using Patchwork)\n"; - -assert($classes === ['NamedObject']); +assert(count($classes) === 2); +assert($classes[0] === 'NamedObject'); +assert($classes[1] !== 'NamedObject'); ?> ===DONE=== @@ -49,5 +49,5 @@ assert($classes === ['NamedObject']); Named class: Patchwork\Exceptions\NonNullToVoid Anonymous subclass: -(has its own destructor that is not redefinable using Patchwork) +Patchwork\Exceptions\NonNullToVoid ===DONE===