addFile( 'somefile.php', 'vars_in_scope['$b'] = Type::getBool(); $context->vars_in_scope['$foo'] = Type::getArray(); $this->analyzeFile('somefile.php', $context); $this->assertFalse(isset($context->vars_in_scope['$foo[\'a\']'])); } public function providerValidCodeParse(): iterable { return [ 'assignUnionOfLiterals' => [ 'code' => ' [ '$result===' => 'array{a: true, b: true}', '$resultOpt===' => 'array{a?: true, b?: true}', ], ], 'assignUnionOfLiteralsClassKeys' => [ 'code' => ' $v) { $vv = new $k; }', 'assertions' => [ '$result===' => 'array{a::class: true, b::class: true}', ], ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' [ '$out' => 'list{int}', ], ], 'genericArrayCreationWithInt' => [ 'code' => ' [ '$out' => 'non-empty-list', ], ], 'generic2dArrayCreation' => [ 'code' => ' [ '$out' => 'non-empty-list', ], ], 'generic2dArrayCreationAddedInIf' => [ 'code' => ' 50) { $out[] = $bits; $bits = []; } $bits[] = 4; } $out[] = $bits;', 'assertions' => [ '$out' => 'non-empty-list>', ], ], 'genericArrayCreationWithObjectAddedInIf' => [ 'code' => ' [ '$out' => 'list{0?: B}', ], ], 'genericArrayCreationWithElementAddedInSwitch' => [ 'code' => ' [ '$out' => 'list{0?: int}', ], ], 'genericArrayCreationWithElementsAddedInSwitch' => [ 'code' => ' [ '$out' => 'list{0?: int|string}', ], ], 'genericArrayCreationWithElementsAddedInSwitchWithNothing' => [ 'code' => ' [ '$out' => 'list{0?: int|string}', ], ], 'implicit2dIntArrayCreation' => [ 'code' => ' [ '$foo' => 'non-empty-list>', ], ], 'implicit3dIntArrayCreation' => [ 'code' => ' [ '$foo' => 'non-empty-list>>', ], ], 'implicit4dIntArrayCreation' => [ 'code' => ' [ '$foo' => 'non-empty-list>>>', ], ], 'implicitIndexedIntArrayCreation' => [ 'code' => ' $text) { $bat[$text] = $bar[$i]; }', 'assertions' => [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ 'code' => ' [ '$foo' => 'array{bar: string}', '$foo[\'bar\']' => 'string', ], ], 'implicit2dStringArrayCreation' => [ 'code' => ' [ '$foo' => 'array{bar: array{baz: string}}', '$foo[\'bar\'][\'baz\']' => 'string', ], ], 'implicit3dStringArrayCreation' => [ 'code' => ' [ '$foo' => 'array{bar: array{baz: array{bat: string}}}', '$foo[\'bar\'][\'baz\'][\'bat\']' => 'string', ], ], 'implicit4dStringArrayCreation' => [ 'code' => ' [ '$foo' => 'array{bar: array{baz: array{bat: array{bap: string}}}}', '$foo[\'bar\'][\'baz\'][\'bat\'][\'bap\']' => 'string', ], ], '2Step2dStringArrayCreation' => [ 'code' => ' []]; $foo["bar"]["baz"] = "hello";', 'assertions' => [ '$foo' => 'array{bar: array{baz: string}}', '$foo[\'bar\'][\'baz\']' => 'string', ], ], '2StepImplicit3dStringArrayCreation' => [ 'code' => ' []]; $foo["bar"]["baz"]["bat"] = "hello";', 'assertions' => [ '$foo' => 'array{bar: array{baz: array{bat: string}}}', ], ], 'conflictingTypesWithNoAssignment' => [ 'code' => ' ["a" => "b"], "baz" => [1] ];', 'assertions' => [ '$foo' => 'array{bar: array{a: string}, baz: list{int}}', ], ], 'implicitTKeyedArrayCreation' => [ 'code' => ' 1, ]; $foo["baz"] = "a";', 'assertions' => [ '$foo' => 'array{bar: int, baz: string}', ], ], 'conflictingTypesWithAssignment' => [ 'code' => ' ["a" => "b"], "baz" => [1] ]; $foo["bar"]["bam"]["baz"] = "hello";', 'assertions' => [ '$foo' => 'array{bar: array{a: string, bam: array{baz: string}}, baz: list{int}}', ], ], 'conflictingTypesWithAssignment2' => [ 'code' => ' [ '$foo' => 'array{a: string, b: non-empty-list}', '$foo[\'a\']' => 'string', '$foo[\'b\']' => 'non-empty-list', '$bar' => 'string', ], ], 'conflictingTypesWithAssignment3' => [ 'code' => ' [ '$foo' => 'array{a: string, b: array{c: array{d: string}}}', ], ], 'nestedTKeyedArrayAssignment' => [ 'code' => ' [ '$foo' => 'array{a: array{b: string, c: int}}', ], ], 'conditionalTKeyedArrayAssignment' => [ 'code' => ' "hello"]; if (rand(0, 10) === 5) { $foo["b"] = 1; } else { $foo["b"] = 2; }', 'assertions' => [ '$foo' => 'array{a: string, b: int}', ], ], 'arrayKey' => [ 'code' => ' "foo", "b"=> "bar"]; $d = "a"; $e = $c[$d];', 'assertions' => [ '$b' => 'string', '$e' => 'string', ], ], 'conditionalCheck' => [ 'code' => ' [], ], 'variableKeyArrayCreate' => [ 'code' => ' [ '$a' => 'array{boop: non-empty-list}', '$c' => 'array{boop: array{boop: non-empty-list}}', ], ], 'assignExplicitValueToGeneric' => [ 'code' => '> */ $a = []; $a["foo"] = ["bar" => "baz"];', 'assertions' => [ '$a' => 'array{foo: array{bar: string}, ...>}', ], ], 'additionWithEmpty' => [ 'code' => ' [ '$a' => 'list{string}', '$b' => 'list{string}', ], ], 'additionDifferentType' => [ 'code' => ' [ '$a' => 'array{0: string}', '$b' => 'array{0: string}', ], ], 'present1dArrayTypeWithVarKeys' => [ 'code' => '> */ $a = []; $foo = "foo"; $a[$foo][] = "bat";', 'assertions' => [], ], 'present2dArrayTypeWithVarKeys' => [ 'code' => '>> */ $b = []; $foo = "foo"; $bar = "bar"; $b[$foo][$bar][] = "bat";', 'assertions' => [], ], 'objectLikeWithIntegerKeys' => [ 'code' => ' [ '$b' => 'string', '$c' => 'int', '$d' => 'string', '$e' => 'int', ], ], 'objectLikeArrayAdditionNotNested' => [ 'code' => ' [2, 3]];', 'assertions' => [ '$foo' => 'array{a: int, b: list{int, int}}', ], ], 'objectLikeArrayIsNonEmpty' => [ 'code' => ' */ function test(array $arg): array { return $arg; } ', ], 'nestedTKeyedArrayAddition' => [ 'code' => ' [2, 3]];', 'assertions' => [ '$foo' => 'array{root: array{a: int, b: list{int, int}}}', ], ], 'updateStringIntKey1' => [ 'code' => ' [ '$a' => 'array{0: int, a: int}', ], ], 'updateStringIntKey2' => [ 'code' => ' [ '$b' => 'array{0: int, c: int}', ], ], 'updateStringIntKey3' => [ 'code' => ' [ '$c' => 'array{0: int, c: int}', ], ], 'updateStringIntKey4' => [ 'code' => ' [ '$d' => 'array{5: int, a: int}', ], ], 'updateStringIntKey5' => [ 'code' => ' [ '$e' => 'array{5: int, c: int}', ], ], 'updateStringIntKeyWithIntRootAndNumberOffset' => [ 'code' => ' [ '$a' => 'array{0: array{0: int, a: int}}', ], ], 'updateStringIntKeyWithIntRoot' => [ 'code' => ' [ '$b' => 'array{0: array{0: int, c: int}}', '$c' => 'array{0: array{0: int, c: int}}', '$d' => 'array{0: array{5: int, a: int}}', '$e' => 'array{0: array{5: int, c: int}}', ], ], 'updateStringIntKeyWithTKeyedArrayRootAndNumberOffset' => [ 'code' => ' [ '$a' => 'array{root: array{0: int, a: int}}', ], ], 'updateStringIntKeyWithTKeyedArrayRoot' => [ 'code' => ' [ '$b' => 'array{root: array{0: int, c: int}}', '$c' => 'array{root: array{0: int, c: int}}', '$d' => 'array{root: array{5: int, a: int}}', '$e' => 'array{root: array{5: int, c: int}}', ], ], 'mixedArrayAssignmentWithStringKeys' => [ 'code' => ' [ 'code' => ' [ 'code' => 'id] = $a; } echo $arr[0];', 'assertions' => [], 'ignored_issues' => ['MixedAssignment', 'MixedPropertyFetch', 'MixedArrayOffset', 'MixedArgument'], ], 'changeTKeyedArrayType' => [ 'code' => ' "c"]; $a["d"] = ["e" => "f"]; $a["b"] = 4; $a["d"]["e"] = 5;', 'assertions' => [ '$a[\'b\']' => 'int', '$a[\'d\']' => 'array{e: int}', '$a[\'d\'][\'e\']' => 'int', '$a' => 'array{b: int, d: array{e: int}}', ], ], 'changeTKeyedArrayTypeInIf' => [ 'code' => ' 3) { $a["b"] = new stdClass; } else { $a["b"] = ["e" => "f"]; } if ($a["b"] instanceof stdClass) { $a["b"] = []; } $a["b"]["e"] = "d";', 'assertions' => [ '$a' => 'array{b: array{e: string}}', '$a[\'b\']' => 'array{e: string}', '$a[\'b\'][\'e\']' => 'string', ], ], 'implementsArrayAccess' => [ 'code' => ' */ class A implements \ArrayAccess { /** * @param string|int $offset * @param mixed $value */ public function offsetSet($offset, $value): void {} /** @param string|int $offset */ public function offsetExists($offset): bool { return true; } /** @param string|int $offset */ public function offsetUnset($offset): void {} /** * @param string $offset * @return mixed */ public function offsetGet($offset) { return 1; } } $a = new A(); $a["bar"] = "cool"; $a["bar"]->foo();', 'assertions' => [ '$a' => 'A', ], 'ignored_issues' => ['MixedMethodCall'], ], 'mixedSwallowsArrayAssignment' => [ 'code' => 'offsetExists("baz");', ], 'implementsArrayAccessInheritingDocblock' => [ 'code' => ' */ class A implements \ArrayAccess { /** * @var array */ protected $data = []; /** * @param array $data */ public function __construct(array $data = []) { $this->data = $data; } /** * @param string $offset */ public function offsetExists($offset): bool { return isset($this->data[$offset]); } /** * @param string $offset */ public function offsetGet($offset) { return $this->data[$offset]; } /** * @param string $offset * @param mixed $value */ public function offsetSet($offset, $value): void { $this->data[$offset] = $value; } /** * @param string $offset */ public function offsetUnset($offset): void { unset($this->data[$offset]); } } class B extends A { /** * {@inheritdoc} */ public function offsetSet($offset, $value): void { echo "some log"; $this->data[$offset] = $value; } }', 'assertions' => [], 'ignored_issues' => ['MixedAssignment', 'MixedReturnStatement'], ], 'assignToNullDontDie' => [ 'code' => ' [ '$a' => 'array{0: non-empty-list}', ], 'ignored_issues' => ['PossiblyNullArrayAssignment'], ], 'stringAssignment' => [ 'code' => ' [ '$str' => 'string', ], ], 'ignoreInvalidArrayOffset' => [ 'code' => ' [], ]; $a["b"]["c"] = 0; foreach ([1, 2, 3] as $i) { /** * @psalm-suppress InvalidArrayOffset * @psalm-suppress MixedOperand * @psalm-suppress PossiblyUndefinedArrayOffset * @psalm-suppress MixedAssignment */ $a["b"]["d"] += $a["b"][$i]; }', 'assertions' => [], ], 'keyedIntOffsetArrayValues' => [ 'code' => ' [ '$a' => 'list{string, int}', '$a_values' => 'non-empty-list', '$a_keys' => 'non-empty-list>', ], ], 'changeIntOffsetKeyValuesWithDirectAssignment' => [ 'code' => ' [ '$b' => 'list{int, int}', ], ], 'changeIntOffsetKeyValuesAfterCopy' => [ 'code' => ' [ '$b' => 'list{string, int}', '$c' => 'list{int, int}', ], ], 'mergeIntOffsetValues' => [ 'code' => ' [ '$d' => 'list{string, int}', '$e' => 'list{string, int, string}', ], ], 'addIntOffsetToEmptyArray' => [ 'code' => ' [ '$f' => 'array{0: string}', ], ], 'dontIncrementIntOffsetForKeyedItems' => [ 'code' => ' 2, 3];', 'assertions' => [ '$a' => 'array{0: int, 1: int, a: int}', ], ], 'assignArrayOrSetNull' => [ 'code' => ' [ '$a===' => 'list{4}|null', ], ], 'assignArrayOrSetNullInElseIf' => [ 'code' => ' [ '$a' => 'list{0?: int}|null', ], ], 'assignArrayOrSetNullInElse' => [ 'code' => ' [ '$a' => 'list{int}|null', ], ], 'mixedMethodCallArrayAccess' => [ 'code' => 'foo()] = 1; return $ret["a"]; }', 'assertions' => [], 'ignored_issues' => ['MixedMethodCall', 'MixedArrayOffset'], ], 'mixedAccessNestedKeys' => [ 'code' => ' $item) { $arr[$i]["a"]["b"] = 5; $arr[$i]["a"]["c"] = takesString($arr[$i]["a"]["c"]); } return $arr; }', 'assertions' => [], 'ignored_issues' => [ 'MixedArrayAccess', 'MixedAssignment', 'MixedArrayOffset', 'MixedArrayAssignment', 'MixedArgument', ], ], 'possiblyUndefinedArrayAccessWithIsset' => [ 'code' => ' 1]; } else { $a = [2, 3]; } if (isset($a[0])) { echo $a[0]; }', ], 'accessArrayAfterSuppressingBugs' => [ 'code' => ' [ 'code' => ' 0, 1, 2, 3]; $arr = [1 => "one", 2 => "two", "three"];', ], 'noDuplicateImplicitIntArrayKeyLargeOffset' => [ 'code' => ' "A", 95 => "a", "b", ];', ], 'constArrayAssignment' => [ 'code' => ' 2]; $arr[BAR] = [6]; $bar = $arr[BAR][0];', ], 'castToArray' => [ 'code' => ' "one"] : 0); $b = (array) null;', 'assertions' => [ '$a' => 'array{0?: int, 1?: string}', '$b' => 'array', ], ], 'getOnCoercedArray' => [ 'code' => ' []] : []; } $out = getArray(); $out["attr"] = (array) ($out["attr"] ?? []); $out["attr"]["bar"] = 1;', 'assertions' => [ '$out[\'attr\'][\'bar\']' => 'int', ], ], 'arrayAssignmentOnMixedArray' => [ 'code' => ' [], 'ignored_issues' => ['MixedAssignment'], ], 'implementsArrayAccessAllowNullOffset' => [ 'code' => ' */ class C implements ArrayAccess { public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} public function offsetSet(mixed $offset, mixed $value) : void {} public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'checkEmptinessAfterConditionalArrayAdjustment' => [ 'code' => 'arr["a"] = "hello"; } if (!$this->arr) {} } }', ], 'arrayAssignmentAddsTypePossibilities' => [ 'code' => ' 0]; if (is_int($value["a"])) {} }', ], 'coercePossiblyNullKeyToEmptyString' => [ 'code' => ' */ function foo(): array { $array = []; /** @psalm-suppress PossiblyNullArrayOffset */ $array[string_or_null()] = null; return $array; }', ], 'coerceNullKeyToEmptyString' => [ 'code' => ' */ function foo(): array { $array = []; /** @psalm-suppress NullArrayOffset */ $array[null] = null; return $array; }', ], 'listUsedAsArray' => [ 'code' => ' [ '$a' => 'list{int, int}', ], ], 'listTakesEmptyArray' => [ 'code' => ' $arr */ function takesList(array $arr) : void {} $a = []; takesList($a);', 'assertions' => [ '$a' => 'array', ], ], 'listCreatedInSingleStatementUsedAsArray' => [ 'code' => ' $arr */ function takesList(array $arr) : void {} $a = [1, 2]; takesArray($a); takesList($a); $a[] = 3; takesArray($a); takesList($a); $b = $a; $b[] = rand(0, 10);', 'assertions' => [ '$a' => 'list{int, int, int}', '$b' => 'list{int, int, int, int<0, 10>}', ], ], 'listMergedWithTKeyedArrayList' => [ 'code' => ' $arr */ function takesAnotherList(array $arr) : void {} /** @param list $arr */ function takesList(array $arr) : void { if (rand(0, 1)) { $arr = [1, 2, 3]; } takesAnotherList($arr); }', ], 'listMergedWithTKeyedArrayListAfterAssertion' => [ 'code' => ' $arr */ function takesAnotherList(array $arr) : void {} /** @param list $arr */ function takesList(array $arr) : void { if ($arr) { $arr = [4, 5, 6]; } takesAnotherList($arr); }', ], 'nonEmptyAssertionOnListElement' => [ 'code' => '> $arr */ function takesList(array $arr) : void { if (!empty($arr[0])) { foreach ($arr[0] as $k => $v) {} } }', 'assertions' => [], 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nonEmptyAssignmentToListElement' => [ 'code' => ' $arr * @return non-empty-list */ function takesList(array $arr) : array { $arr[0] = "food"; return $arr; }', ], 'unpackedArgIsList' => [ 'code' => ' */ private $ints = []; /** @no-named-arguments */ public function set(int ...$ints): void { $this->ints = $ints; } }', ], 'assignStringFirstChar' => [ 'code' => ' $arr */ function foo(array $arr) : string { $arr[0][0] = "a"; return $arr[0]; }', ], 'arraySpread' => [ 'code' => ' 1, 1 => 2, 3]; $arr2 = [...$arr1]; $arr3 = [1 => 0, ...$arr1];', 'assertions' => [ '$result' => 'list{int, int, int, int, int, int, int, int}', '$arr2' => 'list{int, int, int}', '$arr3' => 'array{1: int, 2: int, 3: int, 4: int}', ], ], 'arraySpreadWithString' => [ 'code' => ' 0, ...["a" => 1], ...["b" => 2] ];', 'assertions' => [ '$x===' => 'array{a: 1, b: 2}', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'constantArraySpreadWithString' => [ 'code' => ' "a", "b" => "b", ]; } class ChildClass extends BaseClass { public const A = [ ...parent::KEYS, "c" => "c", ]; } $a = ChildClass::A;', 'assertions' => [ '$a===' => "array{a: 'a', b: 'b', c: 'c'}", ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'listPropertyAssignmentAfterIsset' => [ 'code' => ' */ private $list = []; public function override(int $offset): void { if (isset($this->list[$offset])) { $this->list[$offset] = "a"; } } }', ], 'propertyAssignmentToTKeyedArrayIntKeys' => [ 'code' => 'baz[rand(0, 1) ? 0 : 1] = $str; } }', ], 'propertyAssignmentToTKeyedArrayStringKeys' => [ 'code' => ' "c", "b" => "d"]; public function append(string $str) : void { $this->baz[rand(0, 1) ? "a" : "b"] = $str; } }', ], 'arrayMixedMixedNotAllowedFromObject' => [ 'code' => ' $v) { $arr[$k] = $v; } return $arr; }', ], 'arrayMixedMixedNotAllowedFromMixed' => [ 'code' => ' "foo"]; /** * @psalm-suppress MixedAssignment * @psalm-suppress MixedArrayOffset */ foreach ($a as $k => $v) { $arr[$k] = $v; } return $arr; }', ], 'assignNestedKey' => [ 'code' => ' */ function getAutoComplete(array $data): array { $response = ["s" => []]; foreach ($data as $suggestion) { $response["s"][$suggestion] = true; } return $response["s"]; }', ], 'assignArrayUnion' => [ 'code' => ' [ 'code' => ' [ '$arr' => 'array', ], ], 'dontUpdateMixedArrayWithStringKey' => [ 'code' => ' [ 'code' => ' [ '$options[\'b\']' => 'mixed', ], ], 'assignWithLiteralStringKey' => [ 'code' => ' $i * @return array */ function addOneEntry(array $i, int $id): array { $i[$id][rand(0, 1) ? "internal" : "ported"] = true; return $i; }', ], 'binaryOperation' => [ 'code' => ' ""] ); $a += ["e" => new RuntimeException()];', 'assertions' => [ '$a' => 'array{c: RuntimeException, e: RuntimeException}', ], ], 'mergeArrayKeysProperly' => [ 'code' => ', bool> $arr * @return array, bool> */ function createForEntity(array $arr) { $arr[SomeEntity::class] = true; return $arr; }', ], 'lowercaseStringMergeWithLiteral' => [ 'code' => ' $foo * @return array */ function foo(array $foo) : array { $foo["hello"] = true; return $foo; }', ], 'updateListValueAndMaintainListnessAfterGreaterThanOrEqual' => [ 'code' => ' $l * @return list */ function takesList(array $l) { if (count($l) < 2) { throw new \Exception("bad"); } $l[1] = $l[1] + 1; return $l; }', ], 'updateListValueAndMaintainListnessAfterNotIdentical' => [ 'code' => ' $l * @return list */ function takesList(array $l) { if (count($l) !== 2) { throw new \Exception("bad"); } $l[1] = $l[1] + 1; return $l; }', ], 'unpackTypedIterableIntoArray' => [ 'code' => ' $data * @return list */ function unpackIterable(iterable $data): array { return [...$data]; }', ], 'unpackTypedTraversableIntoArray' => [ 'code' => ' $data * @return list */ function unpackIterable(Traversable $data): array { return [...$data]; }', ], 'unpackEmptyArrayIsEmpty' => [ 'code' => ' ['$x===' => 'array'], ], 'unpackListCanBeEmpty' => [ 'code' => ' */ $x = []; /** @var list */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'list'], ], 'unpackNonEmptyListIsNotEmpty' => [ 'code' => ' */ $x = []; /** @var non-empty-list */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'list{int, int, ...}'], ], 'unpackEmptyKeepsCorrectKeys' => [ 'code' => ' ['$e===' => 'list{1, 2, 3}'], ], 'unpackArrayCanBeEmpty' => [ 'code' => ' */ $x = []; /** @var array */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'array'], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackNonEmptyArrayIsNotEmpty' => [ 'code' => ' */ $x = []; /** @var non-empty-array */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'non-empty-array'], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackIntKeyedArrayResultsInList' => [ 'code' => ' */ $x = []; /** @var array */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'list'], ], 'unpackStringKeyedArrayPhp8.1' => [ 'code' => ' */ $x = []; /** @var array */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => 'array'], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackLiteralStringKeyedArrayPhp8.1' => [ 'code' => ' */ $x = []; /** @var array<"baz", int> */ $y = []; $x = [...$x, ...$y]; ', 'assertions' => ['$x===' => "array<'bar'|'baz'|'foo', int>"], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackArrayShapesUnionsLaterUnpacks' => [ 'code' => ' 1, "bar" => 2, 10 => 3]; /** @var array */ $a = []; /** @var list<5> */ $b = []; /** @var array */ $c = []; $x = [...$a, ...$b, ...$c, ...$shape]; // Shape is last so it overrides previous $y = [...$shape, ...$a, ...$b, ...$c]; // Shape is first, but only possibly matching keys union their values ', 'assertions' => [ '$x===' => 'array{0: 3, bar: 2, foo: 1, ...}', '$y===' => 'array{0: 3|4|5|6, bar: 2|6, foo: 1|6, ...}', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackNonObjectlike' => [ 'code' => ' */ function test(): array { return []; } $x = [...test(), "a" => "b"]; ', 'assertions' => ['$x===' => "array{a: 'b', ..., mixed>}"], ], 'checkTraversableUnpackTemplatesCorrectly' => [ 'code' => ' */ interface Foo extends Traversable {} /** * @param Foo<"a"|"b", "c"|"d", "e"|"f", "g"|"h"> $foo * @return array<"e"|"f", "g"|"h"> */ function foobar(Foo $foo): array { return [...$foo]; } ', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackIncorrectlyExtendedInterface' => [ 'code' => ' */ interface Foo extends Traversable {} /** * @psalm-suppress MissingTemplateParam * @template TKey * @extends Foo */ interface Bar extends Foo {} /** * @param Bar $bar * @return list */ function foobar(Bar $bar): array { $unpacked = [...$bar]; return $unpacked; } ', ], 'unpackGrandchildOfTraversable' => [ 'code' => ' */ interface Foo extends Traversable {} /** @extends Foo<"a", "b", "c", "d"> */ interface Bar extends Foo {} /** * @return array<"c", "d"> */ function foobar(Bar $bar): array { return [...$bar]; } ', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackNonGenericGrandchildOfTraversable' => [ 'code' => ' */ interface Foo extends Traversable {} interface Bar extends Foo {} /** * @return array */ function foobar(Bar $bar): array { return [...$bar]; } ', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackTNamedObjectShouldUseTemplateConstraints' => [ 'code' => ' */ interface Foo extends Traversable {} /** * @return array<"a"|"b", "c"|"d"> */ function foobar(Foo $foo): array { return [...$foo]; } ', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'ArrayOffsetNumericSupPHPINTMAX' => [ 'code' => ' 1, "9223372036854775809" => 2 ]; ', ], 'assignToListWithForeachKey' => [ 'code' => ' $list * @return list */ function getList(array $list): array { foreach ($list as $key => $value) { $list[$key] = $value . "!"; } return $list; }', ], 'ArrayCreateTemplateArrayKey' => [ 'code' => ' 123]; }', ], 'assignStringIndexed' => [ 'code' => ' $array * @return non-empty-array */ function getArray(array $array): array { if (rand(0, 1)) { $array["a"] = 2; } else { $array["b"] = 1; } return $array; }', ], 'castPossiblyArray' => [ 'code' => ' $a * @return list */ function addHeaders($a): array { return (array)$a; }', ], 'ClassConstantAsKey' => [ 'code' => ' */ public static function getNames(): array { return [ self::C_ONE => "One", self::C_TWO => "Two", ]; } public function getThisName(): string { $names = self::getNames(); $aprop = $this->aprop; return $names[$aprop]; } }', ], 'AddTwoSealedArrays' => [ 'code' => ' 16, ]; public const TWO = [ 17 => 17, ]; public const THREE = [ 18 => 18, ]; } $_a = Token::ONE + Token::TWO + Token::THREE; ', 'assertions' => ['$_a===' => 'array{16: 16, 17: 17, 18: 18}'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' $data * @return array */ function unpackIterable(iterable $data): array { return [...$data]; }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackTypedTraversableWithStringKeysIntoArray' => [ 'code' => ' $data * @return array */ function unpackIterable(Traversable $data): array { return [...$data]; }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackArrayWithArrayKeyIntoArray' => [ 'code' => ' $data * @return array */ function unpackArray(array $data): array { return [...$data]; }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'unpackArrayWithTwoTypesNotObjectLike' => [ 'code' => ' */ function posiviteIntegers(): array { return [1]; } $_a = [...posiviteIntegers(), int()]; /** @psalm-check-type $_a = non-empty-list */ ', ], 'nullableDestructuring' => [ 'code' => ' [ '$_foo' => 'null|string', '$_bar' => 'null|string', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'allowsArrayAccessNullOffset' => [ 'code' => ' */ class C implements ArrayAccess { public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} public function offsetSet(mixed $offset, mixed $value) : void {} public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'conditionalRestrictedDocblockKeyAssignment' => [ 'code' => ' [ "active" => false, "icon" => "phone-tube", ], "stat" => [ "active" => false, "icon" => "review", ], "booking" => [ "active" => false, "icon" => "settings", ], "support" => [ "active" => false, "icon" => "help", ], ]; } $items = getSections(); /** @var string */ $currentAction = ""; if (\array_key_exists($currentAction, $items)) { $items[$currentAction]["active"] = true; }', ], 'listAppendShape' => [ 'code' => ' [ '$a===' => 'list{0, 1, 2}', '$b===' => 'list{0, 1, 2}', ], ], 'appendValuesToMap' => [ 'code' => ' */ function defaultQueryParams(): array { return [ "foo" => "123", "bar" => "baz", ]; } /** * @return array */ function getQueryParams(): array { $queryParams = defaultQueryParams(); $queryParams["a"] = "zzz"; return $queryParams; }', ], 'stringIntKeys' => [ 'code' => ' $arg * @return bool */ function foo($arg) { foreach ($arg as $k => $v) { if ( $k === 15 ) { return true; } if ( $k === 17 ) { return false; } } return true; } $x = ["15" => "a", 17 => "b"]; foo($x);', ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'objectAssignment' => [ 'code' => ' 'UndefinedMethod', ], 'invalidArrayAccess' => [ 'code' => ' 'InvalidArrayAssignment', ], 'possiblyUndefinedArrayAccess' => [ 'code' => ' 1]; } else { $a = [2, 3]; } echo $a[0];', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'mixedStringOffsetAssignment' => [ 'code' => ' 'MixedStringOffsetAssignment', 'ignored_issues' => ['MixedAssignment'], ], 'mixedArrayArgument' => [ 'code' => ' $foo */ function fooFoo(array $foo): void { } function barBar(array $bar): void { fooFoo($bar); } barBar([1, "2"]);', 'error_message' => 'MixedArgumentTypeCoercion', 'ignored_issues' => ['MixedAssignment'], ], 'arrayPropertyAssignment' => [ 'code' => 'strs = [new stdClass()]; // no issue emitted } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'incrementalArrayPropertyAssignment' => [ 'code' => 'strs[] = new stdClass(); // no issue emitted } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'duplicateStringArrayKey' => [ 'code' => ' 1, "b" => 2, "c" => 3, "c" => 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'duplicateIntArrayKey' => [ 'code' => ' 1, 1 => 2, 2 => 3, 2 => 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'duplicateImplicitIntArrayKey' => [ 'code' => ' 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'mixedArrayAssignmentOnVariable' => [ 'code' => ' 'MixedArrayAssignment', ], 'storageKeyMustBeObject' => [ 'code' => ' 'InvalidArgument', ], 'listUsedAsArrayWrongType' => [ 'code' => ' 'InvalidArgument', ], 'listUsedAsArrayWrongListType' => [ 'code' => ' $arr */ function takesArray(array $arr) : void {} $a = []; $a[] = 1; $a[] = 2; takesArray($a);', 'error_message' => 'InvalidArgument', ], 'nonEmptyAssignmentToListElementChangeType' => [ 'code' => ' $arr * @return non-empty-list */ function takesList(array $arr) : array { $arr[0] = 5; return $arr; }', 'error_message' => 'InvalidReturnStatement', ], 'preventArrayAssignmentOnReturnValue' => [ 'code' => 'foo()[3] = 5;', 'error_message' => 'InvalidArrayAssignment', ], 'mergeIntWithMixed' => [ 'code' => ' 'InvalidReturnStatement', ], 'mergeIntWithNestedMixed' => [ 'code' => ' 'InvalidReturnStatement', ], 'mergeWithDeeplyNestedArray' => [ 'code' => ' 'NullableReturnStatement', ], 'ArrayCreateOffsetObject' => [ 'code' => ' "a"]; ', 'error_message' => 'InvalidArrayOffset', ], 'ArrayDimOffsetObject' => [ 'code' => ' 'InvalidArrayOffset', ], 'ArrayCreateOffsetResource' => [ 'code' => ' "a"]; ', 'error_message' => 'InvalidArrayOffset', ], 'ArrayDimOffsetResource' => [ 'code' => ' 'InvalidArrayOffset', ], 'ArrayCreateOffsetBool' => [ 'code' => ' "a"]; ', 'error_message' => 'InvalidArrayOffset', ], 'ArrayDimOffsetBool' => [ 'code' => ' 'InvalidArrayOffset', ], 'ArrayCreateOffsetStringable' => [ 'code' => ' "a"];', 'error_message' => 'InvalidArrayOffset', ], 'ArrayDimOffsetStringable' => [ 'code' => ' 'InvalidArrayOffset', ], 'coerceListToArray' => [ 'code' => ' $_bar */ function foo(array $_bar) : void {} /** * @param list $bar */ function baz(array $bar) : void { foo((array) $bar); }', 'error_message' => 'RedundantCast', ], 'arrayValuesOnList' => [ 'code' => ' $a * @return list */ function foo(array $a) : array { return array_values($a); }', 'error_message' => 'RedundantFunctionCall', ], 'assignToListWithUpdatedForeachKey' => [ 'code' => ' $list * @return list */ function getList(array $list): array { foreach ($list as $key => $value) { $list[$key + 1] = $value . "!"; } return $list; }', 'error_message' => 'LessSpecificReturnStatement', ], // Skipped because the ref-type of array_pop was fixed (list->list) 'SKIPPED-assignToListWithAlteredForeachKeyVar' => [ 'code' => ' $list * @return list */ function getList(array $list): array { foreach ($list as $key => $value) { if (rand(0, 1)) { array_pop($list); } $list[$key] = $value . "!"; } return $list; }', 'error_message' => 'InvalidReturnStatement', ], 'createArrayWithMixedOffset' => [ 'code' => ' 5]; return $arr; }', 'error_message' => 'MixedArrayOffset', ], 'falseArrayAssignment' => [ 'code' => ' 'InvalidArrayOffset', ], 'TemplateAsKey' => [ 'code' => ' $weird_array */ public function getThisName($offset, $weird_array): string { return $weird_array[$offset]; } }', 'error_message' => 'MixedArrayAccess', 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' $data * @return list */ function unpackIterable(iterable $data): array { return [...$data]; } ', 'error_message' => 'DuplicateArrayKey', 'ignored_issues' => [], 'php_version' => '8.0', ], 'unpackTypedTraversableWithStringKeysIntoArray' => [ 'code' => ' $data * @return list */ function unpackIterable(Traversable $data): array { return [...$data]; } ', 'error_message' => 'DuplicateArrayKey', 'ignored_issues' => [], 'php_version' => '8.0', ], 'unpackArrayWithArrayKeyIntoArray' => [ 'code' => ' $data * @return list */ function unpackArray(array $data): array { return [...$data]; } ', 'error_message' => 'DuplicateArrayKey', 'ignored_issues' => [], 'php_version' => '8.0', ], 'unpackNonIterable' => [ 'code' => ' 'InvalidOperand', ], 'cantUnpackWhenKeyIsntArrayKey' => [ 'code' => ' */ $foo = []; $bar = [...$foo]; ', 'error_message' => 'InvalidOperand', ], 'unpackTraversableWithKeyOmitted' => [ 'code' => ' */ interface Foo extends Traversable {} /** * @return array */ function foobar(Foo $foo): array { return [...$foo]; } ', 'error_message' => 'InvalidOperand', ], ]; } }