array{foo: int, ...}... unseals an array shape: the listed keys keep their declared
types, and any further key is admitted with an unconstrained value. The two
halves have to be probed separately, because an analyzer can get either one
wrong on its own — dropping the ... turns every extra key into a false
positive, while dropping the shape lets a missing or mistyped foo through.
References:
- Psalm "Unsealed array and list shapes", ... as shorthand for ...<array-key, mixed>| 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 | Unrecognized | arrays_unsealed_shape.php:21: Saw possibly unextractable annotation for a fragment of comment '* @param array{foo: int, ...} $shape': after array, did not see an element name (will guess based on comment order) [PhanUnextractableAnnotationElementName]
arrays_unsealed_shape.php:21: Saw a token Phan may have failed to parse after '* @param array{foo: int, ...} $shape': after array, saw '{' [PhanUnextractableAnnotationSuffix]
Notes: Phan parses sealed array shapes (`array{host: string, port: int}` in arrays_shape_required_keys), but the trailing `...` makes the whole annotation unextractable, so the parameter falls back to `array` and none of the three violations is checked. |
| phpstan | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Enforced reported Lv.5+ | arrays_unsealed_shape.php:37: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{} given. [identifier=argument.type] [reported-from-level=5]
arrays_unsealed_shape.php:38: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{buz: 42.0} given. [identifier=argument.type] [reported-from-level=5]
arrays_unsealed_shape.php:41: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{foo: 'one'} given. [identifier=argument.type] [reported-from-level=5]With strict-rulesarrays_unsealed_shape.php:37: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{} given. [identifier=argument.type]
arrays_unsealed_shape.php:38: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{buz: 42.0} given. [identifier=argument.type]
arrays_unsealed_shape.php:41: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{foo: 'one'} given. [identifier=argument.type]
|
| psalm | 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac next: 7.0.0-beta19 |
Enforced | arrays_unsealed_shape.php:37: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array<never, never> provided [InvalidArgument]
arrays_unsealed_shape.php:38: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array{buz: float(42)} provided [InvalidArgument]
arrays_unsealed_shape.php:41: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array{foo: 'one'} provided [InvalidArgument]
|
| mago | 1.46.0mago 1.46.0 | Enforced | arrays_unsealed_shape.php:37: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape`: expected `array{'foo': int, ...}`, but possibly received `array{}`. [possibly-invalid-argument]
arrays_unsealed_shape.php:38: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape`: expected `array{'foo': int, ...}`, but possibly received `array{'buz': float(42.0)}`. [possibly-invalid-argument]
arrays_unsealed_shape.php:41: Possible argument type mismatch for argument #1 of `Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape`: expected `array{'foo': int, ...}`, but possibly received `array{'foo': string('one')}`. [possibly-invalid-argument]
|
| mir | 0.70.1mir 0.70.1 | Enforced | arrays_unsealed_shape.php:37: InvalidArgument: Argument $shape of acceptsUnsealedShape() expects 'array{'foo': int}', got 'array{}' [MIR0201]
arrays_unsealed_shape.php:38: InvalidArgument: Argument $shape of acceptsUnsealedShape() expects 'array{'foo': int}', got 'array{'buz': 42}' [MIR0201]
arrays_unsealed_shape.php:41: InvalidArgument: Argument $shape of acceptsUnsealedShape() expects 'array{'foo': int}', got 'array{'foo': "one"}' [MIR0201]
Notes: mir honours the `...` -- the calls carrying undeclared keys are accepted -- but prints the expected type as `array{'foo': int}` without it, so the message reads as though the shape were sealed. |
| phpantom | 0.9.0phpantom_lsp 0.9.0 | Not enforced | No diagnostics reported.
|
| intelephense | 1.18.5intelephense 1.18.5 | Not enforced | No diagnostics reported.
Notes: Not specific to `...`: Intelephense does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. |
| phpy | 0.2.0phpy 0.2.0 | Not enforced | No diagnostics reported.
Notes: Not specific to `...`: phpy does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. |
| qodana | 262.8665.325Qodana 262.8665.325 | Incidental (3/3) | arrays_unsealed_shape.php:31: Value should be one of: "foo" [PhpArrayKeyDoesNotMatchArrayShapeInspection] arrays_unsealed_shape.php:32: Value should be one of: "foo" [PhpArrayKeyDoesNotMatchArrayShapeInspection] arrays_unsealed_shape.php:32: Value should be one of: "foo" [PhpArrayKeyDoesNotMatchArrayShapeInspection] arrays_unsealed_shape.php:37: Incomplete array according to shape declaration. Missing key(s): foo [PhpMissingArrayKeyInspection] arrays_unsealed_shape.php:38: Incomplete array according to shape declaration. Missing key(s): foo [PhpMissingArrayKeyInspection] arrays_unsealed_shape.php:38: Value should be one of: "foo" [PhpArrayKeyDoesNotMatchArrayShapeInspection] arrays_unsealed_shape.php:41: Expected parameter of type 'int', 'string' provided [PhpParamsInspection]
Expectation diffLine 31: Expected valid value to be accepted, got ["Value should be one of: \"foo\" [PhpArrayKeyDoesNotMatchArrayShapeInspection]"] Line 32: Expected valid value to be accepted, got ["Value should be one of: \"foo\" [PhpArrayKeyDoesNotMatchArrayShapeInspection]","Value should be one of: \"foo\" [PhpArrayKeyDoesNotMatchArrayShapeInspection]"] Notes: Reads the shape as sealed: the trailing `...` is dropped and the undeclared keys are reported as `Value should be one of: "foo"`. The declared key and its type are still enforced, so the shape is modeled and only the opt-out is missing. The one analyzer in this matrix that rejects the extra key. |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Unrecognized | arrays_unsealed_shape.php:23: param $shape miss matched with phpdoc type <<array{foo: int, ...}>> [funcParamTypeMissMatch]
|
| steins | 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) | Enforced | arrays_unsealed_shape.php:37: argument [] to acceptsUnsealedShape() violates declared @param array{foo: int, ...} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_unsealed_shape.php:38: argument ['buz' => 42.0] to acceptsUnsealedShape() violates declared @param array{foo: int, ...} $shape — declared contract violation [phpdoc.param-mismatch]
arrays_unsealed_shape.php:41: argument ['foo' => 'one'] to acceptsUnsealedShape() violates declared @param array{foo: int, ...} $shape — declared contract violation [phpdoc.param-mismatch]
|
| phpstan-strict | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Enforced | arrays_unsealed_shape.php:37: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{} given. [identifier=argument.type]
arrays_unsealed_shape.php:38: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{buz: 42.0} given. [identifier=argument.type]
arrays_unsealed_shape.php:41: Parameter #1 $shape of function Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...}, array{foo: 'one'} given. [identifier=argument.type]
|
| psalm-next | 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 | Enforced | arrays_unsealed_shape.php:37: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array<never, never> provided [InvalidArgument]
arrays_unsealed_shape.php:38: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array{buz: float(42)} provided [InvalidArgument]
arrays_unsealed_shape.php:41: Argument 1 of Conformance\Tests\ArraysUnsealedShape\acceptsUnsealedShape expects array{foo: int, ...<array-key, mixed>}, but array{foo: 'one'} provided [InvalidArgument]
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\ArraysUnsealedShape;
/**
* `array{foo: int, ...}`
*
* A trailing `...` unseals an array shape: the listed keys keep their declared
* types, and any further key is admitted with an unconstrained value. The two
* halves have to be probed separately, because an analyzer can get either one
* wrong on its own — dropping the `...` turns every extra key into a false
* positive, while dropping the shape lets a missing or mistyped `foo` through.
*
* References:
* - Psalm "Unsealed array and list shapes", `...` as shorthand for `...<array-key, mixed>`
*/
/**
* @param array{foo: int, ...} $shape
*/
function acceptsUnsealedShape(array $shape): void // T: array{foo: int, ...}
{
}
// The declared key on its own satisfies the shape.
acceptsUnsealedShape(['foo' => 1]); // V
// So does the declared key alongside undeclared ones of any type.
acceptsUnsealedShape(['foo' => 2, 'buz' => 42.0]); // V
acceptsUnsealedShape(['foo' => 3, 'buz' => 42.0, 'qux' => null]); // V
// `...` relaxes the undeclared keys only. `foo` is still required — an
// analyzer that widened the spelling to plain `array` accepts all three of
// these, so silence here says the shape was thrown away rather than honoured.
acceptsUnsealedShape([]); // E?: array{} does not have the required key foo
acceptsUnsealedShape(['buz' => 42.0]); // E?: an undeclared key does not stand in for foo
// and still an int.
acceptsUnsealedShape(['foo' => 'one']); // E?: foo is int, not string