From 3afcdd8cf1c19287bf8f0bbd751ed11ef8287b38 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:40:02 +0000 Subject: [PATCH 1/2] 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 https://github.com/phpstan/phpstan/issues/7170 --- .../Generic/TemplateConstantArrayType.php | 38 ++++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 5 +++ .../Rules/Properties/data/bug-7170.php | 44 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-7170.php diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index fbc13bad6d..9ea9cee28f 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -35,4 +35,42 @@ public function __construct( $this->default = $default; } + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($this->isOffsetWithinBound($offsetType, $valueType)) { + return $this; + } + + return parent::setOffsetValueType($offsetType, $valueType, $unionValues); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ($this->isOffsetWithinBound($offsetType, $valueType)) { + return $this; + } + + return parent::setExistingOffsetValueType($offsetType, $valueType); + } + + private function isOffsetWithinBound(?Type $offsetType, Type $valueType): bool + { + if ($offsetType === null) { + return false; + } + + $boundKeyTypes = $this->bound->getKeyTypes(); + $boundValueTypes = $this->bound->getValueTypes(); + + foreach ($boundKeyTypes as $i => $boundKeyType) { + if (!$offsetType->equals($boundKeyType)) { + continue; + } + + return $boundValueTypes[$i]->accepts($valueType, true)->yes(); + } + + return false; + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 29c2c32aca..a159d25d46 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1003,4 +1003,9 @@ public function testCloneWith(): void ]); } + public function testBug7170(): void + { + $this->analyse([__DIR__ . '/data/bug-7170.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-7170.php b/tests/PHPStan/Rules/Properties/data/bug-7170.php new file mode 100644 index 0000000000..1d5ff5d05b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7170.php @@ -0,0 +1,44 @@ +} + */ +class Data +{ + /** + * @var Tdata + */ + private $data; + + /** + * @param Tdata $data + */ + public function __construct(array $data = []) + { + $this->data = $data; + } + + public function setExtensionProperty(): void + { + if (!isset($this->data['extension'])) { + $this->data['extension'] = []; + } + } +} + +class NonGeneric +{ + /** + * @var array{extension?: array} + */ + private $data; + + public function setData(): void + { + if (!isset($this->data['extension'])) { + $this->data['extension'] = []; + } + } +} From a14e9628d3b12a5c4301a1759dda302c6d89f280 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 23:47:08 +0000 Subject: [PATCH 2/2] Add regression test for #10172 Closes https://github.com/phpstan/phpstan/issues/10172 --- .../Rules/Methods/ReturnTypeRuleTest.php | 5 +++++ .../PHPStan/Rules/Methods/data/bug-10172.php | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-10172.php diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 84e81f0f33..40f4e46571 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1310,4 +1310,9 @@ public function testBug9669(): void $this->analyse([__DIR__ . '/data/bug-9669.php'], []); } + public function testBug10172(): void + { + $this->analyse([__DIR__ . '/data/bug-10172.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-10172.php b/tests/PHPStan/Rules/Methods/data/bug-10172.php new file mode 100644 index 0000000000..434ac1ae39 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10172.php @@ -0,0 +1,21 @@ +} + * + * @param T $a + * + * @return T + */ + public function foo(array $a): array + { + foreach ($a['data'] as $i) { + } + + return $a; + } +}