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,62 @@ 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
+ return $ this ->specifyRootExprIfSet ($ rootExpr , $ specifiedTypes );
169
174
}
170
175
171
176
/**
172
177
* @param Arg[] $args
178
+ * @return array{?Expr, ?Expr}
173
179
*/
174
180
private static function createExpression (
175
181
Scope $ scope ,
176
182
string $ name ,
177
183
array $ args
178
- ): ? Expr
184
+ ): array
179
185
{
180
186
$ trimmedName = self ::trimName ($ name );
181
187
$ resolvers = self ::getExpressionResolvers ();
182
188
$ 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 ];
186
200
}
187
201
188
202
if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189
- $ expression = new BooleanOr (
190
- $ expression ,
203
+ $ expr = new BooleanOr (
204
+ $ expr ,
191
205
new Identical (
192
206
$ args [0 ]->value ,
193
207
new ConstFetch (new Name ('null ' ))
194
208
)
195
209
);
196
210
}
197
211
198
- return $ expression ;
212
+ return [ $ expr , $ rootExpr ] ;
199
213
}
200
214
201
215
/**
202
- * @return Closure[]
216
+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203
217
*/
204
218
private static function getExpressionResolvers (): array
205
219
{
@@ -723,6 +737,38 @@ private static function getExpressionResolvers(): array
723
737
);
724
738
},
725
739
];
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
+
726
772
}
727
773
728
774
return self ::$ resolvers ;
@@ -790,15 +836,16 @@ private function handleAll(
790
836
{
791
837
$ args = $ node ->getArgs ();
792
838
$ 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 ) {
795
841
return new SpecifiedTypes ();
796
842
}
797
843
798
844
$ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799
845
$ scope ,
800
- $ expression ,
801
- TypeSpecifierContext::createTruthy ()
846
+ $ expr ,
847
+ TypeSpecifierContext::createTruthy (),
848
+ $ rootExpr
802
849
);
803
850
804
851
$ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +864,8 @@ private function handleAll(
817
864
$ node ->getArgs ()[0 ]->value ,
818
865
static function () use ($ type ): Type {
819
866
return $ type ;
820
- }
867
+ },
868
+ $ rootExpr
821
869
);
822
870
}
823
871
@@ -827,7 +875,8 @@ static function () use ($type): Type {
827
875
private function arrayOrIterable (
828
876
Scope $ scope ,
829
877
Expr $ expr ,
830
- Closure $ typeCallback
878
+ Closure $ typeCallback ,
879
+ ?Expr $ rootExpr = null
831
880
): SpecifiedTypes
832
881
{
833
882
$ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -854,13 +903,16 @@ private function arrayOrIterable(
854
903
return new SpecifiedTypes ([], []);
855
904
}
856
905
857
- return $ this ->typeSpecifier ->create (
906
+ $ specifiedTypes = $ this ->typeSpecifier ->create (
858
907
$ expr ,
859
908
$ specifiedType ,
860
909
TypeSpecifierContext::createTruthy (),
861
910
false ,
862
- $ scope
911
+ $ scope ,
912
+ $ rootExpr
863
913
);
914
+
915
+ return $ this ->specifyRootExprIfSet ($ rootExpr , $ specifiedTypes );
864
916
}
865
917
866
918
/**
@@ -900,4 +952,41 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900
952
return self ::implodeExpr ($ resolvers , BooleanOr::class);
901
953
}
902
954
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
+
903
992
}
0 commit comments