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,69 @@ 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+ if ($ rootExpr !== null ) {
174+ // Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true
175+ $ specifiedTypes = $ specifiedTypes ->unionWith (
176+ $ this ->typeSpecifier ->create ($ rootExpr , new ConstantBooleanType (true ), TypeSpecifierContext::createTruthy ())
177+ );
178+ }
179+
180+ return $ specifiedTypes ;
169181 }
170182
171183 /**
172184 * @param Arg[] $args
185+ * @return array{?Expr, ?Expr}
173186 */
174187 private static function createExpression (
175188 Scope $ scope ,
176189 string $ name ,
177190 array $ args
178- ): ? Expr
191+ ): array
179192 {
180193 $ trimmedName = self ::trimName ($ name );
181194 $ resolvers = self ::getExpressionResolvers ();
182195 $ resolver = $ resolvers [$ trimmedName ];
183- $ expression = $ resolver ($ scope , ...$ args );
184- if ($ expression === null ) {
185- return null ;
196+
197+ $ resolverResult = $ resolver ($ scope , ...$ args );
198+ if (is_array ($ resolverResult )) {
199+ [$ expr , $ rootExpr ] = $ resolverResult ;
200+ } else {
201+ $ expr = $ resolverResult ;
202+ $ rootExpr = null ;
203+ }
204+
205+ if ($ expr === null ) {
206+ return [null , null ];
186207 }
187208
188209 if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189- $ expression = new BooleanOr (
190- $ expression ,
210+ $ expr = new BooleanOr (
211+ $ expr ,
191212 new Identical (
192213 $ args [0 ]->value ,
193214 new ConstFetch (new Name ('null ' ))
194215 )
195216 );
196217 }
197218
198- return $ expression ;
219+ return [ $ expr , $ rootExpr ] ;
199220 }
200221
201222 /**
202- * @return Closure[]
223+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203224 */
204225 private static function getExpressionResolvers (): array
205226 {
@@ -723,6 +744,38 @@ private static function getExpressionResolvers(): array
723744 );
724745 },
725746 ];
747+
748+ foreach (['contains ' , 'startsWith ' , 'endsWith ' ] as $ name ) {
749+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value , Arg $ subString ): array {
750+ if ($ scope ->getType ($ subString ->value )->isNonEmptyString ()->yes ()) {
751+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value , $ subString ]);
752+ }
753+
754+ return [self ::$ resolvers ['string ' ]($ scope , $ value ), null ];
755+ };
756+ }
757+
758+ $ assertionsResultingAtLeastInNonEmptyString = [
759+ 'startsWithLetter ' ,
760+ 'unicodeLetters ' ,
761+ 'alpha ' ,
762+ 'digits ' ,
763+ 'alnum ' ,
764+ 'lower ' ,
765+ 'upper ' ,
766+ 'uuid ' ,
767+ 'ip ' ,
768+ 'ipv4 ' ,
769+ 'ipv6 ' ,
770+ 'email ' ,
771+ 'notWhitespaceOnly ' ,
772+ ];
773+ foreach ($ assertionsResultingAtLeastInNonEmptyString as $ name ) {
774+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value ): array {
775+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value ]);
776+ };
777+ }
778+
726779 }
727780
728781 return self ::$ resolvers ;
@@ -790,15 +843,16 @@ private function handleAll(
790843 {
791844 $ args = $ node ->getArgs ();
792845 $ args [0 ] = new Arg (new ArrayDimFetch ($ args [0 ]->value , new LNumber (0 )));
793- $ expression = self ::createExpression ($ scope , $ methodName , $ args );
794- if ($ expression === null ) {
846+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ methodName , $ args );
847+ if ($ expr === null ) {
795848 return new SpecifiedTypes ();
796849 }
797850
798851 $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799852 $ scope ,
800- $ expression ,
801- TypeSpecifierContext::createTruthy ()
853+ $ expr ,
854+ TypeSpecifierContext::createTruthy (),
855+ $ rootExpr
802856 );
803857
804858 $ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +871,8 @@ private function handleAll(
817871 $ node ->getArgs ()[0 ]->value ,
818872 static function () use ($ type ): Type {
819873 return $ type ;
820- }
874+ },
875+ $ rootExpr
821876 );
822877 }
823878
@@ -827,7 +882,8 @@ static function () use ($type): Type {
827882 private function arrayOrIterable (
828883 Scope $ scope ,
829884 Expr $ expr ,
830- Closure $ typeCallback
885+ Closure $ typeCallback ,
886+ ?Expr $ rootExpr = null
831887 ): SpecifiedTypes
832888 {
833889 $ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -854,13 +910,23 @@ private function arrayOrIterable(
854910 return new SpecifiedTypes ([], []);
855911 }
856912
857- return $ this ->typeSpecifier ->create (
913+ $ specifiedTypes = $ this ->typeSpecifier ->create (
858914 $ expr ,
859915 $ specifiedType ,
860916 TypeSpecifierContext::createTruthy (),
861917 false ,
862- $ scope
918+ $ scope ,
919+ $ rootExpr
863920 );
921+
922+ if ($ rootExpr !== null ) {
923+ $ specifiedTypes = $ specifiedTypes ->unionWith (
924+ // Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true
925+ $ this ->typeSpecifier ->create ($ rootExpr , new ConstantBooleanType (true ), TypeSpecifierContext::createTruthy ())
926+ );
927+ }
928+
929+ return $ specifiedTypes ;
864930 }
865931
866932 /**
@@ -900,4 +966,29 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900966 return self ::implodeExpr ($ resolvers , BooleanOr::class);
901967 }
902968
969+ /**
970+ * @param Arg[] $args
971+ * @return array{Expr, Expr}
972+ */
973+ private static function createIsNonEmptyStringAndSomethingExprPair (array $ args ): array
974+ {
975+ $ expr = new BooleanAnd (
976+ new FuncCall (
977+ new Name ('is_string ' ),
978+ [$ args [0 ]]
979+ ),
980+ new NotIdentical (
981+ $ args [0 ]->value ,
982+ new String_ ('' )
983+ )
984+ );
985+
986+ $ rootExpr = new BooleanAnd (
987+ $ expr ,
988+ new FuncCall (new Name ('FAUX_FUNCTION ' ), $ args )
989+ );
990+
991+ return [$ expr , $ rootExpr ];
992+ }
993+
903994}
0 commit comments