From 7a233ba427683921fd8246e37ac50f5699b91894 Mon Sep 17 00:00:00 2001 From: "Nicolas G." Date: Mon, 7 Sep 2026 21:23:10 +0200 Subject: [PATCH] fix: a never-typed receiver is not an Expectation HigherOrderExpectationTypeExtension::getType() decides whether a receiver is an expectation with ObjectType(Expectation::class)->isSuperTypeOf($varType)->yes(). Since never is a subtype of every type, that check answers yes for a never-typed receiver, and the extension fabricates HigherOrderExpectation, mixed> for code that has nothing to do with Pest. This is an ExpressionTypeResolverExtension, so it runs on every analysed file, not only test files: compiled Blade templates carry never-typed receivers and every member access on them became a false HigherOrderExpectation. Bail out early when the receiver type is never. --- .../HigherOrderExpectationTypeExtension.php | 6 ++++ tests/Type/ExpectTypeTest.php | 6 ++++ tests/Type/data/never-receiver.php | 31 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tests/Type/data/never-receiver.php diff --git a/src/Type/Pest/HigherOrderExpectationTypeExtension.php b/src/Type/Pest/HigherOrderExpectationTypeExtension.php index 2f72220..22cafb5 100644 --- a/src/Type/Pest/HigherOrderExpectationTypeExtension.php +++ b/src/Type/Pest/HigherOrderExpectationTypeExtension.php @@ -20,6 +20,7 @@ use PHPStan\Type\ExpressionTypeResolverExtension; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -35,6 +36,11 @@ public function __construct( public function getType(Expr $expr, Scope $scope): ?Type { + if (($expr instanceof PropertyFetch || $expr instanceof MethodCall) + && (new NeverType)->isSuperTypeOf($scope->getType($expr->var))->yes()) { + return null; + } + if ($expr instanceof PropertyFetch) { return $this->resolvePropertyFetch($expr, $scope); } diff --git a/tests/Type/ExpectTypeTest.php b/tests/Type/ExpectTypeTest.php index 583c1a7..ccb4808 100644 --- a/tests/Type/ExpectTypeTest.php +++ b/tests/Type/ExpectTypeTest.php @@ -86,6 +86,12 @@ yield from TestCase::gatherAssertTypes(__DIR__.'/data/higher-order-exhaustive.php'); }); +test('never receiver is not an expectation', function (string $assertType, string $file, mixed ...$args): void { + $this->assertFileAsserts($assertType, $file, ...$args); +})->with(function (): Iterator { + yield from TestCase::gatherAssertTypes(__DIR__.'/data/never-receiver.php'); +}); + test('exhaustive test call types', function (string $assertType, string $file, mixed ...$args): void { $this->assertFileAsserts($assertType, $file, ...$args); })->with(function (): Iterator { diff --git a/tests/Type/data/never-receiver.php b/tests/Type/data/never-receiver.php new file mode 100644 index 0000000..35721db --- /dev/null +++ b/tests/Type/data/never-receiver.php @@ -0,0 +1,31 @@ +title); +} + +/** + * @param never $value + */ +function testMethodCallOnNever(mixed $value): void +{ + assertType('*ERROR*', $value->getTitle()); +} + +function testRealExpectationIsUnaffected(): void +{ + $post = new Post; + assertType('Pest\Expectations\HigherOrderExpectation, string>', expect($post)->title); +}