fix: a never-typed receiver is not an Expectation - #18
Merged
MrPunyapal merged 1 commit intoSep 7, 2026
Merged
Conversation
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.
Member
|
Thanks |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HigherOrderExpectationTypeExtension::getType()decides whether aPropertyFetch/MethodCallreceiver is an expectation withObjectType(Expectation::class)->isSuperTypeOf($varType)->yes(). Sinceneveris a subtype of every type, that check answers yes for anever-typed receiver, and the extension then fabricatesHigherOrderExpectation<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 typednever; every$grid['title']-style access in those templates became aHigherOrderExpectation, producing 237 false positives in application code (160 on a single view), e.g.:Confirmed by instrumenting the resolver: 5516 non-null resolutions, all
->offsetGeton a*NEVER*receiver inside the compiled template.Fix: bail out early when the receiver type is
never— aneverreceiver is never an expectation. With the guard, the false positives drop to zero and the plugin's ownpest.*findings in real test files are unchanged.Regression test
Added
tests/Type/data/never-receiver.php, wired intotests/Type/ExpectTypeTest.phpalongside the existing higher-order cases. It asserts that a property fetch and a method call on anever-typed receiver resolve to*ERROR*(PHP's own member-access-on-never), not to a fabricated expectation, plus one case asserting a realexpect()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 assertionsphpstan analyse→ 0 errorsrector --dry-run→ 0 changed filespint --test→ passed