expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testEnsureArrayOffsetsExistWithIssetCheck() { \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testDontEnsureArrayOffsetsExist() { \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testEnsureArrayOffsetsExistWithIssetCheckFollowedByIsArray() { \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testComplainAfterFirstIsset() { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); \Psalm\Config::getInstance()->ensure_array_string_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new \Psalm\Context()); } /** * @return void */ public function testEnsureArrayIntOffsetsExist() { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset'); \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testNoIssueWhenUsingArrayValuesOnNonEmptyArray() { \Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new \Psalm\Context()); } public function testNoIssueAfterManyIssets() : void { \Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new \Psalm\Context()); } /** * @return void */ public function testEnsureListOffsetExistsNotEmpty() { \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testEnsureListOffsetExistsAfterArrayPop() { \Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(\Psalm\Exception\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 \Psalm\Context()); } /** * @return void */ public function testEnsureListOffsetExistsAfterCountValueInRange() { \Psalm\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 \Psalm\Context()); } /** * @return void */ public function testEnsureListOffsetExistsAfterCountValueOutOfRange() { \Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(\Psalm\Exception\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 \Psalm\Context()); } /** * @return void */ public function testEnsureListOffsetExistsAfterCountValueOutOfRangeSmallerThan() { \Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true; $this->expectException(\Psalm\Exception\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 \Psalm\Context()); } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse() { return [ 'instanceOfStringOffset' => [ 'fooFoo(); } }', ], 'instanceOfIntOffset' => [ 'fooFoo(); } }', ], 'notEmptyStringOffset' => [ ' $a */ function bar (array $a): string { if ($a["bat"]) { return $a["bat"]; } return "blah"; }', ], 'issetPropertyStringOffset' => [ ' */ public $arr = []; } $a = new A(); if (!isset($a->arr["bat"]) || strlen($a->arr["bat"])) { }', ], 'issetPropertyStringOffsetUndefinedClass' => [ 'arr["bat"]) || strlen($a->arr["bat"])) { }', 'assertions' => [], 'error_levels' => ['MixedArgument', 'MixedArrayAccess'], ], 'notEmptyIntOffset' => [ ' $a */ function bar (array $a): string { if ($a[0]) { return $a[0]; } return "blah"; }', ], 'ignorePossiblyNullArrayAccess' => [ ' [], 'error_levels' => ['PossiblyNullArrayAccess'], ], 'ignoreEmptyArrayAccess' => [ ' [ '$x' => 'mixed', ], 'error_levels' => ['EmptyArrayAccess', 'MixedAssignment'], ], 'objectLikeWithoutKeys' => [ ' [ ' "01", "02" => "02"]; foreach ($array as $key => $value) { $len = strlen($key); }', ], 'listAssignmentKeyOffset' => [ ' [ ' [ ' "value"]; unset($x["a"]); $x[] = 5; takesInt($x[0]);', ], 'domNodeListAccessible' => [ 'loadXML(""); $e = $doc->getElementsByTagName("node")[0];', [ '$e' => 'DOMElement|null', ], ], 'getOnArrayAcccess' => [ ' $a */ function foo(ArrayAccess $a) : string { return $a[0]; }', ], 'mixedKeyMixedOffset' => [ ' [], 'error_levels' => ['MixedArgument', 'MixedArrayOffset', 'MissingParamType'], ], 'suppressPossiblyUndefinedStringArrayOffet' => [ ' $elt] = $entry; strlen($elt); strlen($entry["a"]);', 'assertions' => [], 'error_levels' => ['PossiblyUndefinedArrayOffset'], ], 'noRedundantConditionOnMixedArrayAccess' => [ ' */ $b = []; /** @var array */ $c = []; /** @var array */ $d = []; if (!empty($d[0]) && !isset($c[$d[0]])) { if (isset($b[$d[0]])) {} }', [], 'error_levels' => ['MixedArrayOffset'], ], 'noEmptyArrayAccessInLoop' => [ ' [ ' */ public $arr = []; } /** @var array */ $as = []; if (!$as || !$as[0] instanceof B || !$as[0]->arr ) { return null; } $b = $as[0]->arr;', ], 'arrayAccessAfterPassByref' => [ ' [ ' $_) {} 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' => [ ' $_) {} if ($i === "hell") { $i = "hellp"; } if ($i === "hel") {} }', ], 'allowMixedTypeCoercionArrayKeyAccess' => [ ' $i * @param array $arr */ function foo(array $i, array $arr) : void { foreach ($i as $j => $k) { echo $arr[$j]; } }', 'assertions' => [], 'error_levels' => ['MixedArrayTypeCoercion'], ], 'allowNegativeStringOffset' => [ ' [ ' 1, "b" => [ "c" => "a", ] ); if (rand(0, 1)) { $params = getArray(); } echo $params["b"]["c"];', [], ['MixedArrayAccess', 'MixedArgument'], ], 'arrayAccessOnObjectWithNullGet' => [ ' */ protected $data = []; /** * @param array $array * @psalm-suppress MixedTypeCoercion */ 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 MixedTypeCoercion */ 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); } } $array = new C([]); $array["key"] = []; /** @psalm-suppress PossiblyInvalidArrayAssignment */ $array["key"][] = "testing";', ], 'singleLetterOffset' => [ ' "str"]["str"[0]];', ], 'assertConstantOffsetsInMethod' => [ ' ["foo" => true], "b" => [] ]; public function bar(string $key): bool { if (!array_key_exists($key, self::ARR) || !array_key_exists("foo", self::ARR[$key])) { return false; } return self::ARR[$key]["foo"]; } }', [], ['MixedReturnStatement', 'MixedInferredReturnType'], ], 'assertSelfClassConstantOffsetsInFunction' => [ ' ["foo" => true], "b" => [] ]; public function bar(?string $key): bool { if ($key === null || !array_key_exists($key, self::ARR) || !array_key_exists("foo", self::ARR[$key])) { return false; } return self::ARR[$key]["foo"]; } }', [], ['MixedReturnStatement', 'MixedInferredReturnType'], ], 'assertNamedClassConstantOffsetsInFunction' => [ ' ["foo" => true], "b" => [], ]; } function bar(?string $key): bool { if ($key === null || !array_key_exists($key, C::ARR) || !array_key_exists("foo", C::ARR[$key])) { return false; } return C::ARR[$key]["foo"]; }', [], ['MixedReturnStatement', 'MixedInferredReturnType'], ], 'arrayAccessAfterByRefArrayOffsetAssignment' => [ ' &$ar]); $value = "foo"; if (isset($ar[$value])) { echo (string) $ar[$value]; }', [], ['MixedArrayAccess'], ], 'byRefArrayAccessWithoutKnownVarNoNotice' => [ 'foo->bar]);', ], 'accessOffsetOnList' => [ ' $arr */ function foo(array $arr) : void { echo $arr[3] ?? null; }', ], ]; } /** * @return iterable */ public function providerInvalidCodeParse() { return [ 'invalidArrayAccess' => [ ' 'InvalidArrayAccess', ], 'invalidArrayOffset' => [ ' 'InvalidArrayOffset', ], 'possiblyInvalidArrayOffsetWithInt' => [ ' 2 ? ["a" => 5] : "hello"; $y = $x[0];', 'error_message' => 'PossiblyInvalidArrayOffset', ], 'possiblyInvalidArrayOffsetWithString' => [ ' 2 ? ["a" => 5] : "hello"; $y = $x["a"];', 'error_message' => 'PossiblyInvalidArrayOffset', ], 'possiblyInvalidArrayAccessWithNestedArray' => [ '>|string * @return 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' => [ ' 5 ? 5 : ["hello"]; echo $a[0];', 'error_message' => 'PossiblyInvalidArrayAccess', ], 'mixedArrayAccess' => [ ' 'MixedArrayAccess', 'error_level' => ['MixedAssignment'], ], 'mixedArrayOffset' => [ ' 'MixedArrayOffset', 'error_level' => ['MixedAssignment'], ], 'nullArrayAccess' => [ ' 'NullArrayAccess', ], 'possiblyNullArrayAccess' => [ ' 'PossiblyNullArrayAccess', ], 'specificErrorMessage' => [ ' "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' => [ ' "value", "b" => "value"]; unset($x["a"]); echo $x["a"];', 'error_message' => 'InvalidArrayOffset', ], 'noImpossibleStringAccess' => [ ' 'InvalidArrayOffset', ], 'mixedKeyStdClassOffset' => [ ' 'InvalidArrayOffset', ], 'toStringOffset' => [ ' "bar"]; echo $a[new Foo];', 'error_message' => 'InvalidArrayOffset', ], 'possiblyUndefinedIntArrayOffet' => [ ' 'PossiblyUndefinedArrayOffset', ], 'possiblyUndefinedStringArrayOffet' => [ ' $elt] = $entry;', 'error_message' => 'PossiblyUndefinedArrayOffset', ], 'possiblyInvalidMixedArrayOffset' => [ ' 'PossiblyInvalidArrayOffset', ], 'arrayAccessOnIterable' => [ ' 'InvalidArrayAccess', ], 'arrayKeyCannotBeBool' => [ ' $_) {} if ($i === false) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'arrayKeyCannotBeFloat' => [ ' $_) {} if ($i === 4.0) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'arrayKeyCannotBeObject' => [ ' $_) {} if ($i === new stdClass) {} }', 'error_message' => 'TypeDoesNotContainType', ], 'forbidNegativeStringOffsetOutOfRange' => [ ' 'InvalidArrayOffset', ], 'emptyStringAccess' => [ ' 'InvalidArrayOffset', ], 'recogniseBadVar' => [ ' 'UndefinedGlobalVariable', ], 'unsetListElementShouldChangeToArray' => [ ' $arr * @return list */ function takesList(array $arr) : array { unset($arr[0]); return $arr; }', 'error_message' => 'LessSpecificReturnStatement', ], ]; } }