5858use function array_reduce ;
5959use function array_shift ;
6060use function count ;
61+ use function is_array ;
6162use function lcfirst ;
6263use function substr ;
6364
6465class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
6566{
6667
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+
6784 /** @var Closure[] */
6885 private static $ resolvers ;
6986
@@ -104,7 +121,7 @@ public function isStaticMethodSupported(
104121 }
105122
106123 $ resolver = $ resolvers [$ trimmedName ];
107- $ resolverReflection = new ReflectionObject ($ resolver );
124+ $ resolverReflection = new ReflectionObject (Closure:: fromCallable ( $ resolver) );
108125
109126 return count ($ node ->getArgs ()) >= count ($ resolverReflection ->getMethod ('__invoke ' )->getParameters ()) - 1 ;
110127 }
@@ -156,50 +173,60 @@ static function (Type $type) {
156173 );
157174 }
158175
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 ) {
161178 return new SpecifiedTypes ([], []);
162179 }
163180
164181 return $ this ->typeSpecifier ->specifyTypesInCondition (
165182 $ scope ,
166- $ expression ,
167- TypeSpecifierContext::createTruthy ()
183+ $ expr ,
184+ TypeSpecifierContext::createTruthy (),
185+ $ rootExpr
168186 );
169187 }
170188
171189 /**
172190 * @param Arg[] $args
191+ * @return array{?Expr, ?Expr}
173192 */
174193 private static function createExpression (
175194 Scope $ scope ,
176195 string $ name ,
177196 array $ args
178- ): ? Expr
197+ ): array
179198 {
180199 $ trimmedName = self ::trimName ($ name );
181200 $ resolvers = self ::getExpressionResolvers ();
182201 $ 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 ];
186213 }
187214
188215 if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189- $ expression = new BooleanOr (
190- $ expression ,
216+ $ expr = new BooleanOr (
217+ $ expr ,
191218 new Identical (
192219 $ args [0 ]->value ,
193220 new ConstFetch (new Name ('null ' ))
194221 )
195222 );
196223 }
197224
198- return $ expression ;
225+ return [ $ expr , $ rootExpr ] ;
199226 }
200227
201228 /**
202- * @return Closure[]
229+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203230 */
204231 private static function getExpressionResolvers (): array
205232 {
@@ -723,6 +750,23 @@ private static function getExpressionResolvers(): array
723750 );
724751 },
725752 ];
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+
726770 }
727771
728772 return self ::$ resolvers ;
@@ -790,15 +834,16 @@ private function handleAll(
790834 {
791835 $ args = $ node ->getArgs ();
792836 $ 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 ) {
795839 return new SpecifiedTypes ();
796840 }
797841
798842 $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799843 $ scope ,
800- $ expression ,
801- TypeSpecifierContext::createTruthy ()
844+ $ expr ,
845+ TypeSpecifierContext::createTruthy (),
846+ $ rootExpr
802847 );
803848
804849 $ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +862,8 @@ private function handleAll(
817862 $ node ->getArgs ()[0 ]->value ,
818863 static function () use ($ type ): Type {
819864 return $ type ;
820- }
865+ },
866+ $ rootExpr
821867 );
822868 }
823869
@@ -827,7 +873,8 @@ static function () use ($type): Type {
827873 private function arrayOrIterable (
828874 Scope $ scope ,
829875 Expr $ expr ,
830- Closure $ typeCallback
876+ Closure $ typeCallback ,
877+ ?Expr $ rootExpr = null
831878 ): SpecifiedTypes
832879 {
833880 $ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -859,7 +906,8 @@ private function arrayOrIterable(
859906 $ specifiedType ,
860907 TypeSpecifierContext::createTruthy (),
861908 false ,
862- $ scope
909+ $ scope ,
910+ $ rootExpr
863911 );
864912 }
865913
@@ -900,4 +948,31 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900948 return self ::implodeExpr ($ resolvers , BooleanOr::class);
901949 }
902950
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+
903978}
0 commit comments