33
44namespace TheCodingMachine \Safe \PHPStan \Type \Php ;
55
6- use PhpParser \Node \Arg ;
76use PhpParser \Node \Expr \FuncCall ;
87use PHPStan \Analyser \Scope ;
98use PHPStan \Reflection \FunctionReflection ;
109use PHPStan \Reflection \ParametersAcceptorSelector ;
10+ use PHPStan \ShouldNotHappenException ;
11+ use PHPStan \Type \Accessory \AccessoryLowercaseStringType ;
12+ use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
13+ use PHPStan \Type \Accessory \AccessoryNonFalsyStringType ;
14+ use PHPStan \Type \Accessory \AccessoryUppercaseStringType ;
1115use PHPStan \Type \ArrayType ;
1216use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
17+ use PHPStan \Type \IntersectionType ;
1318use PHPStan \Type \MixedType ;
1419use PHPStan \Type \StringType ;
1520use PHPStan \Type \Type ;
1621use PHPStan \Type \TypeCombinator ;
1722use PHPStan \Type \TypeUtils ;
23+ use function array_key_exists ;
24+ use function count ;
25+ use function in_array ;
1826
1927/**
2028 * This file has been copy-pasted from PHPStan's source code but with isFunctionSupported changed.
2432class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2533{
2634
27- /** @var array<string, int> The function name associated with the typed argument position used to type the return */
28- private array $ functions = [
35+ private const FUNCTIONS_SUBJECT_POSITION = [
2936 'Safe\preg_replace ' => 2 ,
3037 'Safe\preg_replace_callback ' => 2 ,
3138 'Safe\preg_replace_callback_array ' => 1 ,
39+ /*
40+ 'str_replace' => 2,
41+ 'str_ireplace' => 2,
42+ 'substr_replace' => 0,
43+ 'strtr' => 0,
44+ */
45+ ];
46+
47+ private const FUNCTIONS_REPLACE_POSITION = [
48+ 'preg_replace ' => 1 ,
49+ 'str_replace ' => 1 ,
50+ 'str_ireplace ' => 1 ,
51+ 'substr_replace ' => 1 ,
52+ 'strtr ' => 2 ,
3253 ];
3354
3455 public function isFunctionSupported (FunctionReflection $ functionReflection ): bool
3556 {
36- return array_key_exists ($ functionReflection ->getName (), $ this -> functions );
57+ return array_key_exists ($ functionReflection ->getName (), self :: FUNCTIONS_SUBJECT_POSITION );
3758 }
3859
3960 public function getTypeFromFunctionCall (
4061 FunctionReflection $ functionReflection ,
4162 FuncCall $ functionCall ,
42- Scope $ scope
43- ): Type {
63+ Scope $ scope ,
64+ ): ?Type
65+ {
4466 $ type = $ this ->getPreliminarilyResolvedTypeFromFunctionCall ($ functionReflection , $ functionCall , $ scope );
4567
46- $ possibleTypes = ParametersAcceptorSelector::selectFromArgs (
47- $ scope ,
48- $ functionCall ->getArgs (),
49- $ functionReflection ->getVariants ()
50- )
51- ->getReturnType ();
52-
53- if (TypeCombinator::containsNull ($ possibleTypes )) {
68+ if ($ type !== null && $ this ->canReturnNull ($ functionReflection , $ functionCall , $ scope )) {
5469 $ type = TypeCombinator::addNull ($ type );
5570 }
5671
@@ -60,36 +75,155 @@ public function getTypeFromFunctionCall(
6075 private function getPreliminarilyResolvedTypeFromFunctionCall (
6176 FunctionReflection $ functionReflection ,
6277 FuncCall $ functionCall ,
63- Scope $ scope
64- ): Type {
65- $ argumentPosition = $ this -> functions [ $ functionReflection -> getName ()];
66-
78+ Scope $ scope,
79+ ): ? Type
80+ {
81+ $ subjectArgumentType = $ this -> getSubjectType ( $ functionReflection , $ functionCall , $ scope );
6782 $ args = $ functionCall ->getArgs ();
68- $ variants = $ functionReflection ->getVariants ();
69- $ defaultReturnType = ParametersAcceptorSelector::selectFromArgs ($ scope , $ args , $ variants )
70- ->getReturnType ();
7183
72- if (count ( $ args ) <= $ argumentPosition ) {
73- return $ defaultReturnType ;
84+ if ($ subjectArgumentType === null ) {
85+ return null ;
7486 }
7587
76- $ subjectArgument = $ args [$ argumentPosition ];
77- $ subjectArgumentType = $ scope ->getType ($ subjectArgument ->value );
78- $ mixedType = new MixedType ();
79- if ($ subjectArgumentType ->isSuperTypeOf ($ mixedType )->yes ()) {
88+ if ($ subjectArgumentType instanceof MixedType) {
89+ $ defaultReturnType = ParametersAcceptorSelector::selectFromArgs (
90+ $ scope ,
91+ $ args ,
92+ $ functionReflection ->getVariants (),
93+ )->getReturnType ();
94+
8095 return TypeUtils::toBenevolentUnion ($ defaultReturnType );
8196 }
8297
83- $ stringType = new StringType ();
84- if ($ stringType ->isSuperTypeOf ($ subjectArgumentType )->yes ()) {
85- return $ stringType ;
98+ $ replaceArgumentType = null ;
99+ if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
100+ $ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
101+
102+ if (count ($ args ) > $ replaceArgumentPosition ) {
103+ $ replaceArgumentType = $ scope ->getType ($ args [$ replaceArgumentPosition ]->value );
104+ if ($ replaceArgumentType ->isArray ()->yes ()) {
105+ $ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
106+ }
107+ } elseif ($ functionReflection ->getName () === 'strtr ' && isset ($ functionCall ->getArgs ()[1 ])) {
108+ // `strtr` has two signatures: `strtr($string1, $string2, $string3)` and `strtr($string1, $array)`
109+ $ secondArgumentType = TypeCombinator::intersect (
110+ new ArrayType (new MixedType (), new MixedType ()),
111+ $ scope ->getType ($ functionCall ->getArgs ()[1 ]->value ),
112+ );
113+ $ replaceArgumentType = $ secondArgumentType ->getIterableValueType ();
114+ }
86115 }
87116
88- $ arrayType = new ArrayType ($ mixedType , $ mixedType );
89- if ($ arrayType ->isSuperTypeOf ($ subjectArgumentType )->yes ()) {
90- return $ arrayType ;
117+ $ result = [];
118+
119+ if ($ subjectArgumentType ->isString ()->yes ()) {
120+ $ stringArgumentType = $ subjectArgumentType ;
121+ } else {
122+ $ stringArgumentType = TypeCombinator::intersect (new StringType (), $ subjectArgumentType );
123+ }
124+ if ($ stringArgumentType ->isString ()->yes ()) {
125+ $ result [] = $ this ->getReplaceType ($ stringArgumentType , $ replaceArgumentType );
91126 }
92127
93- return $ defaultReturnType ;
128+ if ($ subjectArgumentType ->isArray ()->yes ()) {
129+ $ arrayArgumentType = $ subjectArgumentType ;
130+ } else {
131+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
132+ }
133+ if ($ arrayArgumentType ->isArray ()->yes ()) {
134+ $ keyShouldBeOptional = in_array (
135+ $ functionReflection ->getName (),
136+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
137+ true ,
138+ );
139+
140+ $ mapped = $ arrayArgumentType ->mapValueType (
141+ fn (Type $ value ): Type => $ this ->getReplaceType ($ value , $ replaceArgumentType ),
142+ );
143+ if ($ keyShouldBeOptional ) {
144+ $ mapped = $ mapped ->makeAllArrayKeysOptional ();
145+ }
146+
147+ $ result [] = $ mapped ;
148+ }
149+
150+ return TypeCombinator::union (...$ result );
94151 }
152+
153+ private function getReplaceType (
154+ Type $ subjectArgumentType ,
155+ ?Type $ replaceArgumentType ,
156+ ): Type
157+ {
158+ if ($ replaceArgumentType === null ) {
159+ return new StringType ();
160+ }
161+
162+ $ accessories = [];
163+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
164+ $ accessories [] = new AccessoryNonFalsyStringType ();
165+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
166+ $ accessories [] = new AccessoryNonEmptyStringType ();
167+ }
168+
169+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
170+ $ accessories [] = new AccessoryLowercaseStringType ();
171+ }
172+
173+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
174+ $ accessories [] = new AccessoryUppercaseStringType ();
175+ }
176+
177+ if (count ($ accessories ) > 0 ) {
178+ $ accessories [] = new StringType ();
179+ return new IntersectionType ($ accessories );
180+ }
181+
182+ return new StringType ();
183+ }
184+
185+ private function getSubjectType (
186+ FunctionReflection $ functionReflection ,
187+ FuncCall $ functionCall ,
188+ Scope $ scope ,
189+ ): ?Type
190+ {
191+ if (!array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_SUBJECT_POSITION )) {
192+ throw new ShouldNotHappenException ();
193+ }
194+
195+ $ argumentPosition = self ::FUNCTIONS_SUBJECT_POSITION [$ functionReflection ->getName ()];
196+ $ args = $ functionCall ->getArgs ();
197+ if (count ($ args ) <= $ argumentPosition ) {
198+ return null ;
199+ }
200+ return $ scope ->getType ($ args [$ argumentPosition ]->value );
201+ }
202+
203+ private function canReturnNull (
204+ FunctionReflection $ functionReflection ,
205+ FuncCall $ functionCall ,
206+ Scope $ scope ,
207+ ): bool
208+ {
209+ $ args = $ functionCall ->getArgs ();
210+ if (
211+ in_array ($ functionReflection ->getName (), ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ], true )
212+ && count ($ args ) > 0
213+ ) {
214+ $ subjectArgumentType = $ this ->getSubjectType ($ functionReflection , $ functionCall , $ scope );
215+
216+ if (
217+ $ subjectArgumentType !== null
218+ && $ subjectArgumentType ->isArray ()->yes ()
219+ ) {
220+ return false ;
221+ }
222+
223+ return true ;
224+ }
225+
226+ return false ;
227+ }
228+
95229}
0 commit comments