addFile( 'somefile.php', 'vars_in_scope['$b'] = \Psalm\Type::getBool(); $context->vars_in_scope['$foo'] = \Psalm\Type::getArray(); $this->analyzeFile('somefile.php', $context); $this->assertFalse(isset($context->vars_in_scope['$foo[\'a\']'])); } /** * @return array */ public function providerValidCodeParse() { return [ 'genericArrayCreationWithSingleIntValue' => [ ' [ '$out' => 'non-empty-array', ], ], 'genericArrayCreationWithInt' => [ ' [ '$out' => 'non-empty-array', ], ], 'generic2dArrayCreation' => [ ' [ '$out' => 'non-empty-array', ], ], 'generic2dArrayCreationAddedInIf' => [ ' 50) { $out[] = $bits; $bits = []; } $bits[] = 4; } $out[] = $bits;', 'assertions' => [ '$out' => 'non-empty-array>', ], ], 'genericArrayCreationWithObjectAddedInIf' => [ ' [ '$out' => 'array', ], ], 'genericArrayCreationWithElementAddedInSwitch' => [ ' [ '$out' => 'array', ], ], 'genericArrayCreationWithElementsAddedInSwitch' => [ ' [ '$out' => 'array', ], ], 'genericArrayCreationWithElementsAddedInSwitchWithNothing' => [ ' [ '$out' => 'array', ], ], 'implicitIntArrayCreation' => [ ' [ '$foo' => 'non-empty-array', ], ], 'implicit2dIntArrayCreation' => [ ' [ '$foo' => 'non-empty-array>', ], ], 'implicit3dIntArrayCreation' => [ ' [ '$foo' => 'non-empty-array>>', ], ], 'implicit4dIntArrayCreation' => [ ' [ '$foo' => 'non-empty-array>>>', ], ], 'implicitIndexedIntArrayCreation' => [ ' $text) { $bat[$text] = $bar[$i]; }', 'assertions' => [ '$foo' => 'array{0:string, 1:string, 2:string}', '$bar' => 'array{0:int, 1:int, 2:int}', '$bat' => 'array', ], ], 'implicitStringArrayCreation' => [ ' [ '$foo' => 'array{bar:string}', '$foo[\'bar\']' => 'string', ], ], 'implicit2dStringArrayCreation' => [ ' [ '$foo' => 'array{bar:array{baz:string}}', '$foo[\'bar\'][\'baz\']' => 'string', ], ], 'implicit3dStringArrayCreation' => [ ' [ '$foo' => 'array{bar:array{baz:array{bat:string}}}', '$foo[\'bar\'][\'baz\'][\'bat\']' => 'string', ], ], 'implicit4dStringArrayCreation' => [ ' [ '$foo' => 'array{bar:array{baz:array{bat:array{bap:string}}}}', '$foo[\'bar\'][\'baz\'][\'bat\'][\'bap\']' => 'string', ], ], '2Step2dStringArrayCreation' => [ ' []]; $foo["bar"]["baz"] = "hello";', 'assertions' => [ '$foo' => 'array{bar:array{baz:string}}', '$foo[\'bar\'][\'baz\']' => 'string', ], ], '2StepImplicit3dStringArrayCreation' => [ ' []]; $foo["bar"]["baz"]["bat"] = "hello";', 'assertions' => [ '$foo' => 'array{bar:array{baz:array{bat:string}}}', ], ], 'conflictingTypesWithNoAssignment' => [ ' ["a" => "b"], "baz" => [1] ];', 'assertions' => [ '$foo' => 'array{bar:array{a:string}, baz:array{0:int}}', ], ], 'implicitObjectLikeCreation' => [ ' 1, ]; $foo["baz"] = "a";', 'assertions' => [ '$foo' => 'array{bar:int, baz:string}', ], ], 'conflictingTypesWithAssignment' => [ ' ["a" => "b"], "baz" => [1] ]; $foo["bar"]["bam"]["baz"] = "hello";', 'assertions' => [ '$foo' => 'array{bar:array{a:string, bam:array{baz:string}}, baz:array{0:int}}', ], ], 'conflictingTypesWithAssignment2' => [ ' [ '$foo' => 'array{a:string, b:array}', '$foo[\'a\']' => 'string', '$foo[\'b\']' => 'array', '$bar' => 'string', ], ], 'conflictingTypesWithAssignment3' => [ ' [ '$foo' => 'array{a:string, b:array{c:array{d:string}}}', ], ], 'nestedObjectLikeAssignment' => [ ' [ '$foo' => 'array{a:array{b:string, c:int}}', ], ], 'conditionalObjectLikeAssignment' => [ ' "hello"]; if (rand(0, 10) === 5) { $foo["b"] = 1; } else { $foo["b"] = 2; }', 'assertions' => [ '$foo' => 'array{a:string, b:int}', ], ], 'arrayKey' => [ ' "foo", "b"=> "bar"]; $d = "a"; $e = $c[$d];', 'assertions' => [ '$b' => 'string', '$e' => 'string', ], ], 'conditionalCheck' => [ ' [], ], 'variableKeyArrayCreate' => [ ' [ '$a' => 'non-empty-array>', '$c' => 'non-empty-array>>', ], ], 'assignExplicitValueToGeneric' => [ '> */ $a = []; $a["foo"] = ["bar" => "baz"];', 'assertions' => [ '$a' => 'non-empty-array>', ], ], 'additionWithEmpty' => [ ' [ '$a' => 'array{0:string}', '$b' => 'array{0:string}', ], ], 'additionDifferentType' => [ ' [ '$a' => 'array{0:string}', '$b' => 'array{0:string}', ], ], 'present1dArrayTypeWithVarKeys' => [ '> */ $a = []; $foo = "foo"; $a[$foo][] = "bat";', 'assertions' => [], ], 'present2dArrayTypeWithVarKeys' => [ '>> */ $b = []; $foo = "foo"; $bar = "bar"; $b[$foo][$bar][] = "bat";', 'assertions' => [], ], 'objectLikeWithIntegerKeys' => [ ' [ '$b' => 'string', '$c' => 'int', '$d' => 'string', '$e' => 'int', ], ], 'objectLikeArrayAddition' => [ ' [2, 3]];', 'assertions' => [ '$foo' => 'array{a:int, b:array{0:int, 1:int}}', ], ], 'nestedObjectLikeArrayAddition' => [ ' [2, 3]];', 'assertions' => [ '$foo' => 'array{root:array{a:int, b:array{0:int, 1:int}}}', ], ], 'updateStringIntKey1' => [ ' [ '$a' => 'array{a:int, 0:int}', ], ], 'updateStringIntKey2' => [ ' [ '$b' => 'array{0:int, c:int}', ], ], 'updateStringIntKey3' => [ ' [ '$c' => 'array{0:int, c:int}', ], ], 'updateStringIntKey4' => [ ' [ '$d' => 'non-empty-array', ], ], 'updateStringIntKey5' => [ ' [ '$e' => 'non-empty-array', ], ], 'updateStringIntKeyWithIntRootAndNumberOffset' => [ ' [ '$a' => 'array{0:array{a:int, 0:int}}', ], ], 'updateStringIntKeyWithIntRoot' => [ ' [ '$b' => 'array{0:array{0:int, c:int}}', '$c' => 'array{0:array{0:int, c:int}}', '$d' => 'array{0:array}', '$e' => 'array{0:array}', ], ], 'updateStringIntKeyWithObjectLikeRootAndNumberOffset' => [ ' [ '$a' => 'array{root:array{a:int, 0:int}}', ], ], 'updateStringIntKeyWithObjectLikeRoot' => [ ' [ '$b' => 'array{root:array{0:int, c:int}}', '$c' => 'array{root:array{0:int, c:int}}', '$d' => 'array{root:array}', '$e' => 'array{root:array}', ], ], 'mixedArrayAssignmentWithStringKeys' => [ ' [], 'error_levels' => ['MixedArrayAssignment', 'MixedArrayAccess', 'MixedArgument'], ], 'mixedArrayCoercion' => [ ' [], 'error_levels' => ['MixedTypeCoercion', 'MixedArgument'], ], 'suppressMixedObjectOffset' => [ 'id] = $a; } echo $arr[0];', 'assertions' => [], 'error_levels' => ['MixedAssignment', 'MixedPropertyFetch', 'MixedArrayOffset', 'MixedArgument'], ], 'changeObjectLikeType' => [ ' "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}}', ], ], 'changeObjectLikeTypeInIf' => [ ' 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' => [ 'foo();', 'assertions' => [ '$a' => 'A', ], 'error_levels' => ['MixedMethodCall'], ], 'implementsArrayAccessInheritingDocblock' => [ ' */ 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' => [], 'error_levels' => ['MixedAssignment'], ], 'assignToNullDontDie' => [ ' [ '$a' => 'array{0:array}', ], 'error_levels' => ['PossiblyNullArrayAssignment'], ], 'stringAssignment' => [ ' [ '$str' => 'string', ], ], 'ignoreInvalidArrayOffset' => [ ' [], ]; $a["b"]["c"] = 0; foreach ([1, 2, 3] as $i) { /** * @psalm-suppress InvalidArrayOffset * @psalm-suppress MixedOperand * @psalm-suppress PossiblyUndefinedArrayOffset */ $a["b"]["d"] += $a["b"][$i]; }', 'assertions' => [], ], 'keyedIntOffsetArrayValues' => [ ' [ '$a' => 'array{0:string, 1:int}', '$a_values' => 'array', '$a_keys' => 'array', ], ], 'changeIntOffsetKeyValuesWithDirectAssignment' => [ ' [ '$b' => 'array{0:int, 1:int}', ], ], 'changeIntOffsetKeyValuesAfterCopy' => [ ' [ '$b' => 'array{0:string, 1:int}', '$c' => 'array{0:int, 1:int}', ], ], 'mergeIntOffsetValues' => [ ' [ '$d' => 'array{0:string, 1:int}', '$e' => 'array{0:string, 1:int, 2:string}', ], ], 'addIntOffsetToEmptyArray' => [ ' [ '$f' => 'array{0:string}', ], ], 'assignArrayOrSetNull' => [ ' [ '$a' => 'non-empty-array|null', ], ], 'assignArrayOrSetNullInElseIf' => [ ' [ '$a' => 'array|null', ], ], 'assignArrayOrSetNullInElse' => [ ' [ '$a' => 'non-empty-array|null', ], ], 'mixedMethodCallArrayAccess' => [ 'foo()] = 1; return $ret["a"]; }', 'assertions' => [], 'error_levels' => ['MixedMethodCall', 'MixedArrayOffset', 'MixedTypeCoercion'], ], 'mixedAccessNestedKeys' => [ ' $item) { $arr[$i]["a"]["b"] = 5; $arr[$i]["a"]["c"] = takesString($arr[$i]["a"]["c"]); } return $arr; }', 'assertions' => [], 'error_levels' => [ 'MixedArrayAccess', 'MixedAssignment', 'MixedArrayOffset', 'MixedArrayAssignment', 'MixedArgument', ], ], 'possiblyUndefinedArrayAccessWithIsset' => [ ' 1]; } else { $a = [2, 3]; } if (isset($a[0])) { echo $a[0]; }', ], 'possiblyUndefinedArrayAccessWithArrayKeyExists' => [ ' 1]; } else { $a = [2, 3]; } if (array_key_exists(0, $a)) { echo $a[0]; }', ], 'noCrashOnArrayKeyExistsBracket' => [ 'getIterator(); while ($iter->valid() && $count < $numToGet) { $value = $iter->current(); if ($value[0] != $commenter) { if (!array_key_exists($value[0], $posters)) { $posters[$value[0]] = 1; $count++; } } $iter->next(); } return array_keys($posters); } }', 'assertions' => [], 'error_levels' => [ 'MixedArrayAccess', 'MixedAssignment', 'MixedArrayOffset', 'MixedArgument', 'MixedTypeCoercion', ], ], 'accessArrayAfterSuppressingBugs' => [ ' [ ' 0, 1, 2, 3]; $arr = [1 => "one", 2 => "two", "three"];', ], 'noDuplicateImplicitIntArrayKeyLargeOffset' => [ ' "A", 95 => "a", "b", ];', ], 'constArrayAssignment' => [ ' 2]; $arr[BAR] = [6]; $bar = $arr[BAR][0];', ], 'castToArray' => [ ' "one"] : 0); $b = (array) null;', 'assertions' => [ '$a' => 'array{1?:string, 0?:int}', '$b' => 'array', ], ], 'getOnCoercedArray' => [ ' []] : []; } $out = getArray(); $out["attr"] = (array) ($out["attr"] ?? []); $out["attr"]["bar"] = 1;', 'assertions' => [ '$out[\'attr\'][\'bar\']' => 'int' ], ], 'arrayAssignmentOnMixedArray' => [ ' [], 'error_levels' => ['MixedAssignment'], ], 'implementsArrayAccessAllowNullOffset' => [ ' */ class C implements ArrayAccess { public function offsetExists(int $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} public function offsetSet(?int $offset, string $value) : void {} public function offsetUnset(int $offset) : void { } } $c = new C(); $c[] = "hello";', ], ]; } /** * @return array */ public function providerInvalidCodeParse() { return [ 'objectAssignment' => [ ' 'InvalidArrayAssignment', ], 'invalidArrayAccess' => [ ' 'InvalidArrayAssignment', ], 'possiblyUndefinedArrayAccess' => [ ' 1]; } else { $a = [2, 3]; } echo $a[0];', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'mixedStringOffsetAssignment' => [ ' 'MixedStringOffsetAssignment', 'error_level' => ['MixedAssignment'], ], 'mixedArrayArgument' => [ ' $foo */ function fooFoo(array $foo): void { } function barBar(array $bar): void { fooFoo($bar); } barBar([1, "2"]);', 'error_message' => 'MixedTypeCoercion', 'error_level' => ['MixedAssignment'], ], 'arrayPropertyAssignment' => [ 'strs = [new stdClass()]; // no issue emitted } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'incrementalArrayPropertyAssignment' => [ 'strs[] = new stdClass(); // no issue emitted } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'possiblyUndefinedArrayAccessWithArrayKeyExistsOnWrongKey' => [ ' 1]; } else { $a = [2, 3]; } if (array_key_exists("a", $a)) { echo $a[0]; }', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'possiblyUndefinedArrayAccessWithArrayKeyExistsOnMissingKey' => [ ' 1]; } else { $a = [2, 3]; } if (array_key_exists("b", $a)) { echo $a[0]; }', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'duplicateStringArrayKey' => [ ' 1, "b" => 2, "c" => 3, "c" => 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'duplicateIntArrayKey' => [ ' 1, 1 => 2, 2 => 3, 2 => 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'duplicateImplicitIntArrayKey' => [ ' 4, ];', 'error_message' => 'DuplicateArrayKey', ], 'mixedArrayAssignment' => [ ' 'MixedArrayAssignment', ], 'implementsArrayAccessAllowNullOffset' => [ ' */ class C implements ArrayAccess { public function offsetExists(int $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} public function offsetSet(int $offset, string $value) : void {} public function offsetUnset(int $offset) : void { } } $c = new C(); $c[] = "hello";', 'error_message' => 'NullArgument', ], 'storageKeyMustBeObject' => [ ' 'InvalidArgument' ], ]; } }