Fix phpstan/phpstan#11730: array_filter with assert-if-true callable marks all keys optional#5097
Conversation
- When array_filter callback has @phpstan-assert-if-true, check the falsey branch to determine if the callback can actually return false for the given input type - If filtering by falsey value makes the item/key type NeverType, the false branch is impossible, so the key should not be marked optional - New regression test in tests/PHPStan/Analyser/nsrt/bug-11730.php Closes phpstan/phpstan#11730
b650945 to
b2b8c97
Compare
| $scope = $scope->filterByTruthyValue($expr); | ||
| $truthyScope = $scope->filterByTruthyValue($expr); | ||
|
|
||
| $optional = !$booleanResult->isTrue()->yes(); |
There was a problem hiding this comment.
since we checked already for $booleanResult->isFalse()->yes() in line 259 the mutation is not valid
There was a problem hiding this comment.
API Error: 500 {"type":"error","error":{"type":"api_error","message":"Internal server error"},"request_id":"req_011CZT9y6uqseeQQzqCjz7HV"}
…types Adds test cases where input array has Foo|int elements filtered with both conditional return type and assert-if-true callbacks, verifying keys are correctly marked as optional when the false branch is possible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| $truthyScope = $scope->filterByTruthyValue($expr); | ||
|
|
||
| $optional = !$booleanResult->isTrue()->yes(); | ||
| if ($optional) { |
There was a problem hiding this comment.
Rather than having to "fix" manually the optional value, propose a solution in order to have directly $booleanResult->isTrue()->yes() returning true in the context of /** @phpstan-assert-if-true Foo $value */ ; because it works fine for a conditional type.
There was a problem hiding this comment.
Done. Here's a summary of the changes:
Problem: The reviewer (VincentLanglet) asked for a more fundamental fix rather than patching the optionality check in ArrayFilterFunctionReturnTypeHelper. The issue was that @phpstan-assert-if-true Foo $value didn't narrow the function's return type to true when the argument was already Foo, unlike conditional return types (@return ($value is Foo ? true : false)) which did.
Solution: Added assertion-based return type narrowing in FuncCallHandler::resolveType():
-
Reverted the falsey-scope workaround in
ArrayFilterFunctionReturnTypeHelper.php- restored the original simple logic. -
Added
narrowReturnTypeByAssertions()inFuncCallHandler.php- when a function returnsbooland has@phpstan-assert-if-true/@phpstan-assert-if-falseannotations, it checks if the actual argument types make one branch impossible:- If the false branch is impossible (e.g., asserting
NOT Fooon aFooargument) → return type narrows totrue - If the true branch is impossible → return type narrows to
false
- If the false branch is impossible (e.g., asserting
This fix is at the type-system level, so $scope->getType(checkFoo($fooValue)) now correctly returns true everywhere (not just in the array_filter context), making it consistent with conditional return types.
…-true/false Instead of patching the optionality check in ArrayFilterFunctionReturnTypeHelper, fix the root cause: when a function has @phpstan-assert-if-true annotations and the argument types already satisfy the asserted type, narrow the return type from bool to true (or false) at the FuncCallHandler level. This makes $scope->getType(checkFoo($fooVar)) return true when checkFoo has @phpstan-assert-if-true Foo $value and $fooVar is Foo, consistent with how conditional return types (@return ($value is Foo ? true : false)) already work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
When using
array_filterwith a callable annotated with@phpstan-assert-if-true, all keys in the resulting array were incorrectly marked as optional, even when the input elements already satisfied the asserted type. A callable with a conditional return type (@return ($value is Foo ? true : false)) correctly preserved keys as non-optional.Changes
processKeyAndItemType()insrc/Type/Php/ArrayFilterFunctionReturnTypeHelper.phpto check the falsey branch when the boolean result is not definitelytruefilterByFalseyValuemakes the item or key variable typeNeverType, the callback cannot return false for the given input type, so the key is not marked optionalRoot cause
The
processKeyAndItemType()method determined key optionality solely based on whether the callback's return type was definitelytrue($booleanResult->isTrue()->yes()). For@phpstan-assert-if-truecallbacks, the return type isbool(not conditionallytrue), so keys were always marked optional regardless of the input type.The fix adds a secondary check: when the return type isn't definitely
true, it evaluates the falsey branch. If filtering by the falsey value producesNeverTypefor the item or key variable, it means the false branch is impossible for this input type (e.g., passingFoothrough a callback that assertsFooon the true branch means the false branch would require!Foo, which isnever). In that case, the key is correctly kept as non-optional.Test
Added
tests/PHPStan/Analyser/nsrt/bug-11730.php— an NSRT test that verifiesarray_filterwith both a conditional return type callback and an@phpstan-assert-if-truecallback producearray{Foo, Foo}(non-optional keys) when filtering an array ofFooelements.Fixes phpstan/phpstan#11730