From 1746506b4329d8f1455d54a61a5e13ed6bc044eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:05:53 +0000 Subject: [PATCH] Fix phpstan/phpstan#7170: Cannot set optional array element in generic class - Override setOffsetValueType, setExistingOffsetValueType, and unsetOffset in TemplateConstantArrayType to preserve the template wrapper when the result is compatible with the bound - The root cause was that ConstantArrayType::setOffsetValueType returned a plain ConstantArrayType, losing the template context, which then failed the template parameter acceptance check - New regression test in tests/PHPStan/Rules/Properties/data/bug-7170.php --- .../Generic/TemplateConstantArrayType.php | 33 ++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 5 +++ .../Rules/Properties/data/bug-7170.php | 44 +++++++++++++++++++ 3 files changed, 82 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 fbc13bad6d0..9cdae5ec6b6 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -35,4 +35,37 @@ public function __construct( $this->default = $default; } + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $result = parent::setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($this->getBound()->isSuperTypeOf($result)->yes()) { + return $this; + } + + return $result; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + $result = parent::setExistingOffsetValueType($offsetType, $valueType); + + if ($this->getBound()->isSuperTypeOf($result)->yes()) { + return $this; + } + + return $result; + } + + public function unsetOffset(Type $offsetType, bool $preserveListCertainty = false): Type + { + $result = parent::unsetOffset($offsetType, $preserveListCertainty); + + if ($this->getBound()->isSuperTypeOf($result)->yes()) { + return $this; + } + + return $result; + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 88b2bb4a2c6..f9ec8d6a4bf 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1055,4 +1055,9 @@ public function testBug10924(): void $this->analyse([__DIR__ . '/../Methods/data/bug-10924.php'], []); } + 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 00000000000..1953273a595 --- /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'] = []; + } + } +}