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