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
12 changes: 11 additions & 1 deletion src/Analyser/Generics/TemplateArgumentSolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -297,7 +298,16 @@
if (TemplateArgumentStats::$enabled) {
TemplateArgumentStats::increment(count($lowerBounds) > 0 ? 'resolvedWithLowerBounds' : 'resolvedToInitial');
}
return TypeCombinator::union(...$parts);
$resolved = TypeCombinator::union(...$parts);
if ($initial !== null && !$initial instanceof NeverType && !$initial->isSuperTypeOf($resolved)->yes()) {

Check warning on line 302 in src/Analyser/Generics/TemplateArgumentSolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ TemplateArgumentStats::increment(count($lowerBounds) > 0 ? 'resolvedWithLowerBounds' : 'resolvedToInitial'); } $resolved = TypeCombinator::union(...$parts); - if ($initial !== null && !$initial instanceof NeverType && !$initial->isSuperTypeOf($resolved)->yes()) { + if ($initial !== null && !$initial instanceof NeverType && $initial->isSuperTypeOf($resolved)->no()) { // the body puts values into the object it was not created with: what // the observation pass read out of it was the initial type alone, so // a value derived from such a read and put back ($box->set($box->get() + 1))

Check warning on line 302 in src/Analyser/Generics/TemplateArgumentSolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ TemplateArgumentStats::increment(count($lowerBounds) > 0 ? 'resolvedWithLowerBounds' : 'resolvedToInitial'); } $resolved = TypeCombinator::union(...$parts); - if ($initial !== null && !$initial instanceof NeverType && !$initial->isSuperTypeOf($resolved)->yes()) { + if ($initial !== null && !$initial instanceof NeverType && !$resolved->isSuperTypeOf($initial)->yes()) { // the body puts values into the object it was not created with: what // the observation pass read out of it was the initial type alone, so // a value derived from such a read and put back ($box->set($box->get() + 1))
// the body puts values into the object it was not created with: what
// the observation pass read out of it was the initial type alone, so
// a value derived from such a read and put back ($box->set($box->get() + 1))
// is one step of a loop - generalized like a loop's variables
return $resolved->generalize(GeneralizePrecision::lessSpecific());
}

return $resolved;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function testLowerBoundsUnionWithTheInitialUnlessASendWins(): void
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument(new CallableType([], $marker, false), new CallableType([], new StringType(), false)));
$frame = (new TemplateArgumentResolver())->resolve($constraints, null, []);

$this->assertSame("1|2|'a'|null", self::describe($frame->resolve($site, 'T')), 'callable parameters are contravariant and contribute nothing');
$this->assertSame('int|string|null', self::describe($frame->resolve($site, 'T')), 'callable parameters are contravariant and contribute nothing');

[$constraints, $site, $ofMarker] = self::constraintsWithA(new ConstantIntegerType(1));
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument($ofMarker->getTypes()[0], new ConstantStringType('a')));
Expand Down
148 changes: 148 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15430.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15430;

use function PHPStan\Testing\assertType;

/** @template T */
final class Box
{
/** @param T $value */
public function __construct(private mixed $value)
{
}

/** @return T */
public function get(): mixed
{
return $this->value;
}

/** @param T $value */
public function set(mixed $value): void
{
$this->value = $value;
}

/**
* @template U
* @param U $value
* @return self<U>
*/
public static function of(mixed $value): self
{
return new self($value);
}
}

/** @param callable(): void $callback */
function repeat(int $times, callable $callback): void
{
for ($i = 0; $i < $times; $i++) {
$callback();
}
}

/**
* @template U
* @param U $value
* @return Box<U>
*/
function wrap(mixed $value): Box
{
return new Box($value);
}

function test(): void
{
$count = new Box(0);

repeat(5, function () use ($count): void {
$count->set($count->get() + 1);
});

assertType('Bug15430\Box<int>', $count);

if ($count->get() === 5) {
echo 'reached at runtime';
}
}

function straight(): void
{
$count = new Box(0);
$count->set($count->get() + 1);
assertType('Bug15430\Box<int>', $count);
}

function loop(): void
{
$count = new Box(0);
for ($i = 0; $i < 5; $i++) {
$count->set($count->get() + 1);
}
assertType('Bug15430\Box<int>', $count);
}

function arrowFunction(): void
{
$count = new Box(0);
repeat(5, fn () => $count->set($count->get() + 1));
assertType('Bug15430\Box<int>', $count);
}

function staticFactory(): void
{
$count = Box::of(0);
repeat(5, function () use ($count): void {
$count->set($count->get() + 1);
});
assertType('Bug15430\Box<int>', $count);
}

function genericFunction(): void
{
$count = wrap(0);
repeat(5, function () use ($count): void {
$count->set($count->get() + 1);
});
assertType('Bug15430\Box<int>', $count);
}

function concat(): void
{
$s = new Box('a');
repeat(5, function () use ($s): void {
$s->set($s->get() . 'b');
});
assertType('Bug15430\Box<string>', $s);
}

function append(): void
{
$s = new Box([]);
repeat(5, function () use ($s): void {
$a = $s->get();
$a[] = 1;
$s->set($a);
});
assertType('Bug15430\Box<list<int>>', $s);
}

function shape(): void
{
$s = new Box(['a' => 1]);
repeat(5, function () use ($s): void {
$s->set(['a' => $s->get()['a'] + 1]);
});
assertType('Bug15430\Box<non-empty-array<string, int>>', $s);
}

function writtenBackUnchanged(): void
{
$b = new Box(0);
$b->set($b->get());
assertType('Bug15430\Box<0>', $b);
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ function covariantIntoInvariantInferred(): void
$z = collectBoxes($a, $b);
$z->add(3);
assertType('TemplateArgumentMultipleSites\Box<1>', $a);
assertType('TemplateArgumentMultipleSites\Coll<1|2|3>', $z);
assertType('TemplateArgumentMultipleSites\Coll<int>', $z);
}

function invariantIntoCovariant(): void
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4560,6 +4560,15 @@ public function testBug15251(): void
$this->analyse([__DIR__ . '/data/bug-15251.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug15430(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-15430.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug12820(): void
{
Expand Down
49 changes: 49 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15430.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15430Rule;


/** @template T */
final class Box
{
/** @param T $value */
public function __construct(private mixed $value)
{
}

/** @return T */
public function get(): mixed
{
return $this->value;
}

/** @param T $value */
public function set(mixed $value): void
{
$this->value = $value;
}
}

/** @param callable(): void $callback */
function repeat(int $times, callable $callback): void
{
for ($i = 0; $i < $times; $i++) {
$callback();
}
}

function test(): void
{
$count = new Box(0);

repeat(5, function () use ($count): void {
$count->set($count->get() + 1);
});


if ($count->get() === 5) {
echo 'reached at runtime';
}
}
Loading