array_key_exists($key, $map) narrows $key to the keys $map is known to have, not only the offset access after the checkint|string key that is known to exist in array<string, int> is a
string. Passing it to a string sink is silent after the guard and
rejected without it. The same intersection applies to a literal map
['a' => 1, 'b' => 2]: a string key becomes 'a'|'b'.
Source: carthage-software/mago#2199
References:
- PHP array_key_exists()
- mago#2199| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 | Fail | No diagnostics reported. Expectation diffLine 43: Expected 1 error(s) |
| phpstan | 2.2.11PHPStan - PHP Static Analysis Tool 2.2.11 | Pass reported Lv.7+ | assertions_array_key_exists_key_narrowing.php:43: Parameter #1 $value of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, int|string given. [reported-from-level=7] assertions_array_key_exists_key_narrowing.php:64: Parameter #1 $key of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB expects 'a'|'b', string given. [reported-from-level=7] With strict-rulesassertions_array_key_exists_key_narrowing.php:43: Parameter #1 $value of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, int|string given. assertions_array_key_exists_key_narrowing.php:64: Parameter #1 $key of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB expects 'a'|'b', string given. |
| psalm | 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac next: 7.0.0-beta19 |
Fail | assertions_array_key_exists_key_narrowing.php:40: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, but int|string provided [InvalidScalarArgument] assertions_array_key_exists_key_narrowing.php:43: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, but int|string provided [InvalidScalarArgument] assertions_array_key_exists_key_narrowing.php:64: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB expects 'a'|'b', but parent type string provided [ArgumentTypeCoercion] Expectation diffLine 40: Expected silence (quiet probe), got ["Argument 1 of Conformance\\Tests\\AssertionsArrayKeyExistsKeyNarrowing\\takesString expects string, but int|string provided [InvalidScalarArgument]"] |
| mago | 1.47.4mago 1.47.4 | Pass | assertions_array_key_exists_key_narrowing.php:43: Possible argument type mismatch for argument #1 of `Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString`: expected `string`, but possibly received `int|string`. [possibly-invalid-argument]
assertions_array_key_exists_key_narrowing.php:64: Possible argument type mismatch for argument #1 of `Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB`: expected `string('a')|string('b')`, but possibly received `string`. [possibly-invalid-argument] |
| mir | 0.72.1mir 0.72.1 | Fail | assertions_array_key_exists_key_narrowing.php:40: PossiblyInvalidArgument: Argument $value of takesString() expects 'string', possibly different type 'int|string' provided [MIR0105] assertions_array_key_exists_key_narrowing.php:43: PossiblyInvalidArgument: Argument $value of takesString() expects 'string', possibly different type 'int|string' provided [MIR0105] Expectation diffLine 40: Expected silence (quiet probe), got ["PossiblyInvalidArgument: Argument $value of takesString() expects 'string', possibly different type 'int|string' provided [MIR0105]"] |
| phpantom | 0.10.0phpantom_lsp 0.10.0 | Fail | assertions_array_key_exists_key_narrowing.php:40: Argument 1 ($value) expects string, got int|string (int does not satisfy string) [type_mismatch_argument] assertions_array_key_exists_key_narrowing.php:43: Argument 1 ($value) expects string, got int|string (int does not satisfy string) [type_mismatch_argument] assertions_array_key_exists_key_narrowing.php:61: Argument 1 ($key) expects 'a'|'b', got string [type_mismatch_argument] assertions_array_key_exists_key_narrowing.php:64: Argument 1 ($key) expects 'a'|'b', got string [type_mismatch_argument] Expectation diffLine 40: Expected silence (quiet probe), got ["Argument 1 ($value) expects string, got int|string (int does not satisfy string) [type_mismatch_argument]"] |
| intelephense | 1.18.5intelephense 1.18.5 | Fail | assertions_array_key_exists_key_narrowing.php:40: Expected type 'string'. Found 'int|string'. [P1006] assertions_array_key_exists_key_narrowing.php:43: Expected type 'string'. Found 'int|string'. [P1006] Expectation diffLine 40: Expected silence (quiet probe), got ["Expected type 'string'. Found 'int|string'. [P1006]"] |
| phpactor | 2026.07.22.0Phpactor 2026.07.22.0 | Fail | No diagnostics reported. Expectation diffLine 43: Expected 1 error(s) |
| phpy | 0.2.0phpy 0.2.0 | Fail | No diagnostics reported. Expectation diffLine 43: Expected 1 error(s) |
| qodana | 262.9437.196Qodana 262.9437.196 | Fail | No diagnostics reported. Expectation diffLine 43: Expected 1 error(s) |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Fail | assertions_array_key_exists_key_narrowing.php:30: param $key miss matched with phpdoc type <<'a'|'b'>> [funcParamTypeMissMatch] assertions_array_key_exists_key_narrowing.php:37: param $values miss matched with phpdoc type <<array<string, int>>> [funcParamTypeMissMatch] assertions_array_key_exists_key_narrowing.php:40: potentially not safe call in function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString signature of param value [notSafeCall] assertions_array_key_exists_key_narrowing.php:43: potentially not safe call in function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString signature of param value [notSafeCall] assertions_array_key_exists_key_narrowing.php:49: param $values miss matched with phpdoc type <<array<string, int>>> [funcParamTypeMissMatch] Expectation diffLine 40: Expected silence (quiet probe), got ["potentially not safe call in function Conformance\\Tests\\AssertionsArrayKeyExistsKeyNarrowing\\takesString signature of param value [notSafeCall]"] |
| steins | 0.1.6steins 0.1.6 (2026-08-25 revision c13b4c7) | Fail | No diagnostics reported. Expectation diffLine 43: Expected 1 error(s) |
| phpstan-strict | 2.2.11PHPStan - PHP Static Analysis Tool 2.2.11 | Pass | assertions_array_key_exists_key_narrowing.php:43: Parameter #1 $value of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, int|string given. assertions_array_key_exists_key_narrowing.php:64: Parameter #1 $key of function Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB expects 'a'|'b', string given. |
| psalm-next | 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 | Fail | assertions_array_key_exists_key_narrowing.php:40: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, but int|string provided [InvalidScalarArgument] assertions_array_key_exists_key_narrowing.php:43: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesString expects string, but int|string provided [InvalidScalarArgument] assertions_array_key_exists_key_narrowing.php:64: Argument 1 of Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing\takesAB expects 'a'|'b', but parent type string provided [ArgumentTypeCoercion] Expectation diffLine 40: Expected silence (quiet probe), got ["Argument 1 of Conformance\\Tests\\AssertionsArrayKeyExistsKeyNarrowing\\takesString expects string, but int|string provided [InvalidScalarArgument]"] |
<?php
declare(strict_types=1);
namespace Conformance\Tests\AssertionsArrayKeyExistsKeyNarrowing;
/**
* `array_key_exists($key, $map)` narrows `$key` to the keys `$map` is known
* to have, not only the offset access after the check.
*
* A `int|string` key that is known to exist in `array<string, int>` is a
* `string`. Passing it to a `string` sink is silent after the guard and
* rejected without it. The same intersection applies to a literal map
* `['a' => 1, 'b' => 2]`: a `string` key becomes `'a'|'b'`.
*
* Source: carthage-software/mago#2199
*
* References:
* - PHP `array_key_exists()`
* - https://github.com/carthage-software/mago/issues/2199
*/
function takesString(string $value): void
{
}
/**
* @param 'a'|'b' $key
*/
function takesAB(string $key): void // E?[noise]: some tools do not parse 'a'|'b'
{
}
/**
* @param array<string, int> $values
*/
function genericStringKeys(int|string $key, array $values): void // E?[noise]: some tools do not parse array<string, int>
{
if (array_key_exists($key, $values)) {
takesString($key); // Q: $key is string after the guard
}
takesString($key); // E: int|string is not string
}
/**
* @param array<string, int> $values
*/
function alreadyString(string $key, array $values): void // E?[noise]: some tools do not parse array<string, int>
{
if (array_key_exists($key, $values)) {
takesString($key); // V: $key was already string
}
}
function knownStringKeys(string $key): void
{
$values = ['a' => 1, 'b' => 2];
if (array_key_exists($key, $values)) {
takesAB($key); // Q?: $key is 'a'|'b' after the guard
}
takesAB($key); // E?: string is not 'a'|'b'
}
takesAB('a'); // V
takesAB('b'); // V