Skip to content

Commit b28230b

Browse files
authored
Performance Improvements and Strict Invariance on Return type by default (#62)
* Performance Improvements and Strict Invariance on Return type by default * fix failing test
1 parent 73ee351 commit b28230b

19 files changed

Lines changed: 1780 additions & 792 deletions

src/Internal/Ast/ContractVisitor.php

Lines changed: 312 additions & 114 deletions
Large diffs are not rendered by default.

src/Internal/Ast/FunctionContractInjector.php

Lines changed: 200 additions & 187 deletions
Large diffs are not rendered by default.

src/Internal/Ast/NodeBuilder.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,30 @@ public static function createPropertyCheckCall(Node\Expr $valueExpr, Node\Expr $
2424
);
2525
}
2626

27-
public static function createVariableCheckCall(Node\Expr $valueExpr, string $typeString, string $varName): Node\Expr\FuncCall
28-
{
27+
public static function createVariableCheckCall(
28+
Node\Expr $valueExpr,
29+
string $typeString,
30+
string $varName,
31+
?Node\Expr $callerExpr = null,
32+
?Node\Expr $thisArg = null
33+
): Node\Expr\FuncCall {
34+
$args = [
35+
new Node\Arg($valueExpr),
36+
new Node\Arg(new Node\Scalar\String_($typeString)),
37+
new Node\Arg(new Node\Scalar\String_($varName)),
38+
new Node\Arg(new Node\Scalar\MagicConst\File()),
39+
];
40+
41+
if ($callerExpr !== null) {
42+
$args[] = new Node\Arg($callerExpr);
43+
if ($thisArg !== null) {
44+
$args[] = new Node\Arg($thisArg);
45+
}
46+
}
47+
2948
return new Node\Expr\FuncCall(
3049
new Node\Name('\TypePHP\Internal\RuntimeTypeChecker::checkVariable'),
31-
[
32-
new Node\Arg($valueExpr),
33-
new Node\Arg(new Node\Scalar\String_($typeString)),
34-
new Node\Arg(new Node\Scalar\String_($varName)),
35-
new Node\Arg(new Node\Scalar\MagicConst\File()),
36-
]
50+
$args
3751
);
3852
}
3953

src/Internal/Checker/InlineChecker.php

Lines changed: 69 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)