diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index aab2847ebd1..c354b4e05a4 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -552,7 +552,9 @@ private static function getValueForKey( return Type::getMixed($inside_loop); } elseif ($existing_key_type_part instanceof TString) { $new_base_type_candidate = Type::getString(); - } elseif ($existing_key_type_part instanceof Type\Atomic\TNamedObject) { + } elseif ($existing_key_type_part instanceof Type\Atomic\TNamedObject + && ($has_isset || $has_inverted_isset) + ) { $has_object_array_access = true; return null; } elseif (!$existing_key_type_part instanceof Type\Atomic\ObjectLike) { diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 9dcbb9bc669..1511bea8087 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -906,6 +906,69 @@ function foo(SimpleXMLElement $s) : SimpleXMLElement { return $s["a"]; }', ], + 'assertOnArrayAccess' => [ + '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(); + }' + ], ]; }