← All results

Writing or unsetting a key changes the tracked shape

Group: Arrays and shapes · Category: phpdoc · File: arrays_sealed_shape_mutation.php

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

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
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-rules
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 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 diff
Line 62: Expected 1 error(s)
intelephense 1.18.5intelephense 1.18.5 Fail

No diagnostics reported.

Expectation diff
Line 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 diff
Line 44: Expected 1 error(s)
Line 62: Expected 1 error(s)
phpy 1.0.19490phpy 1.0.19490 Fail

No diagnostics reported.

Expectation diff
Line 44: Expected 1 error(s)
Line 62: Expected 1 error(s)
qodana 262.11879.104PhpStorm 262.11879.104 Fail

No diagnostics reported.

Expectation diff
Line 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 diff
Line 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]

Source

<?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
}