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,68 @@ 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
+ $ specifiedTypes = $ specifiedTypes ->unionWith (
175
+ $ this ->typeSpecifier ->create ($ rootExpr , new ConstantBooleanType (true ), TypeSpecifierContext::createTruthy ())
176
+ );
177
+ }
178
+
179
+ return $ specifiedTypes ;
169
180
}
170
181
171
182
/**
172
183
* @param Arg[] $args
184
+ * @return array{?Expr, ?Expr}
173
185
*/
174
186
private static function createExpression (
175
187
Scope $ scope ,
176
188
string $ name ,
177
189
array $ args
178
- ): ? Expr
190
+ ): array
179
191
{
180
192
$ trimmedName = self ::trimName ($ name );
181
193
$ resolvers = self ::getExpressionResolvers ();
182
194
$ resolver = $ resolvers [$ trimmedName ];
183
- $ expression = $ resolver ($ scope , ...$ args );
184
- if ($ expression === null ) {
185
- return null ;
195
+
196
+ $ resolverResult = $ resolver ($ scope , ...$ args );
197
+ if (is_array ($ resolverResult )) {
198
+ [$ expr , $ rootExpr ] = $ resolverResult ;
199
+ } else {
200
+ $ expr = $ resolverResult ;
201
+ $ rootExpr = null ;
202
+ }
203
+
204
+ if ($ expr === null ) {
205
+ return [null , null ];
186
206
}
187
207
188
208
if (substr ($ name , 0 , 6 ) === 'nullOr ' ) {
189
- $ expression = new BooleanOr (
190
- $ expression ,
209
+ $ expr = new BooleanOr (
210
+ $ expr ,
191
211
new Identical (
192
212
$ args [0 ]->value ,
193
213
new ConstFetch (new Name ('null ' ))
194
214
)
195
215
);
196
216
}
197
217
198
- return $ expression ;
218
+ return [ $ expr , $ rootExpr ] ;
199
219
}
200
220
201
221
/**
202
- * @return Closure[]
222
+ * @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203
223
*/
204
224
private static function getExpressionResolvers (): array
205
225
{
@@ -723,6 +743,38 @@ private static function getExpressionResolvers(): array
723
743
);
724
744
},
725
745
];
746
+
747
+ foreach (['contains ' , 'startsWith ' , 'endsWith ' ] as $ name ) {
748
+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value , Arg $ subString ): array {
749
+ if ($ scope ->getType ($ subString ->value )->isNonEmptyString ()->yes ()) {
750
+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value , $ subString ]);
751
+ }
752
+
753
+ return [self ::$ resolvers ['string ' ]($ scope , $ value ), null ];
754
+ };
755
+ }
756
+
757
+ $ assertionsResultingAtLeastInNonEmptyString = [
758
+ 'startsWithLetter ' ,
759
+ 'unicodeLetters ' ,
760
+ 'alpha ' ,
761
+ 'digits ' ,
762
+ 'alnum ' ,
763
+ 'lower ' ,
764
+ 'upper ' ,
765
+ 'uuid ' ,
766
+ 'ip ' ,
767
+ 'ipv4 ' ,
768
+ 'ipv6 ' ,
769
+ 'email ' ,
770
+ 'notWhitespaceOnly ' ,
771
+ ];
772
+ foreach ($ assertionsResultingAtLeastInNonEmptyString as $ name ) {
773
+ self ::$ resolvers [$ name ] = static function (Scope $ scope , Arg $ value ): array {
774
+ return self ::createIsNonEmptyStringAndSomethingExprPair ([$ value ]);
775
+ };
776
+ }
777
+
726
778
}
727
779
728
780
return self ::$ resolvers ;
@@ -790,15 +842,16 @@ private function handleAll(
790
842
{
791
843
$ args = $ node ->getArgs ();
792
844
$ args [0 ] = new Arg (new ArrayDimFetch ($ args [0 ]->value , new LNumber (0 )));
793
- $ expression = self ::createExpression ($ scope , $ methodName , $ args );
794
- if ($ expression === null ) {
845
+ [ $ expr , $ rootExpr ] = self ::createExpression ($ scope , $ methodName , $ args );
846
+ if ($ expr === null ) {
795
847
return new SpecifiedTypes ();
796
848
}
797
849
798
850
$ specifiedTypes = $ this ->typeSpecifier ->specifyTypesInCondition (
799
851
$ scope ,
800
- $ expression ,
801
- TypeSpecifierContext::createTruthy ()
852
+ $ expr ,
853
+ TypeSpecifierContext::createTruthy (),
854
+ $ rootExpr
802
855
);
803
856
804
857
$ sureNotTypes = $ specifiedTypes ->getSureNotTypes ();
@@ -817,7 +870,8 @@ private function handleAll(
817
870
$ node ->getArgs ()[0 ]->value ,
818
871
static function () use ($ type ): Type {
819
872
return $ type ;
820
- }
873
+ },
874
+ $ rootExpr
821
875
);
822
876
}
823
877
@@ -827,7 +881,8 @@ static function () use ($type): Type {
827
881
private function arrayOrIterable (
828
882
Scope $ scope ,
829
883
Expr $ expr ,
830
- Closure $ typeCallback
884
+ Closure $ typeCallback ,
885
+ ?Expr $ rootExpr = null
831
886
): SpecifiedTypes
832
887
{
833
888
$ currentType = TypeCombinator::intersect ($ scope ->getType ($ expr ), new IterableType (new MixedType (), new MixedType ()));
@@ -859,7 +914,8 @@ private function arrayOrIterable(
859
914
$ specifiedType ,
860
915
TypeSpecifierContext::createTruthy (),
861
916
false ,
862
- $ scope
917
+ $ scope ,
918
+ $ rootExpr
863
919
);
864
920
}
865
921
@@ -900,4 +956,29 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900
956
return self ::implodeExpr ($ resolvers , BooleanOr::class);
901
957
}
902
958
959
+ /**
960
+ * @param Arg[] $args
961
+ * @return array{Expr, Expr}
962
+ */
963
+ private static function createIsNonEmptyStringAndSomethingExprPair (array $ args ): array
964
+ {
965
+ $ expr = new BooleanAnd (
966
+ new FuncCall (
967
+ new Name ('is_string ' ),
968
+ [$ args [0 ]]
969
+ ),
970
+ new NotIdentical (
971
+ $ args [0 ]->value ,
972
+ new String_ ('' )
973
+ )
974
+ );
975
+
976
+ $ rootExpr = new BooleanAnd (
977
+ $ expr ,
978
+ new FuncCall (new Name ('FAUX_FUNCTION ' ), $ args )
979
+ );
980
+
981
+ return [$ expr , $ rootExpr ];
982
+ }
983
+
903
984
}
0 commit comments