Skip to content

Fix phpstan/phpstan#11730: array_filter with assert-if-true callable marks all keys optional#5097

Open
phpstan-bot wants to merge 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wrykofq
Open

Fix phpstan/phpstan#11730: array_filter with assert-if-true callable marks all keys optional#5097
phpstan-bot wants to merge 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wrykofq

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When using array_filter with 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

  • Modified processKeyAndItemType() in src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php to check the falsey branch when the boolean result is not definitely true
  • If filterByFalseyValue makes the item or key variable type NeverType, the callback cannot return false for the given input type, so the key is not marked optional

Root cause

The processKeyAndItemType() method determined key optionality solely based on whether the callback's return type was definitely true ($booleanResult->isTrue()->yes()). For @phpstan-assert-if-true callbacks, the return type is bool (not conditionally true), 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 produces NeverType for the item or key variable, it means the false branch is impossible for this input type (e.g., passing Foo through a callback that asserts Foo on the true branch means the false branch would require !Foo, which is never). In that case, the key is correctly kept as non-optional.

Test

Added tests/PHPStan/Analyser/nsrt/bug-11730.php — an NSRT test that verifies array_filter with both a conditional return type callback and an @phpstan-assert-if-true callback produce array{Foo, Foo} (non-optional keys) when filtering an array of Foo elements.

Fixes phpstan/phpstan#11730

@staabm staabm changed the title Fix #11730: array_filter with assert-if-true callable marks all keys optional Fix phpstan/phpstan#11730: array_filter with assert-if-true callable marks all keys optional Mar 27, 2026
- 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
@staabm staabm force-pushed the create-pull-request/patch-wrykofq branch from b650945 to b2b8c97 Compare March 27, 2026 06:34
@staabm staabm requested a review from VincentLanglet March 27, 2026 06:36
Copy link
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense

$scope = $scope->filterByTruthyValue($expr);
$truthyScope = $scope->filterByTruthyValue($expr);

$optional = !$booleanResult->isTrue()->yes();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we checked already for $booleanResult->isFalse()->yes() in line 259 the mutation is not valid

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API Error: 500 {"type":"error","error":{"type":"api_error","message":"Internal server error"},"request_id":"req_011CZT9y6uqseeQQzqCjz7HV"}

phpstan-bot and others added 2 commits March 27, 2026 07:00
…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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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():

  1. Reverted the falsey-scope workaround in ArrayFilterFunctionReturnTypeHelper.php - restored the original simple logic.

  2. Added narrowReturnTypeByAssertions() in FuncCallHandler.php - when a function returns bool and has @phpstan-assert-if-true/@phpstan-assert-if-false annotations, it checks if the actual argument types make one branch impossible:

    • If the false branch is impossible (e.g., asserting NOT Foo on a Foo argument) → return type narrows to true
    • If the true branch is impossible → return type narrows to false

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>
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.

3 participants