Skip to content

Commit 571f4d8

Browse files
committed
Add caching and reset functionality to ParamChecker and ReturnChecker; enhance FunctionContractInjector with improved return handling
1 parent 8e318b6 commit 571f4d8

4 files changed

Lines changed: 81 additions & 31 deletions

File tree

src/Internal/Checker/ParamChecker.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
*/
2727
final class ParamChecker
2828
{
29+
/**
30+
* Resets the effective function cache. Useful for test isolation.
31+
*/
32+
public static function reset(): void
33+
{
34+
self::$effectiveFunctionCache = [];
35+
}
36+
37+
/**
38+
* @var array<string, string>
39+
*/
40+
private static array $effectiveFunctionCache = [];
41+
2942
/**
3043
* @param array<string, mixed> $vars
3144
*/
@@ -98,23 +111,29 @@ public static function checkParams(
98111
}
99112

100113
/**
101-
* Resolves the actual runtime class name vs trait name and matches any active trait aliases.
114+
* Resolves the actual runtime class name vs trait name with $O(1)$ memoization.
102115
*/
103116
private static function resolveEffectiveFunction(string $function, object|string|null $thisOrClass, ?object $thisObj): string
104117
{
105118
if (! str_contains($function, '::')) {
106119
return $function;
107120
}
108121

122+
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : '');
123+
$cacheKey = $function . '|' . $actualClassName;
124+
125+
if (isset(self::$effectiveFunctionCache[$cacheKey])) {
126+
return self::$effectiveFunctionCache[$cacheKey];
127+
}
128+
109129
[$classOrTrait, $methodName] = explode('::', $function, 2);
110-
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : null);
111130

112-
$effectiveFunction = ($actualClassName !== null && $actualClassName !== $classOrTrait)
131+
$effectiveFunction = ($actualClassName !== '' && $actualClassName !== $classOrTrait)
113132
? $actualClassName . '::' . $methodName
114133
: $function;
115134

116135
if ($thisObj !== null) {
117-
$targetClass = $actualClassName ?? $classOrTrait;
136+
$targetClass = $actualClassName !== '' ? $actualClassName : $classOrTrait;
118137
$traitAliases = HierarchyResolver::getTraitAliases($targetClass);
119138

120139
if (\count($traitAliases) > 0) {
@@ -135,14 +154,14 @@ private static function resolveEffectiveFunction(string $function, object|string
135154
$frameFunc = $frame['function'];
136155
$frameClass = $frame['class'] ?? '';
137156
if (($frameClass === $actualClassName || $frameClass === $classOrTrait) && isset($traitAliases[$frameFunc])) {
138-
return $targetClass . '::' . $frameFunc;
157+
return self::$effectiveFunctionCache[$cacheKey] = $targetClass . '::' . $frameFunc;
139158
}
140159
}
141160
}
142161
}
143162
}
144163

145-
return $effectiveFunction;
164+
return self::$effectiveFunctionCache[$cacheKey] = $effectiveFunction;
146165
}
147166

148167
/**

src/Internal/Checker/ReturnChecker.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
*/
2727
final class ReturnChecker
2828
{
29+
/**
30+
* Resets the effective function cache. Useful for test isolation.
31+
*/
32+
public static function reset(): void
33+
{
34+
self::$effectiveFunctionCache = [];
35+
}
36+
37+
/**
38+
* @var array<string, string>
39+
*/
40+
private static array $effectiveFunctionCache = [];
41+
2942
/**
3043
* @param array<string, mixed> $vars
3144
*/
@@ -80,23 +93,29 @@ public static function checkReturn(
8093
}
8194

8295
/**
83-
* Resolves the actual runtime class name vs trait name and matches any active trait aliases.
96+
* Resolves the actual runtime class name vs trait name with $O(1)$ memoization.
8497
*/
8598
private static function resolveEffectiveFunction(string $function, object|string|null $thisOrClass, ?object $thisObj): string
8699
{
87100
if (! str_contains($function, '::')) {
88101
return $function;
89102
}
90103

104+
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : '');
105+
$cacheKey = $function . '|' . $actualClassName;
106+
107+
if (isset(self::$effectiveFunctionCache[$cacheKey])) {
108+
return self::$effectiveFunctionCache[$cacheKey];
109+
}
110+
91111
[$classOrTrait, $methodName] = explode('::', $function, 2);
92-
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : null);
93112

