58
58
use function array_reduce ;
59
59
use function array_shift ;
60
60
use function count ;
61
+ use function is_array ;
61
62
use function lcfirst ;
62
63
use function substr ;
63
64
64
65
class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
65
66
{
66
67
68
+ private const ASSERTIONS_RESULTING_AT_LEAST_IN_NON_EMPTY_STRING = [
69
+ 'startsWithLetter ' ,
70
+ 'unicodeLetters ' ,
71
+ 'alpha ' ,
72
+ 'digits ' ,
73
+ 'alnum ' ,
74
+ 'lower ' ,
75
+ 'upper ' ,
76
+ 'uuid ' ,
77
+ 'ip ' ,
78
+ 'ipv4 ' ,
79
+ 'ipv6 ' ,
80
+ 'email ' ,
81
+ 'notWhitespaceOnly ' ,
82
+ ];
83
+
67
84
/** @var Closure[] */
68
85
private static $ resolvers ;
69
86
@@ -104,7 +121,7 @@ public function isStaticMethodSupported(
104
121
}
105
122
106
123
$ resolver = $ resolvers [$ trimmedName ];
107
- $ resolverReflection = new ReflectionObject ($ resolver );
124
+ $ resolverReflection = new ReflectionObject (Closure:: fromCallable ( $ resolver) );
108
125
109
126
return count ($ node ->getArgs ()) >= count ($ resolverReflection ->getMethod ('__invoke ' )->getParameters ()) - 1 ;
110
127
}
@@ -156,50 +173,60 @@ static function (Type $type) {
156
173
);
157
174
}
158
175
159
- $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
160
- if ($ expression === null ) {
176
+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
177
+ if ($ expr === null ) {
161
178
return new SpecifiedTypes ([], []);
162
179
}
163
180
164
181
return $ this ->typeSpecifier ->specifyTypesInCondition (
165
182
$ scope ,
166
- $ expression ,
167
- TypeSpecifierContext::createTruthy ()
183
+ $ expr ,
184
+ TypeSpecifierContext::createTruthy (),
185
+ $ rootExpr
168
186
);
169
187
}
170
188
171
189
/**
172
190
* @param Arg[] $args
191
+ * @return array{?Expr, ?Expr}
173
192
*/
174
193
private static function createExpression (
175
194
Scope $ scope ,
176
195
string $ name ,
177
196
array $ args
178
- ): ? Expr
197
+ ): array
179
198
{
180
199
$ trimmedName = self ::trimName ($ name );
181
200
$ resolvers = self ::getExpressionResolvers ();
182
201
$ resolver = $ resolvers [$ trimmedName ];
183
- $ expression = $ resolver ($ scope , ...$ args );
184
- if ($ expression === null ) {
185
- return null ;
202
+
203
+ $ resolverResult = $ resolver ($ scope , ...$ args );
204
+ if (is_array ($ resolverResult )) {
205
+ [$ expr , $ rootExpr ] = $ resolverResult ;
206
+ } else {
207
+ $ expr = $ resolverResult ;
208
+ $ rootExpr = null ;
209
+ }
210
+
211
+ if ($ expr === null ) {
212
+ return [null , null ];
186
213
}
187
214
188
215
if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189
- $ expression = new BooleanOr (
190
- $ expression ,
216
+ $ expr = new BooleanOr (
217
+ $ expr ,
191
218
new Identical (
192
219
$ args [0 ]->value ,
193
220
new ConstFetch (new Name ('null ' ))
194
221
)
195
222
);
196
223
}
197
224
198
- return $ expression ;
225
+ return [ $ expr , $ rootExpr ] ;
199
226
}
200
227
201
228
/**
202
- * @return Closure[]
229
+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203
230
*/
204
231
private static function getExpressionResolvers (): array
205
232
{
@@ -723,6 +750,23 @@ private static function getExpressionResolvers(): array
723
750
);
724
751
},
725
752
];
753
+
754
+ foreach (['contains ' , 'startsWith ' , 'endsWith ' ] as $ name ) {
755
+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value , Arg $ subString ): array {
756
+ if ($ scope ->getType ($ subString ->value )->isNonEmptyString ()->yes ()) {
757
+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value ]);
758
+ }
759
+
760
+ return [self ::$ resolvers ['string ' ]($ scope , $ value ), null ];
761
+ };
762
+ }
763
+
764
+ foreach (self ::ASSERTIONS_RESULTING_AT_LEAST_IN_NON_EMPTY_STRING as $ name ) {
765
+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value ): array {
766
+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value ]);
767
+ };
768
+ }
769
+
726
770
}
727
771
728
772
return self ::$ resolvers ;
@@ -790,15 +834,16 @@ private function handleAll(
790
834
{
791
835
$ args = $ node ->getArgs ();
792
836
$ args [0 ] = new Arg (new ArrayDimFetch ($ args [0 ]->value , new LNumber (0 )));
793
- $ expression = self ::createExpression ($ scope , $ methodName , $ args );
794
- if ($ expression === null ) {
837
+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ methodName , $ args );
838
+ if ($ expr === null ) {
795
839
return new SpecifiedTypes ();
796
840
}
797
841
798
842
$ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799
843
$ scope ,
800
- $ expression ,
801
- TypeSpecifierContext::createTruthy ()
844
+ $ expr ,
845
+ TypeSpecifierContext::createTruthy (),
846
+ $ rootExpr
802
847
);
803
848
804
849
$ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +862,8 @@ private function handleAll(
817
862
$ node ->getArgs ()[0 ]->value ,
818
863
static function () use ($ type ): Type {
819
864
return $ type ;
820
- }
865
+ },
866
+ $ rootExpr
821
867
);
822
868
}
823
869
@@ -827,7 +873,8 @@ static function () use ($type): Type {
827
873
private function arrayOrIterable (
828
874
Scope $ scope ,
829
875
Expr $ expr ,
830
- Closure $ typeCallback
876
+ Closure $ typeCallback ,
877
+ ?Expr $ rootExpr = null
831
878
): SpecifiedTypes
832
879
{
833
880
$ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -859,7 +906,8 @@ private function arrayOrIterable(
859
906
$ specifiedType ,
860
907
TypeSpecifierContext::createTruthy (),
861
908
false ,
862
- $ scope
909
+ $ scope ,
910
+ $ rootExpr
863
911
);
864
912
}
865
913
@@ -900,4 +948,31 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900
948
return self ::implodeExpr ($ resolvers , BooleanOr::class);
901
949
}
902
950
951
+ /**
952
+ * @param Arg[] $args
953
+ * @return array{Expr, Expr}
954
+ */
955
+ private static function createIsNonEmptyStringAndSomethingExprPair (array $ args ): array
956
+ {
957
+ $ expr = new BooleanAnd (
958
+ new FuncCall (
959
+ new Name ('is_string ' ),
960
+ [$ args [0 ]]
961
+ ),
962
+ new NotIdentical (
963
+ $ args [0 ]->value ,
964
+ new String_ ('' )
965
+ )
966
+ );
967
+
968
+ $ rootExpr = new BooleanAnd (
969
+ $ expr ,
970
+ new FuncCall (new Name ('FAUX_FUNCTION ' ), [
971
+ new Arg ($ args [0 ]->value ),
972
+ ])
973
+ );
974
+
975
+ return [$ expr , $ rootExpr ];
976
+ }
977
+
903
978
}
0 commit comments