From 89aa00568a92afc14f077a25b42dc18c3676509c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 23 Sep 2025 16:26:08 +0200 Subject: [PATCH] add fixture, keep key --- .../Fixture/yield_provider_iterable.php.inc | 66 +++++++++++ ...rrayTypeLeastCommonDenominatorResolver.php | 105 ++++++++++++++++++ .../TypeManipulator/TypeNormalizer.php | 19 +++- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/yield_provider_iterable.php.inc create mode 100644 rules/Privatization/TypeManipulator/ArrayTypeLeastCommonDenominatorResolver.php diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/yield_provider_iterable.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/yield_provider_iterable.php.inc new file mode 100644 index 00000000000..159e6398e97 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/yield_provider_iterable.php.inc @@ -0,0 +1,66 @@ + [1, 2], + 'two' => ['three'], + ], + ]; + yield [ + [ + 'four' => 'five', + ], + ]; + } +} + +?> +----- +>, mixed>> + */ + public static function provideData() + { + yield [ + [ + 'one' => [1, 2], + 'two' => ['three'], + ], + ]; + yield [ + [ + 'four' => 'five', + ], + ]; + } +} + +?> diff --git a/rules/Privatization/TypeManipulator/ArrayTypeLeastCommonDenominatorResolver.php b/rules/Privatization/TypeManipulator/ArrayTypeLeastCommonDenominatorResolver.php new file mode 100644 index 00000000000..184554d2467 --- /dev/null +++ b/rules/Privatization/TypeManipulator/ArrayTypeLeastCommonDenominatorResolver.php @@ -0,0 +1,105 @@ + preserve shape. + $allConstantArrayTypes = array_reduce($types, fn ($c, $t): bool => $c && $t instanceof ConstantArrayType, true); + if ($allConstantArrayTypes) { + /** @var ConstantArrayType[] $consts */ + $consts = $types; + + // Compare key sets (by stringified key types) + $firstKeys = array_map( + fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), + $consts[0]->getKeyTypes() + ); + foreach ($consts as $c) { + $keys = array_map( + fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), + $c->getKeyTypes() + ); + if ($keys !== $firstKeys) { + $allConstantArrayTypes = false; + break; + } + } + + if ($allConstantArrayTypes) { + $resultKeyTypes = $consts[0]->getKeyTypes(); + $valueColumns = []; + foreach ($consts as $const) { + $valueColumns[] = $const->getValueTypes(); + } + + $resultValueTypes = []; + foreach (array_keys($resultKeyTypes) as $i) { + $col = array_column($valueColumns, $i); + $resultValueTypes[] = $this->sharedArrayStructure(...$col); + } + + return new ConstantArrayType($resultKeyTypes, $resultValueTypes); + } + } + + // Generic ArrayType path: reconcile key type + recurse into item types + /** @var ArrayType[] $types */ + /** @var ArrayType[] $arrayTypes */ + $arrayTypes = $types; + + // Try to keep a compatible key type (intersection; fall back to mixed if impossible) + $firstArrayType = array_shift($arrayTypes); + if (! $firstArrayType instanceof ArrayType) { + return new MixedType(); + } + + $keyType = $firstArrayType->getKeyType(); + foreach ($arrayTypes as $arr) { + $keyType = TypeCombinator::intersect($keyType, $arr->getKeyType()); + } + + if ($keyType instanceof NeverType) { + $keyType = new MixedType(); // incompatible key types + } + + // Recurse on item types; if mixed is returned, that’s our stop depth. + $itemTypes = array_map(fn (ArrayType $arrayType): Type => $arrayType->getItemType(), $types); + $itemType = $this->sharedArrayStructure(...$itemTypes); + + return new ArrayType($keyType, $itemType); + } +} diff --git a/rules/Privatization/TypeManipulator/TypeNormalizer.php b/rules/Privatization/TypeManipulator/TypeNormalizer.php index f46bdfe70ff..a8a3566be63 100644 --- a/rules/Privatization/TypeManipulator/TypeNormalizer.php +++ b/rules/Privatization/TypeManipulator/TypeNormalizer.php @@ -31,7 +31,8 @@ public function __construct( private TypeFactory $typeFactory, - private StaticTypeMapper $staticTypeMapper + private StaticTypeMapper $staticTypeMapper, + private ArrayTypeLeastCommonDenominatorResolver $arrayTypeLeastCommonDenominatorResolver ) { } @@ -107,6 +108,11 @@ public function generalizeConstantTypes(Type $type): Type // too long if (strlen((string) $unionedDocType) > self::MAX_PRINTED_UNION_DOC_LENGHT) { + $alwaysKnownArrayType = $this->narrowToAlwaysKnownArrayType($generalizedUnionType); + if ($alwaysKnownArrayType instanceof ArrayType) { + return $alwaysKnownArrayType; + } + return new MixedType(); } @@ -145,4 +151,15 @@ private function isImplicitNumberedListKeyType(ConstantArrayType $constantArrayT return true; } + + private function narrowToAlwaysKnownArrayType(UnionType $unionType): ?ArrayType + { + // always an array? + if (count($unionType->getArrays()) !== count($unionType->getTypes())) { + return null; + } + + $arrayUniqueKeyType = $this->arrayTypeLeastCommonDenominatorResolver->sharedArrayStructure(...$unionType->getTypes()); + return new ArrayType($arrayUniqueKeyType, new MixedType()); + } }