4
4
5
5
use PhpParser \Node \Arg ;
6
6
use PhpParser \Node \Expr \BinaryOp \BooleanAnd ;
7
- use PhpParser \Node \Expr \BinaryOp \NotIdentical ;
8
7
use PhpParser \Node \Expr \StaticCall ;
9
- use PhpParser \Node \Scalar \String_ ;
10
8
use PHPStan \Analyser \Scope ;
11
9
use PHPStan \Analyser \SpecifiedTypes ;
12
10
use PHPStan \Analyser \TypeSpecifier ;
13
11
use PHPStan \Analyser \TypeSpecifierAwareExtension ;
14
12
use PHPStan \Analyser \TypeSpecifierContext ;
15
13
use PHPStan \Reflection \MethodReflection ;
14
+ use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
16
15
use PHPStan \Type \ArrayType ;
17
16
use PHPStan \Type \Constant \ConstantArrayType ;
18
17
use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
21
20
use PHPStan \Type \MixedType ;
22
21
use PHPStan \Type \ObjectType ;
23
22
use PHPStan \Type \StaticMethodTypeSpecifyingExtension ;
23
+ use PHPStan \Type \StringType ;
24
24
use PHPStan \Type \Type ;
25
25
use PHPStan \Type \TypeCombinator ;
26
26
use PHPStan \Type \TypeUtils ;
27
27
28
28
class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
29
29
{
30
30
31
- private const ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING = [
32
- 'stringNotEmpty ' ,
33
- 'startsWithLetter ' ,
34
- 'unicodeLetters ' ,
35
- 'alpha ' ,
36
- 'digits ' ,
37
- 'alnum ' ,
38
- 'lower ' ,
39
- 'upper ' ,
40
- 'uuid ' ,
41
- 'ip ' ,
42
- 'ipv4 ' ,
43
- 'ipv6 ' ,
44
- 'email ' ,
45
- 'notWhitespaceOnly ' ,
31
+ private const ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER = [
32
+ 'stringNotEmpty ' => 1 ,
33
+ 'contains ' => 2 ,
34
+ 'startsWith ' => 2 ,
35
+ 'startsWithLetter ' => 1 ,
36
+ 'endsWith ' => 2 ,
37
+ 'unicodeLetters ' => 1 ,
38
+ 'alpha ' => 1 ,
39
+ 'digits ' => 1 ,
40
+ 'alnum ' => 1 ,
41
+ 'lower ' => 1 ,
42
+ 'upper ' => 1 ,
43
+ 'uuid ' => 1 ,
44
+ 'ip ' => 1 ,
45
+ 'ipv4 ' => 1 ,
46
+ 'ipv6 ' => 1 ,
47
+ 'email ' => 1 ,
48
+ 'notWhitespaceOnly ' => 1 ,
46
49
];
47
50
48
51
/** @var \Closure[] */
@@ -67,10 +70,6 @@ public function isStaticMethodSupported(
67
70
TypeSpecifierContext $ context
68
71
): bool
69
72
{
70
- if (in_array ($ staticMethodReflection ->getName (), self ::ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING , true )) {
71
- return true ;
72
- }
73
-
74
73
if (substr ($ staticMethodReflection ->getName (), 0 , 6 ) === 'allNot ' ) {
75
74
$ methods = [
76
75
'allNotInstanceOf ' => 2 ,
@@ -84,6 +83,13 @@ public function isStaticMethodSupported(
84
83
$ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
85
84
$ resolvers = self ::getExpressionResolvers ();
86
85
86
+ if (
87
+ array_key_exists ($ trimmedName , self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER )
88
+ && count ($ node ->getArgs ()) >= self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER [$ trimmedName ]
89
+ ) {
90
+ return true ;
91
+ }
92
+
87
93
if (!array_key_exists ($ trimmedName , $ resolvers )) {
88
94
return false ;
89
95
}
@@ -113,45 +119,138 @@ public function specifyTypes(
113
119
TypeSpecifierContext $ context
114
120
): SpecifiedTypes
115
121
{
122
+ $ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
123
+
116
124
if (substr ($ staticMethodReflection ->getName (), 0 , 6 ) === 'allNot ' ) {
117
125
return $ this ->handleAllNot (
118
126
$ staticMethodReflection ->getName (),
119
127
$ node ,
120
128
$ scope
121
129
);
122
130
}
123
- $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
124
- if ($ expression === null ) {
125
- return new SpecifiedTypes ([], []);
131
+
132
+ if (array_key_exists ($ trimmedName , self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER )) {
133
+ $ specifiedTypes = $ this ->specifyTypesViaCustomTypeSpecifier (
134
+ $ staticMethodReflection ,
135
+ $ node ,
136
+ $ scope
137
+ );
138
+ } else {
139
+ $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
140
+ if ($ expression === null ) {
141
+ return new SpecifiedTypes ([], []);
142
+ }
143
+
144
+ $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
145
+ $ scope ,
146
+ $ expression ,
147
+ TypeSpecifierContext::createTruthy ()
148
+ );
126
149
}
127
- $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
128
- $ scope ,
129
- $ expression ,
130
- TypeSpecifierContext::createTruthy ()
131
- );
132
150
133
151
if (substr ($ staticMethodReflection ->getName (), 0 , 3 ) === 'all ' ) {
134
- if ( count ( $ specifiedTypes -> getSureTypes ()) > 0 ) {
135
- $ sureTypes = $ specifiedTypes -> getSureTypes ();
136
- reset ( $ sureTypes );
137
- $ exprString = key ( $ sureTypes );
138
- $ sureType = $ sureTypes [ $ exprString ] ;
139
- return $ this -> arrayOrIterable (
140
- $ scope ,
141
- $ sureType [ 0 ],
142
- function () use ( $ sureType ): Type {
143
- return $ sureType [ 1 ];
144
- }
145
- );
146
- }
147
- if ( count ( $ specifiedTypes -> getSureNotTypes ()) > 0 ) {
148
- throw new \ PHPStan \ ShouldNotHappenException ();
149
- }
152
+ return $ this -> arrayOrIterable (
153
+ $ scope ,
154
+ $ node -> getArgs ()[ 0 ]-> value ,
155
+ function () use ( $ specifiedTypes ): Type {
156
+ return $ this -> getResultingTypeFromSpecifiedTypes ( $ specifiedTypes ) ;
157
+ }
158
+ );
159
+ }
160
+
161
+ if ( substr ( $ staticMethodReflection -> getName (), 0 , 6 ) === ' nullOr ' ) {
162
+ return $ this -> typeSpecifier -> create (
163
+ $ node -> getArgs ()[ 0 ]-> value ,
164
+ TypeCombinator:: addNull ( $ this -> getResultingTypeFromSpecifiedTypes ( $ specifiedTypes )),
165
+ TypeSpecifierContext:: createTruthy (),
166
+ true
167
+ );
150
168
}
151
169
152
170
return $ specifiedTypes ;
153
171
}
154
172
173
+ private function getResultingTypeFromSpecifiedTypes (SpecifiedTypes $ specifiedTypes ): Type
174
+ {
175
+ if (count ($ specifiedTypes ->getSureTypes ()) > 0 ) {
176
+ $ sureTypes = $ specifiedTypes ->getSureTypes ();
177
+ reset ($ sureTypes );
178
+ $ exprString = key ($ sureTypes );
179
+ $ sureType = $ sureTypes [$ exprString ];
180
+
181
+ $ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
182
+
183
+ return array_key_exists ($ exprString , $ sureNotTypes )
184
+ ? TypeCombinator::remove ($ sureType [1 ], $ sureNotTypes [$ exprString ][1 ])
185
+ : $ sureType [1 ];
186
+ }
187
+
188
+ throw new \PHPStan \ShouldNotHappenException ();
189
+ }
190
+
191
+ private function specifyTypesViaCustomTypeSpecifier (
192
+ MethodReflection $ staticMethodReflection ,
193
+ StaticCall $ node ,
194
+ Scope $ scope
195
+ ): SpecifiedTypes
196
+ {
197
+ $ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
198
+
199
+ $ expression = $ node ->getArgs ()[0 ]->value ;
200
+ $ typeBefore = $ scope ->getType ($ expression );
201
+
202
+ // Adds support for calling via all*
203
+ $ typeBefore = $ typeBefore ->isIterable ()->yes () ? $ typeBefore ->getIterableValueType () : $ typeBefore ;
204
+
205
+ switch ($ trimmedName ) {
206
+ case 'startsWithLetter ' :
207
+ case 'digits ' :
208
+ case 'alnum ' :
209
+ case 'lower ' :
210
+ case 'upper ' :
211
+ case 'uuid ' :
212
+ case 'notWhitespaceOnly ' :
213
+ // Assertions narrowing down to non-empty-string if the input is a string
214
+ $ type = (new StringType ())->isSuperTypeOf ($ typeBefore )->yes ()
215
+ ? TypeCombinator::intersect ($ typeBefore , new AccessoryNonEmptyStringType ())
216
+ : $ typeBefore ;
217
+
218
+ return $ this ->typeSpecifier ->create (
219
+ $ expression ,
220
+ $ type ,
221
+ TypeSpecifierContext::createTruthy ()
222
+ );
223
+ case 'stringNotEmpty ' :
224
+ case 'unicodeLetters ' :
225
+ case 'alpha ' :
226
+ case 'ip ' :
227
+ case 'ipv4 ' :
228
+ case 'ipv6 ' :
229
+ case 'email ' :
230
+ // Assertions always narrowing down to non-empty-string
231
+ return $ this ->typeSpecifier ->create (
232
+ $ expression ,
233
+ TypeCombinator::intersect ($ typeBefore , new AccessoryNonEmptyStringType ()),
234
+ TypeSpecifierContext::createTruthy ()
235
+ );
236
+ case 'contains ' :
237
+ case 'startsWith ' :
238
+ case 'endsWith ' :
239
+ // Assertions narrowing down to non-empty-string if the input is a string and arg1 is a non-empty-string
240
+ $ type = (new StringType ())->isSuperTypeOf ($ typeBefore )->yes () && $ scope ->getType ($ node ->getArgs ()[1 ]->value )->isNonEmptyString ()->yes ()
241
+ ? TypeCombinator::intersect ($ typeBefore , new AccessoryNonEmptyStringType ())
242
+ : $ typeBefore ;
243
+
244
+ return $ this ->typeSpecifier ->create (
245
+ $ expression ,
246
+ $ type ,
247
+ TypeSpecifierContext::createTruthy ()
248
+ );
249
+ }
250
+
251
+ throw new \PHPStan \ShouldNotHappenException ();
252
+ }
253
+
155
254
/**
156
255
* @param Scope $scope
157
256
* @param string $name
@@ -165,29 +264,10 @@ private static function createExpression(
165
264
): ?\PhpParser \Node \Expr
166
265
{
167
266
$ trimmedName = self ::trimName ($ name );
168
-
169
- if (in_array ($ trimmedName , self ::ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING , true )) {
170
- return self ::createIsNonEmptyStringExpression ($ args );
171
- }
172
-
173
267
$ resolvers = self ::getExpressionResolvers ();
174
268
$ resolver = $ resolvers [$ trimmedName ];
175
- $ expression = $ resolver ($ scope , ...$ args );
176
- if ($ expression === null ) {
177
- return null ;
178
- }
179
269
180
- if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
181
- $ expression = new \PhpParser \Node \Expr \BinaryOp \BooleanOr (
182
- $ expression ,
183
- new \PhpParser \Node \Expr \BinaryOp \Identical (
184
- $ args [0 ]->value ,
185
- new \PhpParser \Node \Expr \ConstFetch (new \PhpParser \Node \Name ('null ' ))
186
- )
187
- );
188
- }
189
-
190
- return $ expression ;
270
+ return $ resolver ($ scope , ...$ args );
191
271
}
192
272
193
273
/**
@@ -486,27 +566,6 @@ private static function getExpressionResolvers(): array
486
566
)
487
567
);
488
568
},
489
- 'contains ' => function (Scope $ scope , Arg $ value , Arg $ subString ): \PhpParser \Node \Expr {
490
- if ($ scope ->getType ($ subString ->value )->isNonEmptyString ()->yes ()) {
491
- return self ::createIsNonEmptyStringExpression ([$ value ]);
492
- }
493
-
494
- return self ::createIsStringExpression ([$ value ]);
495
- },
496
- 'startsWith ' => function (Scope $ scope , Arg $ value , Arg $ prefix ): \PhpParser \Node \Expr {
497
- if ($ scope ->getType ($ prefix ->value )->isNonEmptyString ()->yes ()) {
498
- return self ::createIsNonEmptyStringExpression ([$ value ]);
499
- }
500
-
501
- return self ::createIsStringExpression ([$ value ]);
502
- },
503
- 'endsWith ' => function (Scope $ scope , Arg $ value , Arg $ suffix ): \PhpParser \Node \Expr {
504
- if ($ scope ->getType ($ suffix ->value )->isNonEmptyString ()->yes ()) {
505
- return self ::createIsNonEmptyStringExpression ([$ value ]);
506
- }
507
-
508
- return self ::createIsStringExpression ([$ value ]);
509
- },
510
569
'length ' => function (Scope $ scope , Arg $ value , Arg $ length ): \PhpParser \Node \Expr {
511
570
return new BooleanAnd (
512
571
new \PhpParser \Node \Expr \FuncCall (
@@ -709,32 +768,4 @@ private function arrayOrIterable(
709
768
);
710
769
}
711
770
712
- /**
713
- * @param \PhpParser\Node\Arg[] $args
714
- */
715
- private static function createIsStringExpression (array $ args ): \PhpParser \Node \Expr
716
- {
717
- return new \PhpParser \Node \Expr \FuncCall (
718
- new \PhpParser \Node \Name ('is_string ' ),
719
- [$ args [0 ]]
720
- );
721
- }
722
-
723
- /**
724
- * @param \PhpParser\Node\Arg[] $args
725
- */
726
- private static function createIsNonEmptyStringExpression (array $ args ): \PhpParser \Node \Expr
727
- {
728
- return new BooleanAnd (
729
- new \PhpParser \Node \Expr \FuncCall (
730
- new \PhpParser \Node \Name ('is_string ' ),
731
- [$ args [0 ]]
732
- ),
733
- new NotIdentical (
734
- $ args [0 ]->value ,
735
- new String_ ('' )
736
- )
737
- );
738
- }
739
-
740
771
}
0 commit comments