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 ;
19
18
use PHPStan \Type \Constant \ConstantStringType ;
19
+ use PHPStan \Type \IntersectionType ;
20
20
use PHPStan \Type \IterableType ;
21
21
use PHPStan \Type \MixedType ;
22
22
use PHPStan \Type \ObjectType ;
23
23
use PHPStan \Type \StaticMethodTypeSpecifyingExtension ;
24
+ use PHPStan \Type \StringType ;
24
25
use PHPStan \Type \Type ;
25
26
use PHPStan \Type \TypeCombinator ;
26
27
use PHPStan \Type \TypeUtils ;
27
28
28
29
class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
29
30
{
30
31
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 ' ,
32
+ private const ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER = [
33
+ 'stringNotEmpty ' => 1 ,
34
+ 'contains ' => 2 ,
35
+ 'startsWith ' => 2 ,
36
+ 'startsWithLetter ' => 1 ,
37
+ 'endsWith ' => 2 ,
38
+ 'unicodeLetters ' => 1 ,
39
+ 'alpha ' => 1 ,
40
+ 'digits ' => 1 ,
41
+ 'alnum ' => 1 ,
42
+ 'lower ' => 1 ,
43
+ 'upper ' => 1 ,
44
+ 'uuid ' => 1 ,
45
+ 'ip ' => 1 ,
46
+ 'ipv4 ' => 1 ,
47
+ 'ipv6 ' => 1 ,
48
+ 'email ' => 1 ,
49
+ 'notWhitespaceOnly ' => 1 ,
46
50
];
47
51
48
52
/** @var \Closure[] */
@@ -67,10 +71,6 @@ public function isStaticMethodSupported(
67
71
TypeSpecifierContext $ context
68
72
): bool
69
73
{
70
- if (in_array ($ staticMethodReflection ->getName (), self ::ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING , true )) {
71
- return true ;
72
- }
73
-
74
74
if (substr ($ staticMethodReflection ->getName (), 0 , 6 ) === 'allNot ' ) {
75
75
$ methods = [
76
76
'allNotInstanceOf ' => 2 ,
@@ -84,6 +84,13 @@ public function isStaticMethodSupported(
84
84
$ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
85
85
$ resolvers = self ::getExpressionResolvers ();
86
86
87
+ if (
88
+ array_key_exists ($ trimmedName , self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER )
89
+ && count ($ node ->getArgs ()) >= self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER [$ trimmedName ]
90
+ ) {
91
+ return true ;
92
+ }
93
+
87
94
if (!array_key_exists ($ trimmedName , $ resolvers )) {
88
95
return false ;
89
96
}
@@ -113,45 +120,138 @@ public function specifyTypes(
113
120
TypeSpecifierContext $ context
114
121
): SpecifiedTypes
115
122
{
123
+ $ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
124
+
116
125
if (substr ($ staticMethodReflection ->getName (), 0 , 6 ) === 'allNot ' ) {
117
126
return $ this ->handleAllNot (
118
127
$ staticMethodReflection ->getName (),
119
128
$ node ,
120
129
$ scope
121
130
);
122
131
}
123
- $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
124
- if ($ expression === null ) {
125
- return new SpecifiedTypes ([], []);
132
+
133
+ if (array_key_exists ($ trimmedName , self ::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER )) {
134
+ $ specifiedTypes = $ this ->specifyTypesViaCustomTypeSpecifier (
135
+ $ staticMethodReflection ,
136
+ $ node ,
137
+ $ scope
138
+ );
139
+ } else {
140
+ $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
141
+ if ($ expression === null ) {
142
+ return new SpecifiedTypes ([], []);
143
+ }
144
+
145
+ $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
146
+ $ scope ,
147
+ $ expression ,
148
+ TypeSpecifierContext::createTruthy ()
149
+ );
126
150
}
127
- $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
128
- $ scope ,
129
- $ expression ,
130
- TypeSpecifierContext::createTruthy ()
131
- );
132
151
133
152
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
- }
153
+ return $ this -> arrayOrIterable (
154
+ $ scope ,
155
+ $ node -> getArgs ()[ 0 ]-> value ,
156
+ function () use ( $ specifiedTypes ): Type {
157
+ return $ this -> getResultingTypeFromSpecifiedTypes ( $ specifiedTypes ) ;
158
+ }
159
+ );
160
+ }
161
+
162
+ if ( substr ( $ staticMethodReflection -> getName (), 0 , 6 ) === ' nullOr ' ) {
163
+ return $ this -> typeSpecifier -> create (
164
+ $ node -> getArgs ()[ 0 ]-> value ,
165
+ TypeCombinator:: addNull ( $ this -> getResultingTypeFromSpecifiedTypes ( $ specifiedTypes )),
166
+ TypeSpecifierContext:: createTruthy (),
167
+ true
168
+ );
150
169
}
151
170
152
171
return $ specifiedTypes ;
153
172
}
154
173
174
+ private function getResultingTypeFromSpecifiedTypes (SpecifiedTypes $ specifiedTypes ): Type
175
+ {
176
+ if (count ($ specifiedTypes ->getSureTypes ()) > 0 ) {
177
+ $ sureTypes = $ specifiedTypes ->getSureTypes ();
178
+ reset ($ sureTypes );
179
+ $ exprString = key ($ sureTypes );
180
+ $ sureType = $ sureTypes [$ exprString ];
181
+
182
+ $ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
183
+
184
+ return array_key_exists ($ exprString , $ sureNotTypes )
185
+ ? TypeCombinator::remove ($ sureType [1 ], $ sureNotTypes [$ exprString ][1 ])
186
+ : $ sureType [1 ];
187
+ }
188
+
189
+ throw new \PHPStan \ShouldNotHappenException ();
190
+ }
191
+
192
+ private function specifyTypesViaCustomTypeSpecifier (
193
+ MethodReflection $ staticMethodReflection ,
194
+ StaticCall $ node ,
195
+ Scope $ scope
196
+ ): SpecifiedTypes
197
+ {
198
+ $ trimmedName = self ::trimName ($ staticMethodReflection ->getName ());
199
+
200
+ $ expression = $ node ->getArgs ()[0 ]->value ;
201
+ $ typeBefore = $ scope ->getType ($ expression );
202
+
203
+ // Adds support for calling via all*
204
+ $ typeBefore = $ typeBefore ->isIterable ()->yes () ? $ typeBefore ->getIterableValueType () : $ typeBefore ;
205
+
206
+ switch ($ trimmedName ) {
207
+ case 'startsWithLetter ' :
208
+ case 'digits ' :
209
+ case 'alnum ' :
210
+ case 'lower ' :
211
+ case 'upper ' :
212
+ case 'uuid ' :
213
+ case 'notWhitespaceOnly ' :
214
+ // Assertions narrowing down to non-empty-string if the input is a string
215
+ $ type = (new StringType ())->isSuperTypeOf ($ typeBefore )->yes ()
216
+ ? TypeCombinator::intersect ($ typeBefore , new AccessoryNonEmptyStringType ())
217
+ : $ typeBefore ;
218
+
219
+ return $ this ->typeSpecifier ->create (
220
+ $ expression ,
221
+ $ type ,
222
+ TypeSpecifierContext::createTruthy ()
223
+ );
224
+ case 'stringNotEmpty ' :
225
+ case 'unicodeLetters ' :
226
+ case 'alpha ' :
227
+ case 'ip ' :
228
+ case 'ipv4 ' :
229
+ case 'ipv6 ' :
230
+ case 'email ' :
231
+ // Assertions always narrowing down to non-empty-string
232
+ return $ this ->typeSpecifier ->create (
233
+ $ expression ,
234
+ new IntersectionType ([new StringType (), new AccessoryNonEmptyStringType ()]),
235
+ TypeSpecifierContext::createTruthy ()
236
+ );
237
+ case 'contains ' :
238
+ case 'startsWith ' :
239
+ case 'endsWith ' :
240
+ // Assertions narrowing down to non-empty-string if the input is a string and arg1 is a non-empty-string
241
+ $ type = (new StringType ())->isSuperTypeOf ($ typeBefore )->yes () && $ scope ->getType ($ node ->getArgs ()[1 ]->value )->isNonEmptyString ()->yes ()
242
+ ? TypeCombinator::intersect ($ typeBefore , new AccessoryNonEmptyStringType ())
243
+ : $ typeBefore ;
244
+
245
+ return $ this ->typeSpecifier ->create (
246
+ $ expression ,
247
+ $ type ,
248
+ TypeSpecifierContext::createTruthy ()
249
+ );
250
+ }
251
+
252
+ throw new \PHPStan \ShouldNotHappenException ();
253
+ }
254
+
155
255
/**
156
256
* @param Scope $scope
157
257
* @param string $name
@@ -165,29 +265,10 @@ private static function createExpression(
165
265
): ?\PhpParser \Node \Expr
166
266
{
167
267
$ 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
268
$ resolvers = self ::getExpressionResolvers ();
174
269
$ resolver = $ resolvers [$ trimmedName ];
175
- $ expression = $ resolver ($ scope , ...$ args );
176
- if ($ expression === null ) {
177
- return null ;
178
- }
179
270
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 ;
271
+ return $ resolver ($ scope , ...$ args );
191
272
}
192
273
193
274
/**
@@ -486,27 +567,6 @@ private static function getExpressionResolvers(): array
486
567
)
487
568
);
488
569
},
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
570
'length ' => function (Scope $ scope , Arg $ value , Arg $ length ): \PhpParser \Node \Expr {
511
571
return new BooleanAnd (
512
572
new \PhpParser \Node \Expr \FuncCall (
@@ -709,32 +769,4 @@ private function arrayOrIterable(
709
769
);
710
770
}
711
771
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
772
}
0 commit comments