$shape['extra'] = … or unset($shape['foo']) on a
local array{foo: int} is not itself an error: the variable just holds a
different shape afterwards. The question is whether the tool tracks that
shape, which shows when the mutated value is handed back to an
array{foo: int} receiver. arrays_shape_sealed_by_default asks the
extra-key question at a call site without the mutation.
References:
- PHPStan array shapes
- Psalm unsealed array shapes, where ... is the opt-in| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.11Phan 6.0.11 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Pass | arrays_sealed_shape_mutation.php:44: Argument 1 ($shape) is $shape of type array{foo:'x'} but \Conformance\Tests\ArraysSealedShapeMutation\takesFoo() takes array{foo:int} defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_sealed_shape_mutation.php:25 [PhanTypeMismatchArgument]
arrays_sealed_shape_mutation.php:62: Argument 1 ($shape) is $shape of type array{} but \Conformance\Tests\ArraysSealedShapeMutation\takesFoo() takes array{foo:int} defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_sealed_shape_mutation.php:25 [PhanTypeMismatchArgument] |
| phpstan | 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 | Pass reported Lv.5+ | arrays_sealed_shape_mutation.php:44: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: 'x'} given. [reported-from-level=5]
arrays_sealed_shape_mutation.php:53: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: int, extra: 'x'} given. [reported-from-level=5]
arrays_sealed_shape_mutation.php:62: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{} given. [reported-from-level=5]With strict-rulesarrays_sealed_shape_mutation.php:44: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: 'x'} given.
arrays_sealed_shape_mutation.php:53: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: int, extra: 'x'} given.
arrays_sealed_shape_mutation.php:62: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{} given. |
| psalm | 6.20.9Psalm 6.20.9@f67170ac3c3b931f5088681e21d6c8babc601c08 next: 7.0.4 |
Pass | arrays_sealed_shape_mutation.php:44: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array{foo: 'x'} provided [InvalidArgument]
arrays_sealed_shape_mutation.php:53: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array{extra: 'x', foo: int} with additional array shape fields (extra) was provided [InvalidArgument]
arrays_sealed_shape_mutation.php:62: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array<never, never> provided [InvalidArgument] |
| mago | 1.59.0mago 1.59.0 | Pass | arrays_sealed_shape_mutation.php:44: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysSealedShapeMutation\takesFoo`: expected `array{'foo': int}`, but possibly received `array{'foo': string('x')}`. [possibly-invalid-argument]
arrays_sealed_shape_mutation.php:53: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysSealedShapeMutation\takesFoo`: expected `array{'foo': int}`, but possibly received `array{'extra': string('x'), 'foo': int}`. [possibly-invalid-argument]
arrays_sealed_shape_mutation.php:62: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysSealedShapeMutation\takesFoo`: expected `array{'foo': int}`, but possibly received `array{}`. [possibly-invalid-argument] |
| mir | 0.91.0mir 0.91.0 | Pass | arrays_sealed_shape_mutation.php:44: InvalidArgument: Argument $shape of takesFoo() expects 'array{'foo': int}', got 'array{'foo': "x"}' [MIR0201]
arrays_sealed_shape_mutation.php:62: InvalidArgument: Argument $shape of takesFoo() expects 'array{'foo': int}', got 'array{}' [MIR0201] |
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Fail | arrays_sealed_shape_mutation.php:44: Argument 1 ($shape) expects array{foo: int}, got array{foo: 'x'} [type_mismatch_argument]Expectation diffLine 62: Expected 1 error(s) |
| intelephense | 1.18.5intelephense 1.18.5 | Fail | No diagnostics reported. Expectation diffLine 44: Expected 1 error(s) Line 62: Expected 1 error(s) |
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Fail | No diagnostics reported. Expectation diffLine 44: Expected 1 error(s) Line 62: Expected 1 error(s) |
| phpy | 1.0.19490phpy 1.0.19490 | Fail | No diagnostics reported. Expectation diffLine 44: Expected 1 error(s) Line 62: Expected 1 error(s) |
| qodana | 262.11879.104PhpStorm 262.11879.104 | Fail | No diagnostics reported. Expectation diffLine 44: Expected 1 error(s) Line 62: 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 | arrays_sealed_shape_mutation.php:25: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]
arrays_sealed_shape_mutation.php:32: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]
arrays_sealed_shape_mutation.php:41: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]
arrays_sealed_shape_mutation.php:50: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]
arrays_sealed_shape_mutation.php:59: param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]Expectation diffLine 44: Expected 1 error(s)
Line 62: Expected 1 error(s)
Line 25: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"]
Line 32: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"]
Line 41: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"]
Line 50: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"]
Line 59: Unexpected errors ["param $shape miss matched with phpdoc type <<array{foo: int}>> [funcParamTypeMissMatch]"] |
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Pass | arrays_sealed_shape_mutation.php:44: argument a value of type array{foo: 'x'} to takesFoo() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_sealed_shape_mutation.php:53: argument a value of type non-empty-array{extra: 'x', foo: int, ...} to takesFoo() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_sealed_shape_mutation.php:62: argument a value of type array{} to takesFoo() violates declared @param array{foo: int} $shape — declared contract violation [phpdoc.param-mismatch] |
| phpstan-strict | 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 | Pass | arrays_sealed_shape_mutation.php:44: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: 'x'} given.
arrays_sealed_shape_mutation.php:53: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{foo: int, extra: 'x'} given.
arrays_sealed_shape_mutation.php:62: Parameter #1 $shape of function Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, array{} given. |
| psalm-next | 7.0.4Psalm 7.0.4@fa5b1f6e665cb743838ffd865b8fc96207f9409f | Pass | arrays_sealed_shape_mutation.php:44: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array{foo: 'x'} provided [InvalidArgument]
arrays_sealed_shape_mutation.php:53: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array{extra: 'x', foo: int} with additional array shape fields (extra) was provided [InvalidArgument]
arrays_sealed_shape_mutation.php:62: Argument 1 of Conformance\Tests\ArraysSealedShapeMutation\takesFoo expects array{foo: int}, but array<never, never> provided [InvalidArgument] |
<?php
declare(strict_types=1);
namespace Conformance\Tests\ArraysSealedShapeMutation;
/**
* Writing or unsetting a key changes the tracked shape.
*
* Arrays are values, so `$shape['extra'] = …` or `unset($shape['foo'])` on a
* local `array{foo: int}` is not itself an error: the variable just holds a
* different shape afterwards. The question is whether the tool tracks that
* shape, which shows when the mutated value is handed back to an
* `array{foo: int}` receiver. `arrays_shape_sealed_by_default` asks the
* extra-key question at a call site without the mutation.
*
* References:
* - PHPStan array shapes
* - Psalm unsealed array shapes, where `...` is the opt-in
*/
/**
* @param array{foo: int} $shape
*/
function takesFoo(array $shape): void
{
}
/**
* @param array{foo: int} $shape
*/
function overwritesDeclaredKey(array $shape): void
{
$shape['foo'] = 2;
takesFoo($shape); // V: the declared key with a matching type keeps the shape
}
/**
* @param array{foo: int} $shape
*/
function changesDeclaredKeyType(array $shape): void
{
$shape['foo'] = 'x';
takesFoo($shape); // E: foo now holds a string
}
/**
* @param array{foo: int} $shape
*/
function addsExtraKey(array $shape): void
{
$shape['extra'] = 'x';
takesFoo($shape); // E?: a sealed array{foo: int} does not admit the extra key — sealedness is tool-dependent
}
/**
* @param array{foo: int} $shape
*/
function unsetsRequiredKey(array $shape): void
{
unset($shape['foo']);
takesFoo($shape); // E: the required key foo is gone
}