3939use PHPStan \Type \ArrayType ;
4040use PHPStan \Type \Constant \ConstantArrayType ;
4141use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
42+ use PHPStan \Type \Constant \ConstantBooleanType ;
4243use PHPStan \Type \Constant \ConstantStringType ;
4344use PHPStan \Type \IterableType ;
4445use PHPStan \Type \MixedType ;
5859use function array_reduce ;
5960use function array_shift ;
6061use function count ;
62+ use function is_array ;
6163use function lcfirst ;
6264use function substr ;
6365
@@ -104,7 +106,7 @@ public function isStaticMethodSupported(
104106 }
105107
106108 $ resolver = $ resolvers [$ trimmedName ];
107- $ resolverReflection = new ReflectionObject ($ resolver );
109+ $ resolverReflection = new ReflectionObject (Closure:: fromCallable ( $ resolver) );
108110
109111 return count ($ node ->getArgs ()) >= count ($ resolverReflection ->getMethod ('__invoke ' )->getParameters ()) - 1 ;
110112 }
@@ -156,50 +158,62 @@ static function (Type $type) {
156158 );
157159 }
158160
159- $ expression = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
160- if ($ expression === null ) {
161+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ staticMethodReflection ->getName (), $ node ->getArgs ());
162+ if ($ expr === null ) {
161163 return new SpecifiedTypes ([], []);
162164 }
163165
164- return $ this ->typeSpecifier ->specifyTypesInCondition (
166+ $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
165167 $ scope ,
166- $ expression ,
167- TypeSpecifierContext::createTruthy ()
168+ $ expr ,
169+ TypeSpecifierContext::createTruthy (),
170+ $ rootExpr
168171 );
172+
173+ return $ this ->specifyRootExprIfSet ($ rootExpr , $ specifiedTypes );
169174 }
170175
171176 /**
172177 * @param Arg[] $args
178+ * @return array{?Expr, ?Expr}
173179 */
174180 private static function createExpression (
175181 Scope $ scope ,
176182 string $ name ,
177183 array $ args
178- ): ? Expr
184+ ): array
179185 {
180186 $ trimmedName = self ::trimName ($ name );
181187 $ resolvers = self ::getExpressionResolvers ();
182188 $ resolver = $ resolvers [$ trimmedName ];
183- $ expression = $ resolver ($ scope , ...$ args );
184- if ($ expression === null ) {
185- return null ;
189+
190+ $ resolverResult = $ resolver ($ scope , ...$ args );
191+ if (is_array ($ resolverResult )) {
192+ [$ expr , $ rootExpr ] = $ resolverResult ;
193+ } else {
194+ $ expr = $ resolverResult ;
195+ $ rootExpr = null ;
196+ }
197+
198+ if ($ expr === null ) {
199+ return [null , null ];
186200 }
187201
188202 if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189- $ expression = new BooleanOr (
190- $ expression ,
203+ $ expr = new BooleanOr (
204+ $ expr ,
191205 new Identical (
192206 $ args [0 ]->value ,
193207 new ConstFetch (new Name ('null ' ))
194208 )
195209 );
196210 }
197211
198- return $ expression ;
212+ return [ $ expr , $ rootExpr ] ;
199213 }
200214
201215 /**
202- * @return Closure[]
216+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203217 */
204218 private static function getExpressionResolvers (): array
205219 {
@@ -723,6 +737,38 @@ private static function getExpressionResolvers(): array
723737 );
724738 },
725739 ];
740+
741+ foreach (['contains ' , 'startsWith ' , 'endsWith ' ] as $ name ) {
742+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value , Arg $ subString ): array {
743+ if ($ scope ->getType ($ subString ->value )->isNonEmptyString ()->yes ()) {
744+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value , $ subString ]);
745+ }
746+
747+ return [self ::$ resolvers ['string ' ]($ scope , $ value ), null ];
748+ };
749+ }
750+
751+ $ assertionsResultingAtLeastInNonEmptyString = [
752+ 'startsWithLetter ' ,
753+ 'unicodeLetters ' ,
754+ 'alpha ' ,
755+ 'digits ' ,
756+ 'alnum ' ,
757+ 'lower ' ,
758+ 'upper ' ,
759+ 'uuid ' ,
760+ 'ip ' ,
761+ 'ipv4 ' ,
762+ 'ipv6 ' ,
763+ 'email ' ,
764+ 'notWhitespaceOnly ' ,
765+ ];
766+ foreach ($ assertionsResultingAtLeastInNonEmptyString as $ name ) {
767+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value ): array {
768+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value ]);
769+ };
770+ }
771+
726772 }
727773
728774 return self ::$ resolvers ;
@@ -790,15 +836,16 @@ private function handleAll(
790836 {
791837 $ args = $ node ->getArgs ();
792838 $ args [0 ] = new Arg (new ArrayDimFetch ($ args [0 ]->value , new LNumber (0 )));
793- $ expression = self ::createExpression ($ scope , $ methodName , $ args );
794- if ($ expression === null ) {
839+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ methodName , $ args );
840+ if ($ expr === null ) {
795841 return new SpecifiedTypes ();
796842 }
797843
798844 $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799845 $ scope ,
800- $ expression ,
801- TypeSpecifierContext::createTruthy ()
846+ $ expr ,
847+ TypeSpecifierContext::createTruthy (),
848+ $ rootExpr
802849 );
803850
804851 $ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +864,8 @@ private function handleAll(
817864 $ node ->getArgs ()[0 ]->value ,
818865 static function () use ($ type ): Type {
819866 return $ type ;
820- }
867+ },
868+ $ rootExpr
821869 );
822870 }
823871
@@ -827,7 +875,8 @@ static function () use ($type): Type {
827875 private function arrayOrIterable (
828876 Scope $ scope ,
829877 Expr $ expr ,
830- Closure $ typeCallback
878+ Closure $ typeCallback ,
879+ ?Expr $ rootExpr = null
831880 ): SpecifiedTypes
832881 {
833882 $ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -854,13 +903,16 @@ private function arrayOrIterable(
854903 return new SpecifiedTypes ([], []);
855904 }
856905
857- return $ this ->typeSpecifier ->create (
906+ $ specifiedTypes = $ this ->typeSpecifier ->create (
858907 $ expr ,
859908 $ specifiedType ,
860909 TypeSpecifierContext::createTruthy (),
861910 false ,
862- $ scope
911+ $ scope ,
912+ $ rootExpr
863913 );
914+
915+ return $ this ->specifyRootExprIfSet ($ rootExpr , $ specifiedTypes );
864916 }
865917
866918 /**
@@ -900,4 +952,41 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900952 return self ::implodeExpr ($ resolvers , BooleanOr::class);
901953 }
902954
955+ /**
956+ * @param Arg[] $args
957+ * @return array{Expr, Expr}
958+ */
959+ private static function createIsNonEmptyStringAndSomethingExprPair (array $ args ): array
960+ {
961+ $ expr = new BooleanAnd (
962+ new FuncCall (
963+ new Name ('is_string ' ),
964+ [$ args [0 ]]
965+ ),
966+ new NotIdentical (
967+ $ args [0 ]->value ,
968+ new String_ ('' )
969+ )
970+ );
971+
972+ $ rootExpr = new BooleanAnd (
973+ $ expr ,
974+ new FuncCall (new Name ('FAUX_FUNCTION ' ), $ args )
975+ );
976+
977+ return [$ expr , $ rootExpr ];
978+ }
979+
980+ private function specifyRootExprIfSet (?Expr $ rootExpr , SpecifiedTypes $ specifiedTypes ): SpecifiedTypes
981+ {
982+ if ($ rootExpr === null ) {
983+ return $ specifiedTypes ;
984+ }
985+
986+ // Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true
987+ return $ specifiedTypes ->unionWith (
988+ $ this ->typeSpecifier ->create ($ rootExpr , new ConstantBooleanType (true ), TypeSpecifierContext::createTruthy ())
989+ );
990+ }
991+
903992}
0 commit comments