5
5
use PhpParser \Node \Expr \FuncCall ;
6
6
use PHPStan \Analyser \Scope ;
7
7
use PHPStan \Reflection \FunctionReflection ;
8
+ use PHPStan \TrinaryLogic ;
9
+ use PHPStan \Type \Accessory \AccessoryArrayListType ;
8
10
use PHPStan \Type \Accessory \NonEmptyArrayType ;
9
11
use PHPStan \Type \ArrayType ;
12
+ use PHPStan \Type \Constant \ConstantArrayType ;
13
+ use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
14
+ use PHPStan \Type \Constant \ConstantIntegerType ;
15
+ use PHPStan \Type \Constant \ConstantStringType ;
10
16
use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
17
+ use PHPStan \Type \NeverType ;
11
18
use PHPStan \Type \Type ;
12
19
use PHPStan \Type \TypeCombinator ;
20
+ use function array_keys ;
13
21
use function count ;
22
+ use function in_array ;
14
23
use function strtolower ;
15
24
16
25
final class ArrayReplaceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -23,54 +32,107 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
23
32
24
33
public function getTypeFromFunctionCall (FunctionReflection $ functionReflection , FuncCall $ functionCall , Scope $ scope ): ?Type
25
34
{
26
- $ arrayTypes = $ this -> collectArrayTypes ( $ functionCall , $ scope );
35
+ $ args = $ functionCall -> getArgs ( );
27
36
28
- if (count ( $ arrayTypes ) === 0 ) {
37
+ if (! isset ( $ args [ 0 ]) ) {
29
38
return null ;
30
39
}
31
40
32
- return $ this ->getResultType (...$ arrayTypes );
33
- }
41
+ $ argTypes = [];
42
+ $ optionalArgTypes = [];
43
+ foreach ($ args as $ arg ) {
44
+ $ argType = $ scope ->getType ($ arg ->value );
34
45
35
- private function getResultType (Type ...$ arrayTypes ): Type
36
- {
37
- $ keyTypes = [];
38
- $ valueTypes = [];
39
- $ nonEmptyArray = false ;
40
- foreach ($ arrayTypes as $ arrayType ) {
41
- if (!$ nonEmptyArray && $ arrayType ->isIterableAtLeastOnce ()->yes ()) {
42
- $ nonEmptyArray = true ;
46
+ if ($ arg ->unpack ) {
47
+ if ($ argType ->isConstantArray ()->yes ()) {
48
+ foreach ($ argType ->getConstantArrays () as $ constantArray ) {
49
+ foreach ($ constantArray ->getValueTypes () as $ valueType ) {
50
+ $ argTypes [] = $ valueType ;
51
+ }
52
+ }
53
+ } else {
54
+ $ argTypes [] = $ argType ->getIterableValueType ();
55
+ }
56
+
57
+ if (!$ argType ->isIterableAtLeastOnce ()->yes ()) {
58
+ // unpacked params can be empty, making them optional
59
+ $ optionalArgTypesOffset = count ($ argTypes ) - 1 ;
60
+ foreach (array_keys ($ argTypes ) as $ key ) {
61
+ $ optionalArgTypes [] = $ optionalArgTypesOffset + $ key ;
62
+ }
63
+ }
64
+ } else {
65
+ $ argTypes [] = $ argType ;
43
66
}
44
-
45
- $ keyTypes [] = $ arrayType ->getIterableKeyType ();
46
- $ valueTypes [] = $ arrayType ->getIterableValueType ();
47
67
}
48
68
49
- $ keyType = TypeCombinator::union (...$ keyTypes );
50
- $ valueType = TypeCombinator::union (...$ valueTypes );
69
+ $ allConstant = TrinaryLogic::createYes ()->lazyAnd (
70
+ $ argTypes ,
71
+ static fn (Type $ argType ) => $ argType ->isConstantArray (),
72
+ );
73
+
74
+ if ($ allConstant ->yes ()) {
75
+ $ newArrayBuilder = ConstantArrayTypeBuilder::createEmpty ();
76
+
77
+ foreach ($ argTypes as $ argType ) {
78
+ /** @var array<int|string, ConstantIntegerType|ConstantStringType> $keyTypes */
79
+ $ keyTypes = [];
80
+ foreach ($ argType ->getConstantArrays () as $ constantArray ) {
81
+ foreach ($ constantArray ->getKeyTypes () as $ keyType ) {
82
+ $ keyTypes [$ keyType ->getValue ()] = $ keyType ;
83
+ }
84
+ }
85
+
86
+ foreach ($ keyTypes as $ keyType ) {
87
+ $ newArrayBuilder ->setOffsetValueType (
88
+ $ keyType ,
89
+ $ argType ->getOffsetValueType ($ keyType ),
90
+ !$ argType ->hasOffsetValueType ($ keyType )->yes (),
91
+ );
92
+ }
93
+ }
51
94
52
- $ arrayType = new ArrayType ($ keyType , $ valueType );
53
- return $ nonEmptyArray ? TypeCombinator::intersect ($ arrayType , new NonEmptyArrayType ()) : $ arrayType ;
54
- }
95
+ return $ newArrayBuilder ->getArray ();
96
+ }
55
97
56
- /**
57
- * @return Type[]
58
- */
59
- private function collectArrayTypes (FuncCall $ functionCall , Scope $ scope ): array
60
- {
61
- $ args = $ functionCall ->getArgs ();
98
+ $ keyTypes = [];
99
+ $ valueTypes = [];
100
+ $ nonEmpty = false ;
101
+ $ isList = true ;
102
+ foreach ($ argTypes as $ key => $ argType ) {
103
+ $ keyType = $ argType ->getIterableKeyType ();
104
+ $ keyTypes [] = $ keyType ;
105
+ $ valueTypes [] = $ argType ->getIterableValueType ();
106
+
107
+ if (!$ argType ->isList ()->yes ()) {
108
+ $ isList = false ;
109
+ }
62
110
63
- $ arrayTypes = [];
64
- foreach ($ args as $ arg ) {
65
- $ argType = $ scope ->getType ($ arg ->value );
66
- if (!$ argType ->isArray ()->yes ()) {
111
+ if (in_array ($ key , $ optionalArgTypes , true ) || !$ argType ->isIterableAtLeastOnce ()->yes ()) {
67
112
continue ;
68
113
}
69
114
70
- $ arrayTypes [] = $ arg ->unpack ? $ argType ->getIterableValueType () : $ argType ;
115
+ $ nonEmpty = true ;
116
+ }
117
+
118
+ $ keyType = TypeCombinator::union (...$ keyTypes );
119
+ if ($ keyType instanceof NeverType) {
120
+ return new ConstantArrayType ([], []);
121
+ }
122
+
123
+ $ arrayType = new ArrayType (
124
+ $ keyType ,
125
+ TypeCombinator::union (...$ valueTypes ),
126
+ );
127
+
128
+ if ($ nonEmpty ) {
129
+ $ arrayType = TypeCombinator::intersect ($ arrayType , new NonEmptyArrayType ());
130
+ }
131
+ if ($ isList ) {
132
+ $ arrayType = TypeCombinator::intersect ($ arrayType , new AccessoryArrayListType ());
71
133
}
72
134
73
- return $ arrayTypes ;
135
+ return $ arrayType ;
74
136
}
75
137
76
138
}
0 commit comments