expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', ' $arr */ function takesArrayIteratorOfString(array $arr): void { echo $arr["hello"]; }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureArrayOffsetsExistWithIssetCheck(): void { Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', ' $arr */ function takesArrayIteratorOfString(array $arr): void { if (isset($arr["hello"])) { echo $arr["hello"]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testDontEnsureArrayOffsetsExist(): void { Config::getInstance()->ensure_array_string_offsets_exist = false; $this->addFile( 'somefile.php', ' $arr */ function takesArrayIteratorOfString(array $arr): void { echo $arr["hello"]; }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureArrayOffsetsExistWithIssetCheckFollowedByIsArray(): void { Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', ' $s */ function foo(array $s) : void { if (isset($s["a"]) && \is_array($s["a"])) {} }', ); $this->analyzeFile('somefile.php', new Context()); } public function testComplainAfterFirstIsset(): void { $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testEnsureArrayIntOffsetsExist(): void { $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $arr */ function takesArrayIteratorOfString(array $arr): void { echo $arr[4]; }', ); $this->analyzeFile('somefile.php', new Context()); } public function testNoIssueWhenUsingArrayValuesOnNonEmptyArray(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testNoIssueWhenUsingArrayValuesOnNonEmptyArrayCheckedWithSizeof(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testNoIssueAfterManyIssets(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testEnsureListOffsetExistsNotEmpty(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $arr */ function takesList(array $arr) : void { if ($arr) { echo $arr[0]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureListOffsetExistsAfterArrayPop(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); $this->addFile( 'somefile.php', ' $arr */ function takesList(array $arr) : void { if ($arr) { echo $arr[0]; array_pop($arr); echo $arr[0]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureOffsetExistsAfterArrayPush(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testEnsureOffsetExistsAfterNestedIsset(): void { Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', ' $value */ function test(array $value): int { return isset($value["a"]->foo) ? $value["a"]->foo : 0; }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureListOffsetExistsAfterCountValueInRange(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $arr */ function takesList(array $arr) : void { if (count($arr) >= 3) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } if (count($arr) > 2) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } if (count($arr) === 3) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } if (3 === count($arr)) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } if (3 <= count($arr)) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } if (2 < count($arr)) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testCountOnKeyedArrayInRange(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $list */ function bar(array $list) : void { if (rand(0, 1)) { $list = ["a"]; } if (count($list) > 1) { echo $list[1]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testCountOnKeyedArrayInRangeWithUpdate(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $list */ function bar(array $list) : void { if (rand(0, 1)) { $list = ["a"]; } if (count($list) > 1) { if ($list[1][0] === "a") { $list[1] = "foo"; } echo $list[1]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testCountOnKeyedArrayOutOfRange(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); $this->addFile( 'somefile.php', ' $list */ function bar(array $list) : void { if (rand(0, 1)) { $list = ["a"]; } if (count($list) > 1) { echo $list[2]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureListOffsetExistsAfterCountValueOutOfRange(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); $this->addFile( 'somefile.php', ' $arr */ function takesList(array $arr) : void { if (count($arr) >= 2) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testEnsureListOffsetExistsAfterCountValueOutOfRangeSmallerThan(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); $this->addFile( 'somefile.php', ' $arr */ function takesList(array $arr) : void { if (2 <= count($arr)) { echo $arr[0]; echo $arr[1]; echo $arr[2]; } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testDontWorryWhenUnionedWithPositiveInt(): void { Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', ' $a * @param 0|positive-int $b */ function foo(array $a, int $b): void { echo $a[$b]; }', ); $this->analyzeFile('somefile.php', new Context()); } public function providerValidCodeParse(): iterable { return [ 'testBuildList' => [ 'code' => ' [ '$pre===' => 'list{0?: 0|1, 1?: 1}', '$a===' => 'list{0: 0|1|2, 1?: 1|2, 2?: 2}', ], ], 'testBuildListOther' => [ 'code' => ' [ '$list===' => "list{0: 'A'|'B'|'C', 1?: 'C'}", ], ], 'testBuildList3' => [ 'code' => ' [ '$a===' => "list{0: 0, 1: 1|2|3, 2?: 2|3, 3?: 3}", ], ], 'instanceOfStringOffset' => [ 'code' => 'fooFoo(); } }', ], 'instanceOfIntOffset' => [ 'code' => 'fooFoo(); } }', ], 'notEmptyStringOffset' => [ 'code' => ' $a */ function bar (array $a): string { if ($a["bat"]) { return $a["bat"]; } return "blah"; }', ], 'issetPropertyStringOffset' => [ 'code' => ' */ public $arr = []; } $a = new A(); if (!isset($a->arr["bat"]) || strlen($a->arr["bat"])) { }', ], 'issetPropertyStringOffsetUndefinedClass' => [ 'code' => 'arr["bat"]) || strlen($a->arr["bat"])) { }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedArrayAccess'], ], 'notEmptyIntOffset' => [ 'code' => ' $a */ function bar (array $a): string { if ($a[0]) { return $a[0]; } return "blah"; }', ], 'ignorePossiblyNullArrayAccess' => [ 'code' => ' [], 'ignored_issues' => ['PossiblyNullArrayAccess'], ], 'ignoreEmptyArrayAccess' => [ 'code' => ' [ '$x' => 'mixed', ], 'ignored_issues' => ['EmptyArrayAccess', 'MixedAssignment'], ], 'objectLikeWithoutKeys' => [ 'code' => ' [ 'code' => ' "01", "02" => "02"]; foreach ($array as $key => $value) { $len = strlen($key); }', ], 'listAssignmentKeyOffset' => [ 'code' => ' [ 'code' => ' [ 'code' => ' "value"]; unset($x1["a"]); $x2 = ["a" => "value", "b" => "value"]; unset($x2["a"]); $x3 = ["a" => "value", "b" => "value"]; $k = "a"; unset($x3[$k]);', 'assertions' => [ '$x1===' => 'array', '$x2===' => "array{b: 'value'}", '$x3===' => "array{b: 'value'}", ], ], 'domNodeListAccessible' => [ 'code' => 'loadXML(""); $e = $doc->getElementsByTagName("node")[0];', 'assertions' => [ '$e' => 'DOMElement|null', ], ], 'getOnArrayAcccess' => [ 'code' => ' $a */ function foo(ArrayAccess $a) : string { return $a[0]; }', ], 'mixedKeyMixedOffset' => [ 'code' => ' [], 'ignored_issues' => ['MixedArgument', 'MixedArrayOffset', 'MissingParamType'], ], 'suppressPossiblyUndefinedStringArrayOffet' => [ 'code' => ' $elt] = $entry; strlen($elt); strlen($entry["a"]);', 'assertions' => [], 'ignored_issues' => ['PossiblyUndefinedArrayOffset'], ], 'noRedundantConditionOnMixedArrayAccess' => [ 'code' => ' */ $b = []; /** @var array */ $c = []; /** @var array */ $d = []; if (!empty($d[0]) && !isset($c[$d[0]])) { if (isset($b[$d[0]])) {} }', 'assertions' => [], 'ignored_issues' => ['MixedArrayOffset'], ], 'noEmptyArrayAccessInLoop' => [ 'code' => ' [ 'code' => ' */ public $arr = []; } /** @var array */ $as = []; if (!$as || !$as[0] instanceof B || !$as[0]->arr ) { return null; } $b = $as[0]->arr;', ], 'arrayAccessAfterPassByref' => [ 'code' => ' [ 'code' => ' $_) {} if ($i === "hello") {} if ($i !== "hello") {} if ($i === 5) {} if ($i !== 5) {} if (is_string($i)) {} if (is_int($i)) {} foreach ($arr as $i => $_) {} if ($i === "hell") { $i = "hellp"; } if ($i === "hel") {} }', ], 'arrayKeyChecksAfterDefinition' => [ 'code' => ' $_) {} if ($i === "hell") { $i = "hellp"; } if ($i === "hel") {} }', ], 'allowMixedTypeCoercionArrayKeyAccess' => [ 'code' => ' $i * @param array $arr */ function foo(array $i, array $arr) : void { foreach ($i as $j => $k) { echo $arr[$j]; } }', 'assertions' => [], 'ignored_issues' => ['MixedArrayTypeCoercion'], ], 'allowNegativeStringOffset' => [ 'code' => ' [ 'code' => ' 1, "b" => [ "c" => "a", ] ); if (rand(0, 1)) { $params = getArray(); } echo $params["b"]["c"];', 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess', 'MixedArgument'], ], 'arrayAccessOnObjectWithNullGet' => [ 'code' => ' */ protected $data = []; /** * @param array $array * @psalm-suppress MixedArgumentTypeCoercion */ final public function __construct(array $array) { foreach ($array as $key => $value) { if (is_array($value)) { $this->data[$key] = new static($value); } else { $this->data[$key] = $value; } } } /** * @param string $name * @return C|scalar */ public function offsetGet($name) { return $this->data[$name]; } /** * @param ?string $name * @param scalar|array $value * @psalm-suppress MixedArgumentTypeCoercion */ public function offsetSet($name, $value) : void { if (is_array($value)) { $value = new static($value); } if (null === $name) { $this->data[] = $value; } else { $this->data[$name] = $value; } } public function __isset(string $name) : bool { return isset($this->data[$name]); } public function __unset(string $name) : void { unset($this->data[$name]); } /** * @psalm-suppress MixedArgument */ public function offsetExists($offset) : bool { return $this->__isset($offset); } /** * @psalm-suppress MixedArgument */ public function offsetUnset($offset) : void { $this->__unset($offset); } }', 'assertions' => [ '$c' => 'C|null|scalar', ], ], 'singleLetterOffset' => [ 'code' => ' "str"]["str"[0]];', ], 'arrayAccessAfterByRefArrayOffsetAssignment' => [ 'code' => ' &$ar]); $value = "foo"; if (isset($ar[$value])) { echo (string) $ar[$value]; }', 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess'], ], 'byRefArrayAccessWithoutKnownVarNoNotice' => [ 'code' => 'foo->bar]);', ], 'accessOffsetOnList' => [ 'code' => ' $arr */ function foo(array $arr) : void { echo $arr[3] ?? null; }', ], 'destructureMixed' => [ 'code' => 'a) { return; } $popped = array_pop($this->a); /** @psalm-suppress MixedArrayAccess */ [$this->b, $this->c] = $popped; } }', ], 'simpleXmlArrayFetch' => [ 'code' => ' [ 'code' => 'children(); assert($children !== null); foreach ($children as $img) { yield $img["src"] ?? ""; } }', ], 'assertOnArrayAccess' => [ 'code' => 'data[$name]; } /** * @param string $name * @param mixed $value */ public function offsetSet($name, $value) : void { $this->data[$name] = $value; } public function __isset(string $name) : bool { return isset($this->data[$name]); } public function __unset(string $name) : void { unset($this->data[$name]); } /** * @psalm-suppress MixedArgument */ public function offsetExists($offset) : bool { return $this->__isset($offset); } /** * @psalm-suppress MixedArgument */ public function offsetUnset($offset) : void { $this->__unset($offset); } } $container = new C(); if ($container["a"] instanceof A) { $container["a"]->foo(); }', ], 'assignmentListCheckForNull' => [ 'code' => ' $foo] = bar(0); if ($foo !== null) {}', ], 'SKIPPED-accessKnownArrayWithPositiveInt' => [ 'code' => ' $arr */ function foo(array $arr) : void { $o = [4, 15, 18, 21, 51]; $i = 0; foreach ($arr as $a) { if ($o[$i] === $a) {} $i++; } }', ], 'arrayAccessOnArraylikeObjectOrArray' => [ 'code' => '|array $arr */ function test($arr): string { return $arr[0]; } test(["a", "b"]); test(new ArrayObject(["a", "b"]));', ], 'nullCoalesceArrayAccess' => [ 'code' => ' $a */ function foo(?ArrayAccess $a) : void { echo $a[0] ?? "default"; }', ], 'allowUnsettingNested' => [ 'code' => ' $test]; unset($a[$test->value]);', ], 'arrayAssertionShouldNotBeNull' => [ 'code' => ' [ 'code' => ' 5]; $_arr2 = []; /** @psalm-suppress InvalidArrayOffset */ $_arr2[$index] = 5;', 'assertions' => [ '$_arr1===' => 'non-empty-array<1, 5>', '$_arr2===' => 'array{1: 5}', ], ], 'accessArrayWithSingleStringLiteralOffset' => [ 'code' => ' $p */ function f($p): int { return $p["name"]; }', ], 'unsetListKeyedArrayDisableListFlag' => [ 'code' => ' ['$a===' => "array{1: 'b'}"], ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'invalidArrayAccess' => [ 'code' => ' 'InvalidArrayAccess', ], 'invalidArrayOffset' => [ 'code' => ' 'InvalidArrayOffset', ], 'possiblyInvalidArrayOffsetWithInt' => [ 'code' => ' 2 ? ["a" => 5] : "hello"; $y = $x[0];', 'error_message' => 'PossiblyInvalidArrayOffset', ], 'possiblyInvalidArrayOffsetWithString' => [ 'code' => ' 2 ? ["a" => 5] : "hello"; $y = $x["a"];', 'error_message' => 'PossiblyInvalidArrayOffset', ], 'possiblyInvalidArrayAccessWithNestedArray' => [ 'code' => '>|string */ function return_array() { return rand() % 5 > 3 ? [["key" => 3.5]] : "key:3.5"; } $result = return_array(); $v = $result[0]["key"];', 'error_message' => 'PossiblyInvalidArrayOffset', ], 'possiblyInvalidArrayAccess' => [ 'code' => ' 5 ? 5 : ["hello"]; echo $a[0];', 'error_message' => 'PossiblyInvalidArrayAccess', ], 'mixedArrayAccess' => [ 'code' => ' 'MixedArrayAccess', 'ignored_issues' => ['MixedAssignment'], ], 'mixedArrayOffset' => [ 'code' => ' 'MixedArrayOffset', 'ignored_issues' => ['MixedAssignment'], ], 'nullArrayAccess' => [ 'code' => ' 'NullArrayAccess', ], 'possiblyNullArrayAccess' => [ 'code' => ' 'PossiblyNullArrayAccess', ], 'specificErrorMessage' => [ 'code' => ' "value"]; echo $params["fieldName"];', 'error_message' => 'InvalidArrayOffset - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:26 - Cannot access ' . 'value on variable $params using offset value of', ], 'missingArrayOffsetAfterUnset' => [ 'code' => ' "value", "b" => "value"]; unset($x["a"]); echo $x["a"];', 'error_message' => 'InvalidArrayOffset', ], 'noImpossibleStringAccess' => [ 'code' => ' 'InvalidArrayOffset', ], 'mixedKeyStdClassOffset' => [ 'code' => ' 'InvalidArrayOffset', ], 'toStringOffset' => [ 'code' => ' "bar"]; echo $a[new Foo];', 'error_message' => 'InvalidArrayOffset', ], 'possiblyUndefinedIntArrayOffet' => [ 'code' => ' 'PossiblyUndefinedArrayOffset', ], 'possiblyUndefinedStringArrayOffet' => [ 'code' => ' $elt] = $entry;', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'possiblyInvalidMixedArrayOffset' => [ 'code' => ' 'PossiblyInvalidArrayOffset', ], 'arrayAccessOnIterable' => [ 'code' => ' 'InvalidArrayAccess', ], 'arrayKeyCannotBeBool' => [ 'code' => ' $_) {} if ($i === false) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'arrayKeyCannotBeFloat' => [ 'code' => ' $_) {} if ($i === 4.0) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'arrayKeyCannotBeObject' => [ 'code' => ' $_) {} if ($i === new stdClass) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'forbidNegativeStringOffsetOutOfRange' => [ 'code' => ' 'InvalidArrayOffset', ], 'emptyStringAccess' => [ 'code' => ' 'InvalidArrayOffset', ], 'recogniseBadVar' => [ 'code' => ' 'UndefinedGlobalVariable', ], 'unsetListElementShouldChangeToArray' => [ 'code' => ' $arr * @return list */ function takesList(array $arr) : array { unset($arr[0]); return $arr; }', 'error_message' => 'LessSpecificReturnStatement', ], 'simpleXmlArrayFetchResultCannotEqualString' => [ 'code' => ' 'TypeDoesNotContainType', ], 'undefinedTKeyedArrayOffset' => [ 'code' => ' 'InvalidArrayOffset', ], 'destructureNullable' => [ 'code' => ' 1]; } ["key" => $a] = maybeReturnArray();', 'error_message' => 'PossiblyNullArrayAccess', ], 'destructureTuple' => [ 'code' => ' 'InvalidArrayOffset', ], 'negativeListAccess' => [ 'code' => ' 'InvalidArrayOffset', ], 'arrayOpenByDefault' => [ 'code' => ' $arr */ function takesArrayOfFloats(array $arr): void { foreach ($arr as $a) { echo $a; } } avg(["a" => 0.5, "b" => 1.5, "c" => new Exception()]);', 'error_message' => 'InvalidArgument', ], ]; } }