diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 10866514d52..8e0b130a21e 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -669,14 +669,12 @@ private static function expandNamedObject( if (!$final && $return_type instanceof TNamedObject) { /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = true; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ + $return_type->is_static_resolved = true; } - } elseif ($return_type->is_static - && (($static_class_type instanceof TNamedObject - && $codebase->classlikes->classExtends( - $static_class_type->value, - $return_type->value - ) - ) || $static_class_type instanceof TTemplateParam) + } elseif ($return_type->is_static && !$return_type->is_static_resolved + && ($static_class_type instanceof TNamedObject + || $static_class_type instanceof TTemplateParam) ) { $return_type_types = $return_type->getIntersectionTypes(); $cloned_static = $static_class_type->setIntersectionTypes([]); @@ -691,7 +689,8 @@ private static function expandNamedObject( $return_type_types[$extra_static_type->getKey()] = clone $extra_static_type; } } - $return_type = $return_type->setIntersectionTypes($return_type_types); + $return_type = $return_type->setIntersectionTypes($return_type_types) + ->setIsStatic(true, true); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { $return_type = clone $return_type; /** @psalm-suppress InaccessibleProperty Acting on a clone */ diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 0323952391f..0a72efcdc9a 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -31,6 +31,11 @@ class TNamedObject extends Atomic */ public $is_static = false; + /** + * @var bool + */ + public $is_static_resolved = false; + /** * Whether or not this type can represent a child of the class named in $value * @var bool @@ -59,13 +64,15 @@ public function __construct( $this->from_docblock = $from_docblock; } - public function setIsStatic(bool $is_static): self + public function setIsStatic(bool $is_static, ?bool $is_static_resolved = null): self { - if ($this->is_static === $is_static) { + $this->is_static_resolved ??= $this->is_static_resolved; + if ($this->is_static === $is_static && $this->is_static_resolved === $is_static_resolved) { return $this; } $cloned = clone $this; $cloned->is_static = $is_static; + $cloned->is_static_resolved = $is_static_resolved; return $cloned; } diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 782660aa593..89fc4524012 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -611,6 +611,116 @@ function intersect(A $a) { return $b; }' ], + 'preventDoubleStaticResolution1' => [ + 'code' => ' + */ + class iter extends ArrayObject { + /** + * @return self + */ + public function stabilize(): self { + return $this; + } + } + + class a { + /** + * @return iter + */ + public function ret(): iter { + return new iter([$this]); + } + } + class b extends a { + } + + $a = new b; + $a = $a->ret(); + $a = $a->stabilize();', + 'assertions' => [ + '$a===' => 'iter' + ] + ], + 'preventDoubleStaticResolution2' => [ + 'code' => ' + */ + class iter extends ArrayObject { + /** + * @return self + */ + public function stabilize(): self { + return $this; + } + } + + interface a { + /** + * @return iter + */ + public function ret(): iter; + } + class b implements a { + public function ret(): iter { + return new iter([$this]); + } + } + + /** @var a */ + $a = new b; + $a = $a->ret(); + $a = $a->stabilize();', + 'assertions' => [ + '$a===' => 'iter' + ] + ], + 'preventDoubleStaticResolution3' => [ + 'code' => ' + */ + class iter extends ArrayObject { + /** + * @return self + */ + public function stabilize(): self { + return $this; + } + } + + interface a { + /** + * @return iter + */ + public function ret(): iter; + } + class b implements a { + public function ret(): iter { + return new iter([$this]); + } + } + + /** @var a */ + $a = new b; + $a = $a->ret(); + $a = $a->stabilize();', + 'assertions' => [ + '$a===' => 'iter' + ] + ], 'allowTraversableImplementationAlongWithIteratorAggregate' => [ 'code' => '