diff --git a/src/Analyser/Generics/TemplateArgumentSolver.php b/src/Analyser/Generics/TemplateArgumentSolver.php index 25fcb53900b..df66ea705cb 100644 --- a/src/Analyser/Generics/TemplateArgumentSolver.php +++ b/src/Analyser/Generics/TemplateArgumentSolver.php @@ -134,6 +134,9 @@ private function removeSelfBounds(Type $type, string $key): Type /** @var array */ private array $resolving = []; + /** How many times a substitution reached a site that was still being resolved. */ + private int $cyclesReached = 0; + private function resolveKey(string $key): Type { $key = $this->representative($key); @@ -144,14 +147,18 @@ private function resolveKey(string $key): Type if (isset($this->resolving[$key])) { // a site whose inferred argument refers back to itself through another // site (wrap($x = new Foo($x))): the inferred type stands + $this->cyclesReached++; return $observation['marker']->getDelegate(); } $this->resolving[$key] = true; + $cyclesReached = $this->cyclesReached; try { return $this->resolutions[$key] = $this->resolveObservation($observation); } finally { unset($this->resolving[$key]); + // a cycle closed inside another site's resolution was settled there + $this->cyclesReached = $cyclesReached; } } @@ -208,7 +215,14 @@ private function resolveObservation(array $observation): Type $template = $observation['marker']->getTemplate(); $lowerBounds = []; foreach ($observation['lowerBounds'] as $lowerBound) { - $inferred = $template->inferTemplateTypes($this->substituteResolutions($lowerBound))->getType($template->getName()); + $cyclesReached = $this->cyclesReached; + $lowerBound = $this->substituteResolutions($lowerBound); + if ($this->cyclesReached !== $cyclesReached) { + // the object is sent into its own argument ($map->set($map)): no + // finite type contains Foo, only the bound does + return $template->getBound(); + } + $inferred = $template->inferTemplateTypes($lowerBound)->getType($template->getName()); if ($inferred === null) { continue; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15434.php b/tests/PHPStan/Analyser/nsrt/bug-15434.php new file mode 100644 index 00000000000..330724c195b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15434.php @@ -0,0 +1,86 @@ +', $map); + $map->set($map); + assertType('Bug15434Types\WeakAnalysingMap', $map); + } + + public function indirectCycle(): void + { + $a = new WeakAnalysingMap(); + $b = new WeakAnalysingMap(); + $a->set($b); + $b->set($a); + assertType('Bug15434Types\WeakAnalysingMap>', $a); + assertType('Bug15434Types\WeakAnalysingMap', $b); + } + + public function afterAnotherLowerBound(): void + { + $b = new Box(); + $b->add(1); + $b->add($b); + assertType('Bug15434Types\Box', $b); + } + + public function contravariant(): void + { + $s = new Sink(); + $s->put($s); + assertType('Bug15434Types\Sink', $s); + } + + public function notACycle(): void + { + $b = new Box(); + $c = new Box(); + $b->add($c); + $c->add(1); + assertType('Bug15434Types\Box<1>', $c); + assertType('Bug15434Types\Box>', $b); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8e6c4084026..f754af637bb 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4616,4 +4616,12 @@ public function testDeadCatch(): void ]); } + public function testBug15434(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-15434.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-15434.php b/tests/PHPStan/Rules/Methods/data/bug-15434.php new file mode 100644 index 00000000000..11752e1643b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15434.php @@ -0,0 +1,82 @@ +set($map); + } +} + +/** + * @template TOwner of object + */ +class WeakAnalysingMap +{ + /** + * @param TOwner $owner + */ + public function set(object $owner): void {} +} + +/** @template T */ +class Box +{ + + /** @param T $v */ + public function __construct($v = null) + { + } + + /** @param T $v */ + public function add($v): void + { + } + + /** @param list $v */ + public function addAll(array $v): void + { + } + +} + +class Analogous +{ + + public function indirectCycle(): void + { + $a = new WeakAnalysingMap(); + $b = new WeakAnalysingMap(); + $a->set($b); + $b->set($a); + } + + public function afterAnotherLowerBound(): void + { + $b = new Box(); + $b->add(1); + $b->add($b); + } + + public function wrappedInList(): void + { + $b = new Box(); + $b->addAll([$b]); + } + + public function wrappedInArray(): void + { + $b = new Box(); + $b->add([$b]); + } + + public function withInitialType(): void + { + $b = new Box(1); + $b->add($b); + } + +}