99use PhpParser \Node \Expr \Closure ;
1010use PhpParser \Node \Expr \FuncCall ;
1111use PhpParser \Node \Param ;
12- use PhpParser \Node \VariadicPlaceholder ;
1312use PHPStan \Reflection \Native \NativeFunctionReflection ;
1413use PHPStan \Type \ArrayType ;
1514use PHPStan \Type \MixedType ;
1615use PHPStan \Type \Type ;
17- use PHPStan \Type \UnionType ;
18- use PHPStan \Type \UnionTypeHelper ;
19- use Rector \NodeTypeResolver \TypeComparator \TypeComparator ;
2016use Rector \PHPStanStaticTypeMapper \Enum \TypeKind ;
2117use Rector \Rector \AbstractRector ;
2218use Rector \Reflection \ReflectionResolver ;
3026final class AddClosureParamTypeForArrayMapRector extends AbstractRector
3127{
3228 public function __construct (
33- private readonly TypeComparator $ typeComparator ,
3429 private readonly StaticTypeMapper $ staticTypeMapper ,
3530 private readonly ReflectionResolver $ reflectionResolver ,
3631 ) {
@@ -43,14 +38,14 @@ public function getRuleDefinition(): RuleDefinition
4338 [
4439 new CodeSample (
4540 <<<'CODE_SAMPLE'
46- array_map(function ($value, $key): string {
47- return $value . $key ;
41+ array_map(function ($value) {
42+ return strlen( $value) ;
4843}, $strings);
4944CODE_SAMPLE
5045 ,
5146 <<<'CODE_SAMPLE'
52- array_map(function (string $value, int $key): bool {
53- return $value . $key ;
47+ array_map(function (string $value) {
48+ return strlen( $value) ;
5449}, $strings);
5550CODE_SAMPLE
5651 ,
@@ -89,90 +84,46 @@ public function refactor(Node $node): ?Node
8984 return null ;
9085 }
9186
92- /** @var ArrayType[] $types */
93- $ types = array_filter (array_map (function (Arg |VariadicPlaceholder $ arg ): ?ArrayType {
94- if (! $ arg instanceof Arg) {
95- return null ;
96- }
87+ // array_map passes the value of each array positionally to the closure:
88+ // the i-th closure param receives values from the i-th array argument.
89+ $ arrayArgs = array_slice ($ node ->args , 1 );
9790
98- $ type = $ this ->getType ($ arg ->value );
91+ $ closure = $ args [0 ]->value ;
92+ $ changed = false ;
9993
100- if ($ type instanceof ArrayType) {
101- return $ type ;
94+ foreach ($ arrayArgs as $ position => $ arg ) {
95+ // spread (...$arrays) cannot be mapped positionally, bail out
96+ if (! $ arg instanceof Arg || $ arg ->unpack ) {
97+ return null ;
10298 }
10399
104- return null ;
105- }, array_slice ($ node ->args , 1 )));
106-
107- $ values = [];
108- $ keys = [];
109-
110- foreach ($ types as $ type ) {
111- $ values [] = $ type ->getIterableValueType ();
112- $ keys [] = $ type ->getIterableKeyType ();
113- }
114-
115- foreach ($ values as $ value ) {
116- if ($ value instanceof MixedType) {
117- $ values = [];
118- break ;
100+ $ param = $ closure ->params [$ position ] ?? null ;
101+ if (! $ param instanceof Param) {
102+ continue ;
119103 }
120104
121- if ($ value instanceof UnionType) {
122- $ values = [...$ values , ...$ value ->getTypes ()];
105+ $ arrayType = $ this ->getType ($ arg ->value );
106+ if (! $ arrayType instanceof ArrayType) {
107+ continue ;
123108 }
124- }
125109
126- foreach ($ keys as $ key ) {
127- if ($ key instanceof MixedType) {
128- $ keys = [];
129- break ;
110+ $ valueType = $ arrayType ->getIterableValueType ();
111+ if ($ valueType instanceof MixedType) {
112+ continue ;
130113 }
131114
132- if ($ key instanceof UnionType ) {
133- $ keys = [... $ keys , ... $ key -> getTypes ()] ;
115+ if ($ this -> refactorParameter ( $ param , $ valueType ) ) {
116+ $ changed = true ;
134117 }
135118 }
136119
137- $ filter = fn (Type $ type ): bool => ! $ type instanceof UnionType;
138-
139- $ valueType = $ this ->combineTypes (array_filter ($ values , $ filter ));
140- $ keyType = $ this ->combineTypes (array_filter ($ keys , $ filter ));
141-
142- if (! $ keyType instanceof Type && ! $ valueType instanceof Type) {
143- return null ;
144- }
145-
146- if ($ this ->updateClosureWithTypes ($ args [0 ]->value , $ keyType , $ valueType )) {
120+ if ($ changed ) {
147121 return $ node ;
148122 }
149123
150124 return null ;
151125 }
152126
153- private function updateClosureWithTypes (Closure $ closure , ?Type $ keyType , ?Type $ valueType ): bool
154- {
155- $ changes = false ;
156- $ valueParam = $ closure ->params [0 ] ?? null ;
157- $ keyParam = $ closure ->params [1 ] ?? null ;
158-
159- if ($ valueParam instanceof Param && $ valueType instanceof Type && $ this ->refactorParameter (
160- $ closure ->params [0 ],
161- $ valueType
162- )) {
163- $ changes = true ;
164- }
165-
166- if ($ keyParam instanceof Param && $ keyType instanceof Type && $ this ->refactorParameter (
167- $ closure ->params [1 ],
168- $ keyType
169- )) {
170- return true ;
171- }
172-
173- return $ changes ;
174- }
175-
176127 private function refactorParameter (Param $ param , Type $ type ): bool
177128 {
178129 // already set → no change
@@ -190,31 +141,4 @@ private function refactorParameter(Param $param, Type $type): bool
190141
191142 return true ;
192143 }
193-
194- /**
195- * @param Type[] $types
196- */
197- private function combineTypes (array $ types ): ?Type
198- {
199- if ($ types === []) {
200- return null ;
201- }
202-
203- $ types = array_reduce ($ types , function (array $ types , Type $ type ): array {
204- foreach ($ types as $ previousType ) {
205- if ($ this ->typeComparator ->areTypesEqual ($ type , $ previousType )) {
206- return $ types ;
207- }
208- }
209-
210- $ types [] = $ type ;
211- return $ types ;
212- }, []);
213-
214- if (count ($ types ) === 1 ) {
215- return $ types [0 ];
216- }
217-
218- return new UnionType (UnionTypeHelper::sortTypes ($ types ));
219- }
220144}
0 commit comments