94-
$effectiveFunction = ($actualClassName !== null && $actualClassName !== $classOrTrait)
113+
$effectiveFunction = ($actualClassName !== '' && $actualClassName !== $classOrTrait)
95114
? $actualClassName . '::' . $methodName
96115
: $function;
97116

98117
if ($thisObj !== null) {
99-
$targetClass = $actualClassName ?? $classOrTrait;
118+
$targetClass = $actualClassName !== '' ? $actualClassName : $classOrTrait;
100119
$traitAliases = HierarchyResolver::getTraitAliases($targetClass);
101120

102121
if (\count($traitAliases) > 0) {
@@ -117,14 +136,14 @@ private static function resolveEffectiveFunction(string $function, object|string
117136
$frameFunc = $frame['function'];
118137
$frameClass = $frame['class'] ?? '';
119138
if (($frameClass === $actualClassName || $frameClass === $classOrTrait) && isset($traitAliases[$frameFunc])) {
120-
return $targetClass . '::' . $frameFunc;
139+
return self::$effectiveFunctionCache[$cacheKey] = $targetClass . '::' . $frameFunc;
121140
}
122141
}
123142
}
124143
}
125144
}
126145

127-
return $effectiveFunction;
146+
return self::$effectiveFunctionCache[$cacheKey] = $effectiveFunction;
128147
}
129148

