Skip to content

fix: a never-typed receiver is not an Expectation - #18

Merged
MrPunyapal merged 1 commit into
pestphp:5.xfrom
LeFrenchie:fix/never-receiver-is-not-an-expectation
Sep 7, 2026
Merged

MrPunyapal merged 1 commit into
pestphp:5.xfrom
LeFrenchie:fix/never-receiver-is-not-an-expectation

Conversation

@LeFrenchie

Copy link
Copy Markdown
Contributor

HigherOrderExpectationTypeExtension::getType() decides whether a PropertyFetch / MethodCall 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 then fabricates HigherOrderExpectation<Expectation<never>, mixed> for code that has nothing to do with Pest.

This is an ExpressionTypeResolverExtension, so it runs on every file PHPStan analyses, not only test files. In a Laravel project analysed together with bladestan, Blade templates are compiled to PHP where unknown view variables are typed never; every $grid['title']-style access in those templates became a HigherOrderExpectation, producing 237 false positives in application code (160 on a single view), e.g.:

Argument of an invalid type Pest\Expectations\HigherOrderExpectation<Pest\Expectation<*NEVER*>, mixed> supplied for foreach, only iterables are supported.

Confirmed by instrumenting the resolver: 5516 non-null resolutions, all ->offsetGet on a *NEVER* receiver inside the compiled template.

Fix: bail out early when the receiver type is never — a never receiver is never an expectation. With the guard, the false positives drop to zero and the plugin's own pest.* findings in real test files are unchanged.

Regression test

Added tests/Type/data/never-receiver.php, wired into tests/Type/ExpectTypeTest.php alongside the existing higher-order cases. It asserts that a property fetch and a method call on a never-typed receiver resolve to *ERROR* (PHP's own member-access-on-never), not to a fabricated expectation, plus one case asserting a real expect() chain is unaffected.

Without the guard the two new cases fail with Expected type *ERROR*, got type Pest\Expectations\HigherOrderExpectation<Pest\Expectation<*NEVER*>, mixed>; with it they pass.

Checks

All green on this branch:

  • pest → 514 passed, 630 assertions
  • phpstan analyse → 0 errors
  • rector --dry-run → 0 changed files
  • pint --test → passed

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<Expectation<never>,
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.
@MrPunyapal
MrPunyapal merged commit e0b9fce into pestphp:5.x Sep 7, 2026
@MrPunyapal

Copy link
Copy Markdown
Member

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants