← 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". PHPStan fixed this in 2.2.9 (phpstan-src#6025). Mago is silent since 1.43. Phan reports the check as "maybe", which is the runtime-correct verdict. References: - PHPStan phpstan#14938 (fix: phpstan-src#6025, 2.2.9) - Psalm psalm#11905 (open) - Mago mago#2073 (fixed in 1.43) - Cross-tool survey phpstan#14939

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.11Phan 6.0.11 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 Pass

No diagnostics reported.

phpstan 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 Pass

No diagnostics reported.

psalm 6.20.9Psalm 6.20.9@f67170ac3c3b931f5088681e21d6c8babc601c08
next: 7.0.4
Pass
regressions_optional_key_shape_is_list.php:29: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
regressions_optional_key_shape_is_list.php:37: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
mago 1.59.0mago 1.59.0 Pass

No diagnostics reported.

mir 0.91.0mir 0.91.0 Pass
regressions_optional_key_shape_is_list.php:29: RedundantCondition: Condition is always false, so the then branch is never reached [MIR0400]
phpantom 0.11.1phpantom_lsp 0.11.1 Pass

No diagnostics reported.

intelephense 1.18.5intelephense 1.18.5 Pass

No diagnostics reported.

phpactor 2026.10.07.0Phpactor 2026.10.07.0 Pass

No diagnostics reported.

phpy 1.0.19490phpy 1.0.19490 Pass

No diagnostics reported.

qodana 262.11879.104PhpStorm 262.11879.104 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:27: param $a miss matched with phpdoc type <<array{a?: string}>> [funcParamTypeMissMatch]
regressions_optional_key_shape_is_list.php:35: param $a miss matched with phpdoc type <<array{a?: string}>> [funcParamTypeMissMatch]
steins 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) Pass

No diagnostics reported.

phpstan-strict 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 Pass

No diagnostics reported.

psalm-next 7.0.4Psalm 7.0.4@fa5b1f6e665cb743838ffd865b8fc96207f9409f Pass
regressions_optional_key_shape_is_list.php:29: Type array{a?: string} for $a is never list<mixed> [TypeDoesNotContainType]
regressions_optional_key_shape_is_list.php:37: 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". PHPStan fixed this
 * in 2.2.9 (phpstan-src#6025). Mago is silent since 1.43. Phan reports the check
 * as "maybe", which is the runtime-correct verdict.
 *
 * References:
 * - PHPStan  https://github.com/phpstan/phpstan/issues/14938  (fix: phpstan-src#6025, 2.2.9)
 * - 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<psalm>: TypeDoesNotContainType, reported as never list<mixed> (#11905) // E<pzoom>: same TypeDoesNotContainType as Psalm // E<mir>: RedundantCondition — collapses the check. PHPStan reported always-false through 2.2.8 (#14938) and is silent since 2.2.9. Mago reported impossible-type-comparison in 1.19 and is silent since 1.43 (#2073), which is the runtime-correct verdict
        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<psalm>: TypeDoesNotContainType, reported as never list<mixed> (#11905) // E<pzoom>: same TypeDoesNotContainType as Psalm. PHPStan reported always-false through 2.2.8 (#14938) and is silent since 2.2.9. Mago is silent since 1.43 (#2073)
}

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