Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/Analyser/Generics/TemplateArgumentSolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ private function removeSelfBounds(Type $type, string $key): Type
/** @var array<string, true> */
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);
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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<itself>, only the bound does
return $template->getBound();
}
$inferred = $template->inferTemplateTypes($lowerBound)->getType($template->getName());
if ($inferred === null) {
continue;
}
Expand Down
86 changes: 86 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15434.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare(strict_types = 1);

namespace Bug15434Types;

use function PHPStan\Testing\assertType;

/**
* @template TOwner of object
*/
class WeakAnalysingMap
{
/**
* @param TOwner $owner
*/
public function set(object $owner): void {}
}

/** @template T */
class Box
{

/** @param T $v */
public function add($v): void
{
}

}

/** @template-contravariant T */
class Sink
{

/** @param T $v */
public function put($v): void
{
}

}

class Test
{

public function testSet(): void
{
$map = new WeakAnalysingMap();
assertType('Bug15434Types\WeakAnalysingMap<object>', $map);
$map->set($map);
assertType('Bug15434Types\WeakAnalysingMap<object>', $map);
}

public function indirectCycle(): void
{
$a = new WeakAnalysingMap();
$b = new WeakAnalysingMap();
$a->set($b);
$b->set($a);
assertType('Bug15434Types\WeakAnalysingMap<Bug15434Types\WeakAnalysingMap<object>>', $a);
assertType('Bug15434Types\WeakAnalysingMap<object>', $b);
}

public function afterAnotherLowerBound(): void
{
$b = new Box();
$b->add(1);
$b->add($b);
assertType('Bug15434Types\Box<mixed>', $b);
}

public function contravariant(): void
{
$s = new Sink();
$s->put($s);
assertType('Bug15434Types\Sink<mixed>', $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<Bug15434Types\Box<1>>', $b);
}

}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
}

}
82 changes: 82 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15434.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php declare(strict_types = 1);

namespace Bug15434;

class Test
{
public function testSet(): void
{
$map = new WeakAnalysingMap();
$map->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<T> $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);
}

}
Loading