From 54151224331aefd2ef79b43087affbad20f49b92 Mon Sep 17 00:00:00 2001 From: Thiago Marsola <36829328+thimarsola@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:05:55 -0300 Subject: [PATCH] test: cover non-final class branch of the higher-order property crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectType::hasProperty()` returns `maybe` on two independent branches: `allowsDynamicProperties()` (a `__get()` method) and `!isFinal()`. The regression tests added with the crash fix only covered the first one. The second branch is reachable whenever the object type does not come from a `new` expression — PHPStan marks those class reflections as final via `ClassReflection::asFinal()`, which is why `new NonFinalObject` answers `no` while a parameter typed `NonFinalObject` answers `maybe`. Reverting the try/catch in `resolvePropertyType()` makes these tests fail with: Property $name was not found in reflection of class Tests\Type\Fixtures\NonFinalObject. The fixture is abstract so it stays non-final under Pint's `final_class` rule, which also matches the real-world trigger (`Eloquent\Model` is an abstract non-final class). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TvRLUyRV47CJr9NfpxWg2M --- tests/Type/Fixtures/NonFinalObject.php | 10 ++++++++++ tests/Type/data/higher-order-expectations.php | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/Type/Fixtures/NonFinalObject.php diff --git a/tests/Type/Fixtures/NonFinalObject.php b/tests/Type/Fixtures/NonFinalObject.php new file mode 100644 index 0000000..c569508 --- /dev/null +++ b/tests/Type/Fixtures/NonFinalObject.php @@ -0,0 +1,10 @@ +, Tests\Type\Fixtures\MagicPropertyObject>', $result); } +function testNonFinalClassMissingPropertyDoesNotCrash(NonFinalObject $object): void +{ + $result = expect($object)->name; + assertType('Pest\Expectations\HigherOrderExpectation, mixed>', $result); +} + +function testNonFinalClassMissingPropertyChainDoesNotCrash(NonFinalObject $object): void +{ + $result = expect($object)->name->toBe('Nuno'); + assertType('Pest\Expectations\HigherOrderExpectation, Tests\Type\Fixtures\NonFinalObject>', $result); +} + +function testNonFinalClassRealPropertyStillResolves(NonFinalObject $object): void +{ + $result = expect($object)->title; + assertType('Pest\Expectations\HigherOrderExpectation, string>', $result); +} + function testUnionWithoutClassPropertyDoesNotCrash(): void { /** @var array|object|null $payload */