Skip to content

Commit 89aa005

Browse files
committed
add fixture, keep key
1 parent d4b8a03 commit 89aa005

3 files changed

Lines changed: 189 additions & 1 deletion

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Class_\AddReturnDocblockDataProviderRector\Fixture;
4+
5+
use PHPUnit\Framework\Attributes\DataProvider;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class YieldProviderIterable extends TestCase
9+
{
10+
#[DataProvider('provideData')]
11+
public function testSomething()
12+
{
13+
}
14+
15+
public static function provideData()
16+
{
17+
yield [
18+
[
19+
'one' => [1, 2],
20+
'two' => ['three'],
21+
],
22+
];
23+
yield [
24+
[
25+
'four' => 'five',
26+
],
27+
];
28+
}
29+
}
30+
31+
?>
32+
-----
33+
<?php
34+
35+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Class_\AddReturnDocblockDataProviderRector\Fixture;
36+
37+
use PHPUnit\Framework\Attributes\DataProvider;
38+
use PHPUnit\Framework\TestCase;
39+
40+
final class YieldProviderIterable extends TestCase
41+
{
42+
#[DataProvider('provideData')]
43+
public function testSomething()
44+
{
45+
}
46+
47+
/**
48+
* @return \Iterator<array<array<int, array<string, mixed>>, mixed>>
49+
*/
50+
public static function provideData()
51+
{
52+
yield [
53+
[
54+
'one' => [1, 2],
55+
'two' => ['three'],
56+
],
57+
];
58+
yield [
59+
[
60+
'four' => 'five',
61+
],
62+
];
63+
}
64+
}
65+
66+
?>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Privatization\TypeManipulator;
6+
7+
use PHPStan\Type\ArrayType;
8+
use PHPStan\Type\Constant\ConstantArrayType;
9+
use PHPStan\Type\MixedType;
10+
use PHPStan\Type\NeverType;
11+
use PHPStan\Type\Type;
12+
use PHPStan\Type\TypeCombinator;
13+
use PHPStan\Type\VerbosityLevel;
14+
15+
/**
16+
* Made with GPT-5
17+
* @see https://chatgpt.com/share/68d2c183-7708-800a-848b-c63822c4625a
18+
*/
19+
final class ArrayTypeLeastCommonDenominatorResolver
20+
{
21+
/**
22+
* Return the deepest common "array structure" shared by all $types.
23+
* - Keeps exact keys when all are ConstantArrayType with the same keys
24+
* - Keeps generic key type (int|string) when consistent
25+
* - Falls back to mixed at the first conflicting depth
26+
*/
27+
public function sharedArrayStructure(Type ...$types): Type
28+
{
29+
if ($types === []) {
30+
return new MixedType();
31+
}
32+
33+
// If any is not an ArrayType, we cannot descend further.
34+
foreach ($types as $type) {
35+
if (! $type instanceof ArrayType) {
36+
return new MixedType();
37+
}
38+
}
39+
40+
// If all are ConstantArrayType and have the *same* ordered key list -> preserve shape.
41+
$allConstantArrayTypes = array_reduce($types, fn ($c, $t): bool => $c && $t instanceof ConstantArrayType, true);
42+
if ($allConstantArrayTypes) {
43+
/** @var ConstantArrayType[] $consts */
44+
$consts = $types;
45+
46+
// Compare key sets (by stringified key types)
47+
$firstKeys = array_map(
48+
fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()),
49+
$consts[0]->getKeyTypes()
50+
);
51+
foreach ($consts as $c) {
52+
$keys = array_map(
53+
fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()),
54+
$c->getKeyTypes()
55+
);
56+
if ($keys !== $firstKeys) {
57+
$allConstantArrayTypes = false;
58+
break;
59+
}
60+
}
61+
62+
if ($allConstantArrayTypes) {
63+
$resultKeyTypes = $consts[0]->getKeyTypes();
64+
$valueColumns = [];
65+
foreach ($consts as $const) {
66+
$valueColumns[] = $const->getValueTypes();
67+
}
68+
69+
$resultValueTypes = [];
70+
foreach (array_keys($resultKeyTypes) as $i) {
71+
$col = array_column($valueColumns, $i);
72+
$resultValueTypes[] = $this->sharedArrayStructure(...$col);
73+
}
74+
75+
return new ConstantArrayType($resultKeyTypes, $resultValueTypes);
76+
}
77+
}
78+
79+
// Generic ArrayType path: reconcile key type + recurse into item types
80+
/** @var ArrayType[] $types */
81+
/** @var ArrayType[] $arrayTypes */
82+
$arrayTypes = $types;
83+
84+
// Try to keep a compatible key type (intersection; fall back to mixed if impossible)
85+
$firstArrayType = array_shift($arrayTypes);
86+
if (! $firstArrayType instanceof ArrayType) {
87+
return new MixedType();
88+
}
89+
90+
$keyType = $firstArrayType->getKeyType();
91+
foreach ($arrayTypes as $arr) {
92+
$keyType = TypeCombinator::intersect($keyType, $arr->getKeyType());
93+
}
94+
95+
if ($keyType instanceof NeverType) {
96+
$keyType = new MixedType(); // incompatible key types
97+
}
98+
99+
// Recurse on item types; if mixed is returned, that’s our stop depth.
100+
$itemTypes = array_map(fn (ArrayType $arrayType): Type => $arrayType->getItemType(), $types);
101+
$itemType = $this->sharedArrayStructure(...$itemTypes);
102+
103+
return new ArrayType($keyType, $itemType);
104+
}
105+
}

rules/Privatization/TypeManipulator/TypeNormalizer.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131

3232
public function __construct(
3333
private TypeFactory $typeFactory,
34-
private StaticTypeMapper $staticTypeMapper
34+
private StaticTypeMapper $staticTypeMapper,
35+
private ArrayTypeLeastCommonDenominatorResolver $arrayTypeLeastCommonDenominatorResolver
3536
) {
3637

3738
}
@@ -107,6 +108,11 @@ public function generalizeConstantTypes(Type $type): Type
107108

108109
// too long
109110
if (strlen((string) $unionedDocType) > self::MAX_PRINTED_UNION_DOC_LENGHT) {
111+
$alwaysKnownArrayType = $this->narrowToAlwaysKnownArrayType($generalizedUnionType);
112+
if ($alwaysKnownArrayType instanceof ArrayType) {
113+
return $alwaysKnownArrayType;
114+
}
115+
110116
return new MixedType();
111117
}
112118

@@ -145,4 +151,15 @@ private function isImplicitNumberedListKeyType(ConstantArrayType $constantArrayT
145151

146152
return true;
147153
}
154+
155+
private function narrowToAlwaysKnownArrayType(UnionType $unionType): ?ArrayType
156+
{
157+
// always an array?
158+
if (count($unionType->getArrays()) !== count($unionType->getTypes())) {
159+
return null;
160+
}
161+
162+
$arrayUniqueKeyType = $this->arrayTypeLeastCommonDenominatorResolver->sharedArrayStructure(...$unionType->getTypes());
163+
return new ArrayType($arrayUniqueKeyType, new MixedType());
164+
}
148165
}

0 commit comments

Comments
 (0)