130149
/**

src/Internal/Config.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use TypePHP\Contract\HierarchyResolver;
1010
use TypePHP\Extension\ExtensionInterface;
1111
use TypePHP\Extension\ExtensionManager;
12+
use TypePHP\Internal\Checker\ParamChecker;
13+
use TypePHP\Internal\Checker\ReturnChecker;
1214
use TypePHP\Resolver\TemplateManager;
1315

1416
/**
@@ -210,6 +212,8 @@ public static function set(array $config): void
210212
self::syncFlags($mergedConfig);
211213

212214
ContractParser::reset();
215+
ParamChecker::reset();
216+
ReturnChecker::reset();
213217
FileFilter::reset();
214218
PathMatcher::reset();
215219
StreamWrapper::reset();
@@ -230,6 +234,8 @@ public static function reset(): void
230234
self::$respectIgnoreTags = true;
231235

232236
ContractParser::reset();
237+
ParamChecker::reset();
238+
ReturnChecker::reset();
233239
TemplateManager::reset();
234240
HierarchyResolver::reset();
235241
FileFilter::reset();

src/Internal/Visitor/FunctionContractInjector.php

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node):
3434
}
3535

3636
$methodName = $isClassMethod ? strtolower($node->name->toString()) : '';
37-
$isMagicLifecycle = $isClassMethod && \in_array($methodName, ['__construct', '__destruct', '__clone'], strict: true);
37+
$isMagicLifecycle = $isClassMethod && \in_array($methodName, ['__construct', '__destruct', '__clone'], true);
3838

3939
$hasParam = $isClassMethod || str_contains($docText, '@param') || str_contains($docText, '@phpstan-param') || str_contains($docText, '@psalm-param');
4040
$hasReturn = ! $isMagicLifecycle && ($isClassMethod || str_contains($docText, '@return') || str_contains($docText, '@phpstan-return') || str_contains($docText, '@psalm-return'));
@@ -45,6 +45,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node):
4545

4646
$thisArg = self::resolveThisArg($isClassMethod, $node);
4747
$isNativeVoid = $node->returnType instanceof Node\Identifier && strtolower($node->returnType->name) === 'void';
48+
$needsReturnVars = str_contains($docText, ' is ') || (str_contains($docText, '@return') && str_contains($docText, '$'));
4849

4950
$injectedStmts = [];
5051
if ($hasParam) {
@@ -54,10 +55,10 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node):
5455
if ($hasReturn) {
5556
$node->stmts = self::isGenerator($node)
5657
? self::wrapGeneratorReturns($node->stmts, $thisArg)
57-
: self::wrapNonGeneratorReturns($node->stmts, $thisArg, $isNativeVoid);
58+
: self::wrapNonGeneratorReturns($node->stmts, $thisArg, $isNativeVoid, $needsReturnVars);
5859
}
5960

60-
$node->stmts = array_merge($injectedStmts, $node->stmts);
61+
$node->stmts = [...$injectedStmts, ...$node->stmts];
6162
}
6263

6364
private static function shouldSkipInjection(string $docText): bool
@@ -138,7 +139,7 @@ private static function buildParamInjections(
138139
str_contains($docText, 'iterable') || str_contains($docText, 'Traversable') || str_contains($docText, 'Generator') || str_contains($docText, 'Iterator')
139140
);
140141

141-
return array_merge($injectedStmts, $callableWrappers, $iterableWrappers);
142+
return [...$injectedStmts, ...$callableWrappers, ...$iterableWrappers];
142143
}
143144

144145
private static function buildSetupScopeStmt(Node\Expr $thisArg): Node\Stmt\If_
@@ -162,7 +163,7 @@ private static function buildSetupScopeStmt(Node\Expr $thisArg): Node\Stmt\If_
162163
['stmts' => [$throwStmt]]
163164
);
164165

165-
$ifStmt->setAttribute('typephp_injected', value: true);
166+
$ifStmt->setAttribute('typephp_injected', true);
166167

167168
return $ifStmt;
168169
}
@@ -196,7 +197,7 @@ private static function buildParamWrappers(array $params, string $wrapperFunc, N
196197
)
197198
)
198199
);
199-
$expr->setAttribute('typephp_injected', value: true);
200+
$expr->setAttribute('typephp_injected', true);
200201
$wrappers[] = $expr;
201202
}
202203
}
@@ -228,15 +229,19 @@ public static function buildTypeErrorThrowStmt(Node\Expr $errorVar): Node\Stmt\E
228229
);
229230
}
230231

231-
public static function buildReturnCheckCall(Node\Expr $exprToWrap, Node\Expr $thisArg): Node\Expr\FuncCall
232+
public static function buildReturnCheckCall(Node\Expr $exprToWrap, Node\Expr $thisArg, bool $needsReturnVars = false): Node\Expr\FuncCall
232233
{
234+
$varsArg = $needsReturnVars
235+
? new Node\Expr\FuncCall(new Node\Name('get_defined_vars'))
236+
: new Node\Expr\Array_();
237+
233238
return new Node\Expr\FuncCall(
234239
new Node\Name('\TypePHP\Internal\RuntimeTypeChecker::checkReturn'),
235240
[
236241
new Node\Arg(new Node\Scalar\MagicConst\Method()),
237242
new Node\Arg($exprToWrap),
238243
new Node\Arg($thisArg),
239-
new Node\Arg(new Node\Expr\FuncCall(new Node\Name('get_defined_vars'))),
244+
new Node\Arg($varsArg),
240245
]
241246
);
242247
}
@@ -253,10 +258,10 @@ public static function buildVoidReturnGuard(Node\Expr\FuncCall $checkCall): arra
253258
),
254259
['stmts' => [self::buildTypeErrorThrowStmt(new Node\Expr\Variable('__typephpRet'))]]
255260
);
256-
$ifStmt->setAttribute('typephp_injected', value: true);
261+
$ifStmt->setAttribute('typephp_injected', true);
257262

258263
$retStmt = new Node\Stmt\Return_(null);
259-
$retStmt->setAttribute('typephp_injected', value: true);
264+
$retStmt->setAttribute('typephp_injected', true);
260265

261266
return [$ifStmt, $retStmt];
262267
}
@@ -389,7 +394,7 @@ public function enterNode(Node $n): int|Node|null
389394
return null;
390395
}
391396

392-
$n->setAttribute('typephp_wrapped', value: true);
397+
$n->setAttribute('typephp_wrapped', true);
393398

394399
return FunctionContractInjector::buildWrappedYieldNode($n, $this->thisArg);
395400
}
@@ -399,7 +404,7 @@ public function enterNode(Node $n): int|Node|null
399404
return null;
400405
}
401406

402-
$n->setAttribute('typephp_wrapped', value: true);
407+
$n->setAttribute('typephp_wrapped', true);
403408

404409
$n->expr = new Node\Expr\FuncCall(
405410
new Node\Name('\TypePHP\Internal\RuntimeTypeChecker::wrapIterable'),
@@ -427,13 +432,14 @@ public function enterNode(Node $n): int|Node|null
427432
*
428433
* @return array<Node\Stmt>
429434
*/
430-
private static function wrapNonGeneratorReturns(array $stmts, Node\Expr $thisArg, bool $isNativeVoid): array
435+
private static function wrapNonGeneratorReturns(array $stmts, Node\Expr $thisArg, bool $isNativeVoid, bool $needsReturnVars = false): array
431436
{
432437
$traverser = new NodeTraverser();
433-
$traverser->addVisitor(new class ($thisArg, $isNativeVoid) extends NodeVisitorAbstract {
438+
$traverser->addVisitor(new class ($thisArg, $isNativeVoid, $needsReturnVars) extends NodeVisitorAbstract {
434439
public function __construct(
435440
private Node\Expr $thisArg,
436-
private bool $isNativeVoid
441+
private bool $isNativeVoid,
442+
private bool $needsReturnVars
437443
) {
438444
}
439445

@@ -445,7 +451,7 @@ public function enterNode(Node $n): int|array|null
445451

446452
if ($n instanceof Node\Stmt\Return_) {
447453
$exprToWrap = $n->expr ?? new Node\Expr\ConstFetch(new Node\Name('null'));
448-
$checkCall = FunctionContractInjector::buildReturnCheckCall($exprToWrap, $this->thisArg);
454+
$checkCall = FunctionContractInjector::buildReturnCheckCall($exprToWrap, $this->thisArg, $this->needsReturnVars);
449455

450456
if ($this->isNativeVoid) {
451457
return FunctionContractInjector::buildVoidReturnGuard($checkCall);
@@ -463,13 +469,13 @@ public function enterNode(Node $n): int|array|null
463469

464470
$lastStmt = end($newStmts);
465471
if (! $lastStmt instanceof Node\Stmt\Return_ && ! ($lastStmt instanceof Node\Stmt\Expression && $lastStmt->expr instanceof Node\Expr\Throw_)) {
466-
$checkCall = self::buildReturnCheckCall(new Node\Expr\ConstFetch(new Node\Name('null')), $thisArg);
472+
$checkCall = self::buildReturnCheckCall(new Node\Expr\ConstFetch(new Node\Name('null')), $thisArg, $needsReturnVars);
467473

468474
if ($isNativeVoid) {
469-
$newStmts = array_merge($newStmts, self::buildVoidReturnGuard($checkCall));
475+
$newStmts = [...$newStmts, ...self::buildVoidReturnGuard($checkCall)];
470476
} else {
471477
$retStmt = new Node\Stmt\Return_(self::buildTernaryReturnExpr($checkCall));
472-
$retStmt->setAttribute('typephp_injected', value: true);
478+
$retStmt->setAttribute('typephp_injected', true);
473479
$newStmts[] = $retStmt;
474480
}
475481
}

0 commit comments

Comments
 (0)