($key is Model ? … : ($key is Arrayable ? …)) must narrow $key in the inner condition to the else of the outer onefind(?User): the outer is Model takes the User arm (User|null). The
inner is Arrayable only sees what remains (null), which is not
Arrayable, so static (the collection) is not in the result. PHPStan
2.2.15 resolved the inner condition against the whole ?User, User
matched Arrayable, and the return became Collection<User>|User|null.
Source: phpstan/phpstan#15296, phpstan/phpstan-src#6586 (2.2.16).
References:
- phpstan#15296
- phpstan-src#6586| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Enforced | regressions_nested_param_conditional.php:75: Argument 1 ($user) is $collection->find([1]) of type \Conformance\Tests\RegressionsNestedParamConditional\Collection|\Conformance\Tests\RegressionsNestedParamConditional\Collection<\Conformance\Tests\RegressionsNestedParamConditional\User> but \Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull() takes ?\Conformance\Tests\RegressionsNestedParamConditional\User defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/regressions_nested_param_conditional.php:50 [PhanTypeMismatchArgument]
|
| phpstan | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Unrecognized | regressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns Conformance\Tests\RegressionsNestedParamConditional\Collection<TModel of Conformance\Tests\RegressionsNestedParamConditional\Model> so it can be removed from the return type. [reported-from-level=4] regressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns TModel of Conformance\Tests\RegressionsNestedParamConditional\Model so it can be removed from the return type. [reported-from-level=4] regressions_nested_param_conditional.php:75: Parameter #1 $user of function Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> given. [reported-from-level=5] With strict-rulesregressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns Conformance\Tests\RegressionsNestedParamConditional\Collection<TModel of Conformance\Tests\RegressionsNestedParamConditional\Model> so it can be removed from the return type. regressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns TModel of Conformance\Tests\RegressionsNestedParamConditional\Model so it can be removed from the return type. regressions_nested_param_conditional.php:75: Parameter #1 $user of function Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> given.
|
| psalm | 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c next: 7.0.0-rc2 |
Enforced (pzoom≠) | regressions_nested_param_conditional.php:75: Argument 1 of Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, but Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> provided [InvalidArgument] pzoom (Psalm port)regressions_nested_param_conditional.php:74: Argument 1 of takesCollection expects Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>, but parent type Conformance\Tests\RegressionsNestedParamConditional\Collection provided [ArgumentTypeCoercion] regressions_nested_param_conditional.php:75: Argument 1 of takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, but Conformance\Tests\RegressionsNestedParamConditional\Collection provided [InvalidArgument]
|
| mago | 1.57.0mago 1.57.0 | Partly enforced (2/3) | regressions_nested_param_conditional.php:66: Possible argument type mismatch for argument #1 of `Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull`: expected `Conformance\Tests\RegressionsNestedParamConditional\User|null`, but possibly received `Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>|Conformance\Tests\RegressionsNestedParamConditional\User|null`. [possibly-invalid-argument] regressions_nested_param_conditional.php:75: Invalid argument type for argument #1 of `Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull`: expected `Conformance\Tests\RegressionsNestedParamConditional\User|null`, but found `Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>`. [invalid-argument]
Expectation diffLine 66: Expected silence (quiet probe), got ["Possible argument type mismatch for argument #1 of `Conformance\\Tests\\RegressionsNestedParamConditional\\takesUserOrNull`: expected `Conformance\\Tests\\RegressionsNestedParamConditional\\User|null`, but possibly received `Conformance\\Tests\\RegressionsNestedParamConditional\\Collection<Conformance\\Tests\\RegressionsNestedParamConditional\\User>|Conformance\\Tests\\RegressionsNestedParamConditional\\User|null`. [possibly-invalid-argument]"] |
| mir | 0.87.0mir 0.87.0 | Partly enforced (1/3) | regressions_nested_param_conditional.php:66: PossiblyInvalidArgument: Argument $user of takesUserOrNull() expects 'Conformance\Tests\RegressionsNestedParamConditional\User|null', possibly different type 'Conformance\Tests\RegressionsNestedParamConditional\User|null|static()' provided [MIR0105] regressions_nested_param_conditional.php:74: PossiblyNullArgument: Argument $collection of takesCollection() might be null [MIR0104] regressions_nested_param_conditional.php:74: InvalidArgument: Argument $collection of takesCollection() expects 'Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>', got 'Conformance\Tests\RegressionsNestedParamConditional\User|null|static()' [MIR0201] regressions_nested_param_conditional.php:75: PossiblyInvalidArgument: Argument $user of takesUserOrNull() expects 'Conformance\Tests\RegressionsNestedParamConditional\User|null', possibly different type 'Conformance\Tests\RegressionsNestedParamConditional\User|null|static()' provided [MIR0105]
Expectation diffLine 66: Expected silence (quiet probe), got ["PossiblyInvalidArgument: Argument $user of takesUserOrNull() expects 'Conformance\\Tests\\RegressionsNestedParamConditional\\User|null', possibly different type 'Conformance\\Tests\\RegressionsNestedParamConditional\\User|null|static()' provided [MIR0105]"] |
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Partly enforced (2/3) | regressions_nested_param_conditional.php:66: Argument 1 ($user) expects ?Conformance\Tests\RegressionsNestedParamConditional\User, got Conformance\Tests\RegressionsNestedParamConditional\User|Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>|null (Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> does not satisfy ?Conformance\Tests\RegressionsNestedParamConditional\User) [type_mismatch_argument] regressions_nested_param_conditional.php:75: Argument 1 ($user) expects ?Conformance\Tests\RegressionsNestedParamConditional\User, got Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> [type_mismatch_argument]
Expectation diffLine 66: Expected silence (quiet probe), got ["Argument 1 ($user) expects ?Conformance\\Tests\\RegressionsNestedParamConditional\\User, got Conformance\\Tests\\RegressionsNestedParamConditional\\User|Conformance\\Tests\\RegressionsNestedParamConditional\\Collection<Conformance\\Tests\\RegressionsNestedParamConditional\\User>|null (Conformance\\Tests\\RegressionsNestedParamConditional\\Collection<Conformance\\Tests\\RegressionsNestedParamConditional\\User> does not satisfy ?Conformance\\Tests\\RegressionsNestedParamConditional\\User) [type_mismatch_argument]"] |
| intelephense | 1.18.5intelephense 1.18.5 | Partly enforced (2/3) | regressions_nested_param_conditional.php:66: Expected type 'null|Conformance\Tests\RegressionsNestedParamConditional\User'. Found 'Conformance\Tests\RegressionsNestedParamConditional\User|null|Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>'. [P1006] regressions_nested_param_conditional.php:75: Expected type 'null|Conformance\Tests\RegressionsNestedParamConditional\User'. Found 'Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User>'. [P1006]
Expectation diffLine 66: Expected silence (quiet probe), got ["Expected type 'null|Conformance\\Tests\\RegressionsNestedParamConditional\\User'. Found 'Conformance\\Tests\\RegressionsNestedParamConditional\\User|null|Conformance\\Tests\\RegressionsNestedParamConditional\\Collection<Conformance\\Tests\\RegressionsNestedParamConditional\\User>'. [P1006]"] |
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Partly enforced (2/3) | No diagnostics reported.
|
| phpy | 1.0.19409phpy 1.0.19409 | Enforced | regressions_nested_param_conditional.php:75: Argument '1' passed to Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull() is expected to be of type Conformance\Tests\RegressionsNestedParamConditional\User|null, Conformance\Tests\RegressionsNestedParamConditional\Collection given
|
| qodana | 262.10968.76PhpStorm 262.10968.76 | Partly enforced (2/3) | No diagnostics reported.
|
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Unrecognized | regressions_nested_param_conditional.php:42: Class or interface named \Conformance\Tests\RegressionsNestedParamConditional\$key does not exist [undefinedClass] regressions_nested_param_conditional.php:57: param $collection miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch] regressions_nested_param_conditional.php:64: param $collection miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch] regressions_nested_param_conditional.php:72: param $collection miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch]
|
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Partly enforced (2/3) | No diagnostics reported.
|
| phpstan-strict | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Unrecognized | regressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns Conformance\Tests\RegressionsNestedParamConditional\Collection<TModel of Conformance\Tests\RegressionsNestedParamConditional\Model> so it can be removed from the return type. regressions_nested_param_conditional.php:44: Method Conformance\Tests\RegressionsNestedParamConditional\Collection::find() never returns TModel of Conformance\Tests\RegressionsNestedParamConditional\Model so it can be removed from the return type. regressions_nested_param_conditional.php:75: Parameter #1 $user of function Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> given.
|
| psalm-next | 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 | Enforced | regressions_nested_param_conditional.php:75: Argument 1 of Conformance\Tests\RegressionsNestedParamConditional\takesUserOrNull expects Conformance\Tests\RegressionsNestedParamConditional\User|null, but Conformance\Tests\RegressionsNestedParamConditional\Collection<Conformance\Tests\RegressionsNestedParamConditional\User> provided [InvalidArgument]
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\RegressionsNestedParamConditional;
/**
* Nested `($key is Model ? … : ($key is Arrayable ? …))` must narrow `$key`
* in the inner condition to the else of the outer one.
*
* `find(?User)`: the outer `is Model` takes the `User` arm (`User|null`). The
* inner `is Arrayable` only sees what remains (`null`), which is not
* `Arrayable`, so `static` (the collection) is not in the result. PHPStan
* 2.2.15 resolved the inner condition against the whole `?User`, `User`
* matched `Arrayable`, and the return became `Collection<User>|User|null`.
*
* Source: phpstan/phpstan#15296, phpstan/phpstan-src#6586 (2.2.16).
*
* References:
* - https://github.com/phpstan/phpstan/issues/15296
* - https://github.com/phpstan/phpstan-src/pull/6586
*/
interface Arrayable
{
}
class Model implements Arrayable
{
}
final class User extends Model
{
}
/**
* @template TModel of Model
*/
final class Collection
{
/**
* @return ($key is Model ? TModel|null : ($key is (Arrayable|array<mixed>) ? static : TModel|null))
*/
public function find(mixed $key): mixed // T: ($key is Model ? TModel|null : ($key is (Arrayable|array<mixed>) ? static : TModel|null)) // E?[noise]: the body is not under test
{
return null;
}
}
function takesUserOrNull(?User $user): void
{
}
/**
* @param Collection<User> $collection
*/
function takesCollection(Collection $collection): void // E?[noise]: some tools flag the phpdoc/native generic mismatch
{
}
/**
* @param Collection<User> $collection
*/
function fromNullableUser(Collection $collection, ?User $user): void // E?[noise]: some tools flag the phpdoc/native generic mismatch
{
takesUserOrNull($collection->find($user)); // Q: User|null — a Collection arm here is the 2.2.15 leak
}
/**
* @param Collection<User> $collection
*/
function fromArrayKey(Collection $collection): void // E?[noise]: some tools flag the phpdoc/native generic mismatch
{
takesCollection($collection->find([1])); // Q?: array key takes the inner Arrayable|array branch
takesUserOrNull($collection->find([1])); // E?: that branch is Collection, not ?User
}