Skip to content

Commit 3afcdd8

Browse files
committed
Fix false positive when setting optional array offset on template constant array property
- Override setOffsetValueType() and setExistingOffsetValueType() in TemplateConstantArrayType - When the offset exists in the template's bound and the value type is accepted by the bound's value type, return the template type itself - This preserves the template contract: modifying an in-bound offset on a template value still produces a valid template value - New regression test in tests/PHPStan/Rules/Properties/data/bug-7170.php Closes phpstan/phpstan#7170
1 parent 106fc93 commit 3afcdd8

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/Type/Generic/TemplateConstantArrayType.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,42 @@ public function __construct(
3535
$this->default = $default;
3636
}
3737

38+
public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
39+
{
40+
if ($this->isOffsetWithinBound($offsetType, $valueType)) {
41+
return $this;
42+
}
43+
44+
return parent::setOffsetValueType($offsetType, $valueType, $unionValues);
45+
}
46+
47+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
48+
{
49+
if ($this->isOffsetWithinBound($offsetType, $valueType)) {
50+
return $this;
51+
}
52+
53+
return parent::setExistingOffsetValueType($offsetType, $valueType);
54+
}
55+
56+
private function isOffsetWithinBound(?Type $offsetType, Type $valueType): bool
57+
{
58+
if ($offsetType === null) {
59+
return false;
60+
}
61+
62+
$boundKeyTypes = $this->bound->getKeyTypes();
63+
$boundValueTypes = $this->bound->getValueTypes();
64+
65+
foreach ($boundKeyTypes as $i => $boundKeyType) {
66+
if (!$offsetType->equals($boundKeyType)) {
67+
continue;
68+
}
69+
70+
return $boundValueTypes[$i]->accepts($valueType, true)->yes();
71+
}
72+
73+
return false;
74+
}
75+
3876
}

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,4 +1003,9 @@ public function testCloneWith(): void
10031003
]);
10041004
}
10051005

1006+
public function testBug7170(): void
1007+
{
1008+
$this->analyse([__DIR__ . '/data/bug-7170.php'], []);
1009+
}
1010+
10061011
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7170;
4+
5+
/**
6+
* @template Tdata of array{extension?: array<mixed>}
7+
*/
8+
class Data
9+
{
10+
/**
11+
* @var Tdata
12+
*/
13+
private $data;
14+
15+
/**
16+
* @param Tdata $data
17+
*/
18+
public function __construct(array $data = [])
19+
{
20+
$this->data = $data;
21+
}
22+
23+
public function setExtensionProperty(): void
24+
{
25+
if (!isset($this->data['extension'])) {
26+
$this->data['extension'] = [];
27+
}
28+
}
29+
}
30+
31+
class NonGeneric
32+
{
33+
/**
34+
* @var array{extension?: array<mixed>}
35+
*/
36+
private $data;
37+
38+
public function setData(): void
39+
{
40+
if (!isset($this->data['extension'])) {
41+
$this->data['extension'] = [];
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)