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