39
39
use PHPStan \Type \ArrayType ;
40
40
use PHPStan \Type \Constant \ConstantArrayType ;
41
41
use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
42
+ use PHPStan \Type \Constant \ConstantBooleanType ;
42
43
use PHPStan \Type \Constant \ConstantStringType ;
43
44
use PHPStan \Type \IterableType ;
44
45
use PHPStan \Type \MixedType ;
58
59
use function array_reduce ;
59
60
use function array_shift ;
60
61
use function count ;
62
+ use function is_array ;
61
63
use function lcfirst ;
62
64
use function substr ;
63
65
@@ -104,7 +106,7 @@ public function isStaticMethodSupported(
104
106
}
105
107
106
108
$ resolver = $ resolvers [$ trimmedName ];
107
- $ resolverReflection = new ReflectionObject ($ resolver );
109
+ $ resolverReflection = new ReflectionObject (Closure:: fromCallable ( $ resolver) );
108
110
109
111
return count ($ node ->getArgs ()) >= count ($ resolverReflection ->getMethod ('__invoke ' )->getParameters ()) - 1 ;
110
112
}
@@ -156,50 +158,69 @@ static function (Type $type) {
156
158
);
157
159
}
158
160
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 ) {
161
163
return new SpecifiedTypes ([], []);
162
164
}
163
165
164
- return $ this ->typeSpecifier ->specifyTypesInCondition (
166
+ $ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
165
167
$ scope ,
166
- $ expression ,
167
- TypeSpecifierContext::createTruthy ()
168
+ $ expr ,
169
+ TypeSpecifierContext::createTruthy (),
170
+ $ rootExpr
168
171
);
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 ;
169
181
}
170
182
171
183
/**
172
184
* @param Arg[] $args
185
+ * @return array{?Expr, ?Expr}
173
186
*/
174
187
private static function createExpression (
175
188
Scope $ scope ,
176
189
string $ name ,
177
190
array $ args
178
- ): ? Expr
191
+ ): array
179
192
{
180
193
$ trimmedName = self ::trimName ($ name );
181
194
$ resolvers = self ::getExpressionResolvers ();
182
195
$ 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 ];
186
207
}
187
208
188
209
if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189
- $ expression = new BooleanOr (
190
- $ expression ,
210
+ $ expr = new BooleanOr (
211
+ $ expr ,
191
212
new Identical (
192
213
$ args [0 ]->value ,
193
214
new ConstFetch (new Name ('null ' ))
194
215
)
195
216
);
196
217
}
197
218
198
- return $ expression ;
219
+ return [ $ expr , $ rootExpr ] ;
199
220
}
200
221
201
222
/**
202
- * @return Closure[]
223
+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203
224
*/
204
225
private static function getExpressionResolvers (): array
205
226
{
@@ -723,6 +744,38 @@ private static function getExpressionResolvers(): array
723
744
);
724
745
},
725
746
];
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
+
726
779
}
727
780
728
781
return self ::$ resolvers ;
@@ -790,15 +843,16 @@ private function handleAll(
790
843
{
791
844
$ args = $ node ->getArgs ();
792
845
$ 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 ) {
795
848
return new SpecifiedTypes ();
796
849
}
797
850
798
851
$ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799
852
$ scope ,
800
- $ expression ,
801
- TypeSpecifierContext::createTruthy ()
853
+ $ expr ,
854
+ TypeSpecifierContext::createTruthy (),
855
+ $ rootExpr
802
856
);
803
857
804
858
$ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +871,8 @@ private function handleAll(
817
871
$ node ->getArgs ()[0 ]->value ,
818
872
static function () use ($ type ): Type {
819
873
return $ type ;
820
- }
874
+ },
875
+ $ rootExpr
821
876
);
822
877
}
823
878
@@ -827,7 +882,8 @@ static function () use ($type): Type {
827
882
private function arrayOrIterable (
828
883
Scope $ scope ,
829
884
Expr $ expr ,
830
- Closure $ typeCallback
885
+ Closure $ typeCallback ,
886
+ ?Expr $ rootExpr = null
831
887
): SpecifiedTypes
832
888
{
833
889
$ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -854,13 +910,23 @@ private function arrayOrIterable(
854
910
return new SpecifiedTypes ([], []);
855
911
}
856
912
857
- return $ this ->typeSpecifier ->create (
913
+ $ specifiedTypes = $ this ->typeSpecifier ->create (
858
914
$ expr ,
859
915
$ specifiedType ,
860
916
TypeSpecifierContext::createTruthy (),
861
917
false ,
862
- $ scope
918
+ $ scope ,
919
+ $ rootExpr
863
920
);
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 ;
864
930
}
865
931
866
932
/**
@@ -900,4 +966,29 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900
966
return self ::implodeExpr ($ resolvers , BooleanOr::class);
901
967
}
902
968
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
+
903
994
}
0 commit comments