Skip to content

Commit 7b29df6

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix phpstan/phpstan#11507: false positive for invariant template with HasOffsetValueType
- When setting an explicit array key on a generic array type (e.g. $item['foo'] = '100'), PHPStan adds HasOffsetValueType to the intersection, making the type structurally unequal to the base array type even though they describe the same type to the user - The invariance check in TemplateTypeVariance::isValidVariance used strict equals(), which failed due to the extra HasOffsetValueType accessory type - Added a fallback that strips HasOffsetValueType/HasOffsetType before comparing, so types differing only in offset tracking are treated as equal for invariance - New regression test in tests/PHPStan/Rules/Functions/data/bug-11507.php
1 parent d8f5be7 commit 7b29df6

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/Type/Generic/TemplateTypeVariance.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
66
use PHPStan\ShouldNotHappenException;
77
use PHPStan\TrinaryLogic;
8+
use PHPStan\Type\Accessory\HasOffsetType;
9+
use PHPStan\Type\Accessory\HasOffsetValueType;
810
use PHPStan\Type\BenevolentUnionType;
911
use PHPStan\Type\IsSuperTypeOfResult;
1012
use PHPStan\Type\MixedType;
1113
use PHPStan\Type\NeverType;
1214
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeTraverser;
1316
use function sprintf;
1417

1518
/**
@@ -177,6 +180,13 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b): I
177180

178181
if ($this->invariant()) {
179182
$result = $a->equals($b);
183+
if (!$result) {
184+
$strippedA = self::stripHasOffsetTypes($a);
185+
$strippedB = self::stripHasOffsetTypes($b);
186+
if ($strippedA !== $a || $strippedB !== $b) {
187+
$result = $strippedA->equals($strippedB);
188+
}
189+
}
180190
$reasons = [];
181191
if (!$result) {
182192
if (
@@ -259,4 +269,15 @@ public function toPhpDocNodeVariance(): string
259269
throw new ShouldNotHappenException();
260270
}
261271

272+
private static function stripHasOffsetTypes(Type $type): Type
273+
{
274+
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
275+
if ($type instanceof HasOffsetValueType || $type instanceof HasOffsetType) {
276+
return new MixedType();
277+
}
278+
279+
return $traverse($type);
280+
});
281+
}
282+
262283
}

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,11 @@ public function testBug12397(): void
411411
$this->analyse([__DIR__ . '/data/bug-12397.php'], []);
412412
}
413413

414+
public function testBug11507(): void
415+
{
416+
$this->checkNullables = true;
417+
$this->checkExplicitMixed = true;
418+
$this->analyse([__DIR__ . '/data/bug-11507.php'], []);
419+
}
420+
414421
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11507;
6+
7+
/**
8+
* @template TValue
9+
*/
10+
class Collection
11+
{
12+
/**
13+
* @param array<int, TValue> $items
14+
*/
15+
public function __construct(
16+
public array $items,
17+
) {}
18+
19+
/**
20+
* Run a map over each of the items.
21+
*
22+
* @template TMapValue
23+
*
24+
* @param callable(TValue, int=): TMapValue $callback
25+
* @return Collection<TMapValue>
26+
*/
27+
public function map(callable $callback): Collection
28+
{
29+
$keys = array_keys($this->items);
30+
31+
$items = array_map($callback, $this->items);
32+
33+
$result = array_combine($keys, $items);
34+
35+
return new self($result);
36+
}
37+
}
38+
39+
/**
40+
* @param Collection<non-empty-array<string>> $collection
41+
* @return Collection<non-empty-array<string>>
42+
*/
43+
function foo(Collection $collection): Collection
44+
{
45+
return $collection->map(function (array $item) {
46+
$item['foo'] = '100';
47+
48+
return $item;
49+
});
50+
}

0 commit comments

Comments
 (0)