@@ -48,19 +48,27 @@ final class InlineChecker
4848 private static array $ parsedTypeNodeCache = [];
4949
5050 /**
51- * Memoized cache for PHP internal function determinations .
51+ * In-memory cache for resolved class contexts with static bounds .
5252 *
53- * @var array<string, bool >
53+ * @var array<string, TypeNode >
5454 */
55- private static array $ internalFunctionsCache = [];
55+ private static array $ resolvedClassContextCache = [];
56+
57+ /**
58+ * In-memory cache for properties known to have no DocBlock annotations.
59+ *
60+ * @var array<string, true>
61+ */
62+ public static array $ nullPropertyCache = [];
5663
5764 /**
5865 * Resets internal type node and function caches. Useful for test isolation.
5966 */
6067 public static function reset (): void
6168 {
6269 self ::$ parsedTypeNodeCache = [];
63- self ::$ internalFunctionsCache = [];
70+ self ::$ resolvedClassContextCache = [];
71+ self ::$ nullPropertyCache = [];
6472 }
6573
6674 /**
@@ -113,8 +121,15 @@ public static function reset(): void
113121 /**
114122 * Evaluates inline variable validation dynamically based on configuration.
115123 */
116- public static function checkVariable (mixed $ value , string $ typeString , string $ varName , string $ file , TypeValidatorRegistry $ registry ): mixed
117- {
124+ public static function checkVariable (
125+ mixed $ value ,
126+ string $ typeString ,
127+ string $ varName ,
128+ string $ file ,
129+ TypeValidatorRegistry $ registry ,
130+ ?string $ caller = null ,
131+ mixed $ thisOrClass = null
132+ ): mixed {
118133 $ rawConfig = Config::get ()['inline_vars ' ] ?? [];
119134 /** @var array<string, bool> $config */
120135 $ config = \is_array ($ rawConfig ) ? $ rawConfig : [];
@@ -131,8 +146,8 @@ public static function checkVariable(mixed $value, string $typeString, string $v
131146 $ typeNode = SpecialTypeResolver::resolveForFile ($ typeNode , $ file );
132147 }
133148
134- if ($ needsContext ) {
135- $ typeNode = self ::resolveCallerContext ($ typeNode );
149+ if ($ needsContext && $ caller !== null ) {
150+ $ typeNode = self ::resolveCallerContext ($ typeNode, $ caller , $ thisOrClass );
136151 }
137152
138153 if (! self ::shouldValidateType ($ typeNode , $ config )) {
@@ -203,6 +218,13 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string
203218 return $ value ;
204219 }
205220
221+ $ className = \is_string ($ objectOrClass ) ? $ objectOrClass : \get_class ($ objectOrClass );
222+ $ cacheKey = $ className . '::$ ' . $ propName ;
223+
224+ if (isset (self ::$ nullPropertyCache [$ cacheKey ])) {
225+ return $ value ;
226+ }
227+
206228 $ rawConfig = Config::get ()['inline_vars ' ] ?? [];
207229 /** @var array<string, bool> $config */
208230 $ config = \is_array ($ rawConfig ) ? $ rawConfig : [];
@@ -211,10 +233,10 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string
211233 return $ value ;
212234 }
213235
214- $ className = \is_string ($ objectOrClass ) ? $ objectOrClass : \get_class ($ objectOrClass );
215-
216236 $ typeNode = DocblockParser::parseProperty ($ className , $ propName );
217237 if ($ typeNode === null ) {
238+ self ::$ nullPropertyCache [$ cacheKey ] = true ;
239+
218240 return $ value ;
219241 }
220242
@@ -255,113 +277,29 @@ private static function hasActiveInlineChecks(array $config): bool
255277 /**
256278 * Resolves caller class or function context and applies templates & type aliases to the AST.
257279 */
258- private static function resolveCallerContext (TypeNode $ typeNode ): TypeNode
259- {
260- $ frameInfo = self ::findCallerFrame ();
261-
262- if ($ frameInfo ['functionName ' ] !== null ) {
263- return self ::resolveFunctionContext ($ typeNode , $ frameInfo ['functionName ' ]);
264- }
265-
266- if ($ frameInfo ['className ' ] !== null ) {
267- return self ::resolveClassContext (
268- $ typeNode ,
269- $ frameInfo ['className ' ],
270- $ frameInfo ['methodName ' ],
271- $ frameInfo ['thisObj ' ]
272- );
273- }
274-
275- return $ typeNode ;
276- }
277-
278- /**
279- * Inspects the backtrace to find the nearest non-internal caller frame.
280- *
281- * @return array{className: ?string, methodName: ?string, functionName: ?string, thisObj: ?object}
282- */
283- private static function findCallerFrame (): array
280+ private static function resolveCallerContext (TypeNode $ typeNode , ?string $ caller = null , mixed $ thisOrClass = null ): TypeNode
284281 {
285- $ className = null ;
286- $ methodName = null ;
287- $ functionName = null ;
288- $ thisObj = null ;
289-
290- $ trace = debug_backtrace (DEBUG_BACKTRACE_PROVIDE_OBJECT , 15 );
291-
292- foreach ($ trace as $ frame ) {
293- $ classCandidate = $ frame ['class ' ] ?? null ;
294- $ funcCandidate = $ frame ['function ' ];
295-
296- if ($ classCandidate === 'Closure ' || $ classCandidate === 'Generator ' ) {
297- if ($ thisObj === null && isset ($ frame ['object ' ]) && ! ($ frame ['object ' ] instanceof \Closure) && ! ($ frame ['object ' ] instanceof \Generator)) {
298- $ thisObj = $ frame ['object ' ];
299- }
300-
301- continue ;
282+ if ($ caller !== null ) {
283+ if ($ caller === '' ) {
284+ return $ typeNode ;
302285 }
303286
304- if ($ funcCandidate === '{closure} ' || str_starts_with ($ funcCandidate , '{closure ' )) {
305- if ($ thisObj === null && isset ($ frame ['object ' ]) && ! ($ frame ['object ' ] instanceof \Closure) && ! ($ frame ['object ' ] instanceof \Generator)) {
306- $ thisObj = $ frame ['object ' ];
307- }
287+ if (str_contains ($ caller , ':: ' )) {
288+ [$ className , $ methodName ] = explode (':: ' , $ caller , 2 );
289+ $ thisObj = \is_object ($ thisOrClass ) ? $ thisOrClass : null ;
308290
309- continue ;
291+ return self ::resolveClassContext (
292+ $ typeNode ,
293+ $ className ,
294+ $ methodName ,
295+ $ thisObj
296+ );
310297 }
311298
312- if ($ classCandidate !== null ) {
313- if (! str_starts_with ($ classCandidate , 'TypePHP \\Internal \\' ) && ! str_starts_with ($ classCandidate , 'TypePHP \\Wrapper \\' )) {
314- $ className = $ classCandidate ;
315- $ methodName = $ funcCandidate ;
316- if ($ thisObj === null ) {
317- $ thisObj = $ frame ['object ' ] ?? null ;
318- }
319-
320- break ;
321- }
322- } else {
323- if (! str_starts_with ($ funcCandidate , 'TypePHP \\' )) {
324- if (! \in_array ($ funcCandidate , ['include ' , 'include_once ' , 'require ' , 'require_once ' , 'eval ' ], true )) {
325- if (self ::isInternalFunction ($ funcCandidate )) {
326- continue ;
327- }
328-
329- $ functionName = $ funcCandidate ;
330-
331- break ;
332- }
333- }
334- }
335- }
336-
337- return [
338- 'className ' => $ className ,
339- 'methodName ' => $ methodName ,
340- 'functionName ' => $ functionName ,
341- 'thisObj ' => $ thisObj ,
342- ];
343- }
344-
345- /**
346- * Fast check if a function name represents an internal PHP built-in function.
347- */
348- private static function isInternalFunction (string $ funcName ): bool
349- {
350- if (! \function_exists ($ funcName )) {
351- return false ;
352- }
353-
354- if (isset (self ::$ internalFunctionsCache [$ funcName ])) {
355- return self ::$ internalFunctionsCache [$ funcName ];
299+ return self ::resolveFunctionContext ($ typeNode , $ caller );
356300 }
357301
358- try {
359- $ rf = new \ReflectionFunction ($ funcName );
360-
361- return self ::$ internalFunctionsCache [$ funcName ] = $ rf ->isInternal ();
362- } catch (\ReflectionException $ e ) {
363- return self ::$ internalFunctionsCache [$ funcName ] = false ;
364- }
302+ return $ typeNode ;
365303 }
366304
367305 /**
@@ -408,21 +346,37 @@ private static function resolveClassContext(
408346 return $ typeNode ;
409347 }
410348
349+ $ cacheKey = null ;
350+ if ($ thisObj === null ) {
351+ $ cacheKey = ((string ) $ typeNode ) . '| ' . $ className . '| ' . ($ methodName ?? '' );
352+ if (isset (self ::$ resolvedClassContextCache [$ cacheKey ])) {
353+ return self ::$ resolvedClassContextCache [$ cacheKey ];
354+ }
355+ }
356+
411357 try {
412358 /** @var class-string<object> $className */
413359 $ refClass = new \ReflectionClass ($ className );
414360 $ typeNode = SpecialTypeResolver::resolve ($ typeNode , $ refClass );
415361
416362 $ classAliases = DocblockParser::parseClassAliases ($ className );
417363
418- $ targetFunc = ($ methodName !== '{closure} ' && $ methodName !== null )
364+ $ targetFunc = ($ methodName !== '{closure} ' && $ methodName !== null && ! str_starts_with ( $ methodName , ' {closure ' ) )
419365 ? $ className . ':: ' . $ methodName
420366 : $ className . '::__construct ' ;
421367
422368 $ contract = DocblockParser::parse ($ targetFunc );
423369 $ declaredTemplates = $ contract ['allTemplates ' ] ?? ($ contract ['classTemplates ' ] ?? []);
424- $ boundTemplates = TemplateManager::getBoundTemplates ($ targetFunc , $ thisObj , $ declaredTemplates );
425370
371+ if (\count ($ classAliases ) === 0 && \count ($ declaredTemplates ) === 0 ) {
372+ if ($ cacheKey !== null ) {
373+ return self ::$ resolvedClassContextCache [$ cacheKey ] = $ typeNode ;
374+ }
375+
376+ return $ typeNode ;
377+ }
378+
379+ $ boundTemplates = TemplateManager::getBoundTemplates ($ targetFunc , $ thisObj , $ declaredTemplates );
426380 $ activeBindings = [...$ classAliases , ...$ boundTemplates ];
427381
428382 if (\count ($ activeBindings ) > 0 || \count ($ declaredTemplates ) > 0 ) {
@@ -433,6 +387,10 @@ private static function resolveClassContext(
433387 // Silently continue if reflection fails
434388 }
435389
390+ if ($ cacheKey !== null ) {
391+ return self ::$ resolvedClassContextCache [$ cacheKey ] = $ typeNode ;
392+ }
393+
436394 return $ typeNode ;
437395 }
438396
0 commit comments