← All results

Optional-key shapes stay list-compatible

Group: Cross-analyzer regressions · Category: regression · File: regressions_optional_key_shape_is_list.php

A shape whose only keys are optional (for example array{a?: string}) includes [] as a valid value, and array_is_list([]) === true. So array_is_list($a) is undecidable (maybe true), not "always false", and narrowing to a list is reachable. Several analyzers historically treated the optional string key as required and collapsed the list check to "always false" / "impossible". Phan alone reports the check as "maybe", which is the runtime-correct verdict. References: - PHPStan phpstan#14938 (fix: phpstan-src#6025) - Psalm psalm#11905 (open) - Mago mago#2073 (fixed in 1.43) - Cross-tool survey phpstan#14939

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 Pass

No diagnostics reported.

phpstan 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 Pass reported Lv.4+
regressions_optional_key_shape_is_list.php:28: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType] [reported-from-level=4]
regressions_optional_key_shape_is_list.php:36: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType] [reported-from-level=4]
regressions_optional_key_shape_is_list.php:36: Call to function assert() with false will always evaluate to false. [identifier=function.impossibleType] [reported-from-level=4]
With strict-rules
regressions_optional_key_shape_is_list.php:28: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType]
regressions_optional_key_shape_is_list.php:36: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType]
regressions_optional_key_shape_is_list.php:36: Call to function assert() with false will always evaluate to false. [identifier=function.impossibleType]
psalm 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
Pass
regressions_optional_key_shape_is_list.php:28: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
regressions_optional_key_shape_is_list.php:36: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
mago 1.46.0mago 1.46.0 Fail

No diagnostics reported.

Expectation diff
Line 28: Expected 1 error(s)
Line 36: Expected 1 error(s)
mir 0.70.1mir 0.70.1 Pass

No diagnostics reported.

phpantom 0.9.0phpantom_lsp 0.9.0 Pass

No diagnostics reported.

intelephense 1.18.5intelephense 1.18.5 Pass

No diagnostics reported.

phpy 0.2.0phpy 0.2.0 Pass

No diagnostics reported.

qodana 262.8665.325Qodana 262.8665.325 Pass

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 Pass
regressions_optional_key_shape_is_list.php:26: param $a miss matched with phpdoc type <<array{a?: string}>> [funcParamTypeMissMatch]
regressions_optional_key_shape_is_list.php:34: param $a miss matched with phpdoc type <<array{a?: string}>> [funcParamTypeMissMatch]
steins 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) Pass

No diagnostics reported.

phpstan-strict 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 Pass
regressions_optional_key_shape_is_list.php:28: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType]
regressions_optional_key_shape_is_list.php:36: Call to function array_is_list() with array{a?: string} will always evaluate to false. [identifier=function.impossibleType]
regressions_optional_key_shape_is_list.php:36: Call to function assert() with false will always evaluate to false. [identifier=function.impossibleType]
psalm-next 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 Pass
regressions_optional_key_shape_is_list.php:28: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
regressions_optional_key_shape_is_list.php:36: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]

Source

<?php

declare(strict_types=1);

namespace Conformance\Tests\RegressionsOptionalKeyShapeIsList;

/**
 * Optional-key shapes stay list-compatible.
 *
 * A shape whose only keys are optional (for example `array{a?: string}`) includes `[]`
 * as a valid value, and `array_is_list([]) === true`. So `array_is_list($a)` is
 * undecidable (maybe true), not "always false", and narrowing to a list is reachable.
 *
 * Several analyzers historically treated the optional string key as required and
 * collapsed the list check to "always false" / "impossible". Phan alone reports the
 * check as "maybe", which is the runtime-correct verdict.
 *
 * References:
 * - PHPStan  https://github.com/phpstan/phpstan/issues/14938  (fix: phpstan-src#6025)
 * - Psalm    https://github.com/vimeo/psalm/issues/11905      (open)
 * - Mago     https://github.com/carthage-software/mago/issues/2073 (fixed in 1.43)
 * - Cross-tool survey https://github.com/phpstan/phpstan/discussions/14939
 */

/** @param array{a?: string} $a */
function optionalStringKeyIsList(array $a): void // E<noverify>: NoVerify does not parse array shape syntax and flags the annotation itself, not the list narrowing
{
    if (array_is_list($a)) { // E<phpstan>: array{a?: string} admits [], so the check is maybe-true not always-false (#14938) // E<phpstan-strict>: same always-false verdict under strict rules // E<psalm>: TypeDoesNotContainType, reported as never list<mixed> (#11905) // E<mago>: impossible-type-comparison in 1.19, fixed in 1.43 (#2073)
        echo "reached at runtime when \$a === []\n";
    }
}

/** @param array{a?: string} $a */
function optionalStringKeyAssertList(array $a): void // E<noverify>: NoVerify does not parse array shape syntax and flags the annotation itself, not the list narrowing
{
    assert(array_is_list($a)); // E<phpstan>: assert collapses to always-false on the optional-key shape (#14938) // E<phpstan-strict>: same always-false verdict under strict rules // E<psalm>: TypeDoesNotContainType, reported as never list<mixed> (#11905) // E<mago>: impossible-type-comparison in 1.19, fixed in 1.43 (#2073)
}

optionalStringKeyIsList([]);
optionalStringKeyAssertList([]);