@@ -49,7 +49,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node):
4949
5050 $ injectedStmts = [];
5151 if ($ hasParam ) {
52- $ injectedStmts = self ::buildParamInjections ($ node ->params , $ docText , $ thisArg, $ isClassMethod );
52+ $ injectedStmts = self ::buildParamInjections ($ node ->params , $ docText , $ thisArg );
5353 }
5454
5555 if ($ hasReturn ) {
@@ -120,35 +120,37 @@ public function enterNode(Node $n): ?int
120120 private static function buildParamInjections (
121121 array $ params ,
122122 string $ docText ,
123- Node \Expr $ thisArg ,
124- bool $ isClassMethod
123+ Node \Expr $ thisArg
125124 ): array {
126- $ injectedStmts = [self ::buildSetupScopeStmt ($ thisArg )];
125+ $ injectedStmts = [self ::buildSetupScopeStmt ($ params , $ thisArg )];
127126
128- $ callableWrappers = self ::buildParamWrappers (
129- $ params ,
130- '\TypePHP\Internal\RuntimeTypeChecker::wrapCallable ' ,
131- $ thisArg ,
132- $ isClassMethod || str_contains ($ docText , 'callable ' ) || str_contains ($ docText , 'Closure ' )
133- );
134-
135- $ iterableWrappers = self ::buildParamWrappers (
136- $ params ,
137- '\TypePHP\Internal\RuntimeTypeChecker::wrapIterable ' ,
138- $ thisArg ,
139- str_contains ($ docText , 'iterable ' ) || str_contains ($ docText , 'Traversable ' ) || str_contains ($ docText , 'Generator ' ) || str_contains ($ docText , 'Iterator ' )
140- );
127+ $ callableWrappers = self ::buildCallableParamWrappers ($ params , $ docText , $ thisArg );
128+ $ iterableWrappers = self ::buildIterableParamWrappers ($ params , $ docText , $ thisArg );
141129
142130 return [...$ injectedStmts , ...$ callableWrappers , ...$ iterableWrappers ];
143131 }
144132
145- private static function buildSetupScopeStmt (Node \Expr $ thisArg ): Node \Stmt \If_
133+ /**
134+ * @param array<Node\Param> $params
135+ */
136+ private static function buildSetupScopeStmt (array $ params , Node \Expr $ thisArg ): Node \Stmt \If_
146137 {
138+ $ arrayItems = [];
139+ foreach ($ params as $ param ) {
140+ if ($ param ->var instanceof Node \Expr \Variable && \is_string ($ param ->var ->name )) {
141+ $ pName = $ param ->var ->name ;
142+ $ arrayItems [] = new Node \ArrayItem (
143+ new Node \Expr \Variable ($ pName ),
144+ new Node \Scalar \String_ ($ pName )
145+ );
146+ }
147+ }
148+
147149 $ checkCall = new Node \Expr \FuncCall (
148150 new Node \Name ('\TypePHP\Internal\RuntimeTypeChecker::setupScope ' ),
149151 [
150152 new Node \Arg (new Node \Scalar \MagicConst \Method ()),
151- new Node \Arg (new Node \Expr \FuncCall ( new Node \ Name ( ' get_defined_vars ' ) )),
153+ new Node \Arg (new Node \Expr \Array_ ( $ arrayItems )),
152154 new Node \Arg ($ thisArg ),
153155 ]
154156 );
@@ -173,21 +175,50 @@ private static function buildSetupScopeStmt(Node\Expr $thisArg): Node\Stmt\If_
173175 *
174176 * @return array<Node\Stmt>
175177 */
176- private static function buildParamWrappers (array $ params , string $ wrapperFunc , Node \Expr $ thisArg, bool $ shouldWrap ): array
178+ private static function buildCallableParamWrappers (array $ params , string $ docText , Node \Expr $ thisArg ): array
177179 {
178- if (! $ shouldWrap ) {
179- return [];
180+ $ wrappers = [];
181+ foreach ($ params as $ param ) {
182+ if (self ::isCallableCandidate ($ param , $ docText ) && $ param ->var instanceof Node \Expr \Variable && \is_string ($ param ->var ->name )) {
183+ $ paramName = $ param ->var ->name ;
184+ $ expr = new Node \Stmt \Expression (
185+ new Node \Expr \Assign (
186+ new Node \Expr \Variable ($ paramName ),
187+ new Node \Expr \FuncCall (
188+ new Node \Name ('\TypePHP\Internal\RuntimeTypeChecker::wrapCallable ' ),
189+ [
190+ new Node \Arg (new Node \Scalar \MagicConst \Method ()),
191+ new Node \Arg (new Node \Scalar \String_ ($ paramName )),
192+ new Node \Arg (new Node \Expr \Variable ($ paramName )),
193+ new Node \Arg ($ thisArg ),
194+ ]
195+ )
196+ )
197+ );
198+ $ expr ->setAttribute ('typephp_injected ' , true );
199+ $ wrappers [] = $ expr ;
200+ }
180201 }
181202
203+ return $ wrappers ;
204+ }
205+
206+ /**
207+ * @param array<Node\Param> $params
208+ *
209+ * @return array<Node\Stmt>
210+ */
211+ private static function buildIterableParamWrappers (array $ params , string $ docText , Node \Expr $ thisArg ): array
212+ {
182213 $ wrappers = [];
183214 foreach ($ params as $ param ) {
184- if ($ param ->var instanceof Node \Expr \Variable && \is_string ($ param ->var ->name )) {
215+ if (self :: isIterableCandidate ( $ param , $ docText ) && $ param ->var instanceof Node \Expr \Variable && \is_string ($ param ->var ->name )) {
185216 $ paramName = $ param ->var ->name ;
186217 $ expr = new Node \Stmt \Expression (
187218 new Node \Expr \Assign (
188219 new Node \Expr \Variable ($ paramName ),
189220 new Node \Expr \FuncCall (
190- new Node \Name ($ wrapperFunc ),
221+ new Node \Name (' \TypePHP\Internal\RuntimeTypeChecker::wrapIterable ' ),
191222 [
192223 new Node \Arg (new Node \Scalar \MagicConst \Method ()),
193224 new Node \Arg (new Node \Scalar \String_ ($ paramName )),
@@ -205,6 +236,81 @@ private static function buildParamWrappers(array $params, string $wrapperFunc, N
205236 return $ wrappers ;
206237 }
207238
239+ private static function isCallableCandidate (Node \Param $ param , string $ docText ): bool
240+ {
241+ if (
242+ str_contains ($ docText , 'callable ' )
243+ || str_contains ($ docText , 'Closure ' )
244+ || str_contains ($ docText , 'pure-callable ' )
245+ || str_contains ($ docText , 'static-closure ' )
246+ ) {
247+ return true ;
248+ }
249+
250+ if ($ param ->type instanceof Node \Identifier) {
251+ return strtolower ($ param ->type ->name ) === 'callable ' ;
252+ }
253+
254+ if ($ param ->type instanceof Node \Name) {
255+ return strtolower ($ param ->type ->getLast ()) === 'closure ' ;
256+ }
257+
258+ if ($ param ->type instanceof Node \UnionType || $ param ->type instanceof Node \IntersectionType) {
259+ foreach ($ param ->type ->types as $ t ) {
260+ if ($ t instanceof Node \Identifier && strtolower ($ t ->name ) === 'callable ' ) {
261+ return true ;
262+ }
263+ if ($ t instanceof Node \Name && strtolower ($ t ->getLast ()) === 'closure ' ) {
264+ return true ;
265+ }
266+ }
267+ }
268+
269+ return false ;
270+ }
271+
272+ private static function isIterableCandidate (Node \Param $ param , string $ docText ): bool
273+ {
274+ if (
275+ str_contains ($ docText , 'iterable ' )
276+ || str_contains ($ docText , 'Traversable ' )
277+ || str_contains ($ docText , 'Generator ' )
278+ || str_contains ($ docText , 'Iterator ' )
279+ || str_contains ($ docText , 'IteratorAggregate ' )
280+ ) {
281+ return true ;
282+ }
283+
284+ $ iterableTypes = [
285+ 'iterable ' => true ,
286+ 'traversable ' => true ,
287+ 'generator ' => true ,
288+ 'iterator ' => true ,
289+ 'iteratoraggregate ' => true ,
290+ ];
291+
292+ if ($ param ->type instanceof Node \Identifier) {
293+ return isset ($ iterableTypes [strtolower ($ param ->type ->name )]);
294+ }
295+
296+ if ($ param ->type instanceof Node \Name) {
297+ return isset ($ iterableTypes [strtolower ($ param ->type ->getLast ())]);
298+ }
299+
300+ if ($ param ->type instanceof Node \UnionType || $ param ->type instanceof Node \IntersectionType) {
301+ foreach ($ param ->type ->types as $ t ) {
302+ if ($ t instanceof Node \Identifier && isset ($ iterableTypes [strtolower ($ t ->name )])) {
303+ return true ;
304+ }
305+ if ($ t instanceof Node \Name && isset ($ iterableTypes [strtolower ($ t ->getLast ())])) {
306+ return true ;
307+ }
308+ }
309+ }
310+
311+ return false ;
312+ }
313+
208314 public static function buildTypeErrorThrowStmt (Node \Expr $ errorVar ): Node \Stmt \Expression
209315 {
210316 return new Node \Stmt \Expression (
0 commit comments