From 886d9c0283155adc231e67a6b57ec955dd77f6c0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 2 Sep 2026 16:17:12 +0200 Subject: [PATCH] [TypeDeclarationDocblocks] Guard DocblockVarArrayFromPropertyDefaultsRector against unsound narrowing Two cases produced a @var narrower than the values actually stored: - non-private property: a child class can reassign it with a wider type that is invisible from the declaring class, so restrict to private - empty nested array default (e.g. ['create' => [], 'update' => []]): generalizes to never[], which then rejects every real value assigned later --- .../array_union_generic_same_type.php.inc | 4 +-- .../Fixture/skip_empty_nested_array.php.inc | 13 +++++++ .../Fixture/skip_non_private_property.php.inc | 8 +++++ ...lockVarArrayFromPropertyDefaultsRector.php | 34 +++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_empty_nested_array.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_non_private_property.php.inc diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/array_union_generic_same_type.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/array_union_generic_same_type.php.inc index 6eb037a11d9..5167555baf3 100644 --- a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/array_union_generic_same_type.php.inc +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/array_union_generic_same_type.php.inc @@ -7,7 +7,7 @@ use stdClass; class ArrayUnionGenericSameType { - protected array $dependencies = [ + private array $dependencies = [ stdClass::class, DateTime::class, ]; @@ -27,7 +27,7 @@ class ArrayUnionGenericSameType /** * @var array|class-string<\DateTime>> */ - protected array $dependencies = [ + private array $dependencies = [ stdClass::class, DateTime::class, ]; diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_empty_nested_array.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_empty_nested_array.php.inc new file mode 100644 index 00000000000..6b537f9aed7 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_empty_nested_array.php.inc @@ -0,0 +1,13 @@ + [], 'update' => []]; + + public function add(string $key, array $item): void + { + $this->payload['create'][$key] = $item; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_non_private_property.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_non_private_property.php.inc new file mode 100644 index 00000000000..e862aea94cc --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector/Fixture/skip_non_private_property.php.inc @@ -0,0 +1,8 @@ + true, 'name' => 'value']; +} diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector.php index 88abd66c32a..d03661f790f 100644 --- a/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector.php +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarArrayFromPropertyDefaultsRector.php @@ -6,6 +6,7 @@ use PhpParser\Comment\Doc; use PhpParser\Node; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Identifier; use PhpParser\Node\PropertyItem; @@ -80,6 +81,11 @@ public function refactor(Node $node): ?Node continue; } + // only private properties are safe; a protected/public one can be reassigned with a wider type by a child class we cannot see here + if (! $property->isPrivate()) { + continue; + } + if (count($property->props) > 1) { continue; } @@ -89,6 +95,11 @@ public function refactor(Node $node): ?Node continue; } + // an empty nested array default generalizes to never[], which then rejects every real value assigned later + if ($this->hasEmptyNestedArray($soleProperty->default)) { + continue; + } + $propertyDefaultType = $this->getType($soleProperty->default); $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); @@ -118,6 +129,29 @@ public function refactor(Node $node): ?Node return $node; } + private function hasEmptyNestedArray(Array_ $array): bool + { + foreach ($array->items as $arrayItem) { + if (! $arrayItem instanceof ArrayItem) { + continue; + } + + if (! $arrayItem->value instanceof Array_) { + continue; + } + + if ($arrayItem->value->items === []) { + return true; + } + + if ($this->hasEmptyNestedArray($arrayItem->value)) { + return true; + } + } + + return false; + } + private function hasUsefulParentPropertyVarTag(Class_ $class, Property $property, Type $propertyDefaultType): bool { $propertyName = $this->getName($property);