Skip to content

Commit d23c711

Browse files
committed
Optimize hoth paths in runtime validation
1 parent dc58f1f commit d23c711

8 files changed

Lines changed: 237 additions & 107 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
/manual-tests
55
composer.lock
66
index.php
7+
benchmark.php
78
.php-cs-fixer.cache

src/Contract/ContractParser.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
2222
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
2323
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
24+
use TypePHP\Internal\Checker\InlineChecker;
2425
use TypePHP\Internal\Config;
2526
use TypePHP\Internal\StubManager;
2627
use TypePHP\Resolver\SpecialTypeResolver;
@@ -68,6 +69,7 @@ public static function reset(): void
6869
self::$propertyCache = [];
6970
self::$magicMethodCache = [];
7071
self::$classLevelDocCache = [];
72+
InlineChecker::reset();
7173
DocblockExtractor::reset();
7274
FileFilter::reset();
7375
TypeValidatorRegistry::reset();

src/Internal/Checker/InlineChecker.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ final class InlineChecker
4242
*/
4343
private static array $parsedTypeNodeCache = [];
4444

45+
/**
46+
* In-memory cache for fully resolved type nodes per type string and file.
47+
*
48+
* @var array<string, TypeNode>
49+
*/
50+
private static array $resolvedTypeNodeCache = [];
51+
52+
/**
53+
* Resets internal type node caches. Useful for test isolation.
54+
*/
55+
public static function reset(): void
56+
{
57+
self::$parsedTypeNodeCache = [];
58+
self::$resolvedTypeNodeCache = [];
59+
}
60+
4561
/**
4662
* Fast lookup set for scalar refinement types.
4763
*/
@@ -103,15 +119,21 @@ public static function checkVariable(mixed $value, string $typeString, string $v
103119
}
104120

105121
try {
106-
$typeString = DocblockNormalizer::normalize($typeString);
107-
$typeNode = self::parseTypeString($typeString);
122+
$cacheKey = $typeString . '|' . $file;
123+
if (isset(self::$resolvedTypeNodeCache[$cacheKey])) {
124+
$typeNode = self::$resolvedTypeNodeCache[$cacheKey];
125+
} else {
126+
$normalized = DocblockNormalizer::normalize($typeString);
127+
$typeNode = self::parseTypeString($normalized);
128+
129+
if ($file !== '') {
130+
$typeNode = SpecialTypeResolver::resolveForFile($typeNode, $file);
131+
}
108132

109-
if ($file !== '') {
110-
$typeNode = SpecialTypeResolver::resolveForFile($typeNode, $file);
133+
$typeNode = self::resolveCallerContext($typeNode);
134+
self::$resolvedTypeNodeCache[$cacheKey] = $typeNode;
111135
}
112136

113-
$typeNode = self::resolveCallerContext($typeNode);
114-
115137
if (! self::shouldValidateType($typeNode, $config)) {
116138
return $value;
117139
}

src/Internal/Checker/ParamChecker.php

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,17 @@ public static function checkParams(
4949
string $function,
5050
array $vars,
5151
object|string|null $thisOrClass,
52-
TypeValidatorRegistry $registry
52+
TypeValidatorRegistry $registry,
53+
string $effectiveFunction = ''
5354
): ?ErrorMessage {
5455
if (! Config::isParamsEnabled()) {
5556
return null;
5657
}
5758

5859
$thisObj = \is_object($thisOrClass) ? $thisOrClass : null;
59-
$effectiveFunction = self::resolveEffectiveFunction($function, $thisOrClass, $thisObj);
60+
if ($effectiveFunction === '') {
61+
$effectiveFunction = self::resolveEffectiveFunction($function, $thisOrClass, $thisObj);
62+
}
6063

6164
$magicError = self::handleMagicCall($effectiveFunction, $vars, $thisObj, $registry);
6265
if ($magicError !== null) {
@@ -140,20 +143,23 @@ public static function resolveEffectiveFunction(string $function, object|string|
140143
}
141144

142145
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : '');
143-
$cacheKey = $function . '|' . $actualClassName;
146+
if ($actualClassName === '') {
147+
return $function;
148+
}
149+
150+
[$classOrTrait, $methodName] = explode('::', $function, 2);
144151

152+
$cacheKey = $function . '|' . $actualClassName;
145153
if (isset(self::$effectiveFunctionCache[$cacheKey])) {
146154
return self::$effectiveFunctionCache[$cacheKey];
147155
}
148156

149-
[$classOrTrait, $methodName] = explode('::', $function, 2);
150-
151-
$effectiveFunction = ($actualClassName !== '' && $actualClassName !== $classOrTrait)
157+
$effectiveFunction = ($actualClassName !== $classOrTrait)
152158
? $actualClassName . '::' . $methodName
153159
: $function;
154160

155161
if ($thisObj !== null) {
156-
$targetClass = $actualClassName !== '' ? $actualClassName : $classOrTrait;
162+
$targetClass = $actualClassName;
157163
$traitAliases = HierarchyResolver::getTraitAliases($targetClass);
158164

159165
if (\count($traitAliases) > 0) {
@@ -195,8 +201,11 @@ private static function handleMagicCall(
195201
?object $thisObj,
196202
TypeValidatorRegistry $registry
197203
): ?ErrorMessage {
198-
$isMagicCall = str_ends_with($effectiveFunction, '::__call') || str_ends_with($effectiveFunction, '::__callStatic');
199-
if (! $isMagicCall || ! Config::isMagicMethodsEnabled()) {
204+
if (! Config::isMagicMethodsEnabled()) {
205+
return null;
206+
}
207+
208+
if (! str_ends_with($effectiveFunction, '::__call') && ! str_ends_with($effectiveFunction, '::__callStatic')) {
200209
return null;
201210
}
202211

src/Internal/Checker/ReturnChecker.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,23 @@ private static function resolveEffectiveFunction(string $function, object|string
106106
}
107107

108108
$actualClassName = \is_object($thisOrClass) ? \get_class($thisOrClass) : (\is_string($thisOrClass) ? $thisOrClass : '');
109-
$cacheKey = $function . '|' . $actualClassName;
109+
if ($actualClassName === '') {
110+
return $function;
111+
}
110112

113+
[$classOrTrait, $methodName] = explode('::', $function, 2);
114+
115+
$cacheKey = $function . '|' . $actualClassName;
111116
if (isset(self::$effectiveFunctionCache[$cacheKey])) {
112117
return self::$effectiveFunctionCache[$cacheKey];
113118
}
114119

115-
[$classOrTrait, $methodName] = explode('::', $function, 2);
116-
117-
$effectiveFunction = ($actualClassName !== '' && $actualClassName !== $classOrTrait)
120+
$effectiveFunction = ($actualClassName !== $classOrTrait)
118121
? $actualClassName . '::' . $methodName
119122
: $function;
120123

121124
if ($thisObj !== null) {
122-
$targetClass = $actualClassName !== '' ? $actualClassName : $classOrTrait;
125+
$targetClass = $actualClassName;
123126
$traitAliases = HierarchyResolver::getTraitAliases($targetClass);
124127

125128
if (\count($traitAliases) > 0) {
@@ -163,8 +166,11 @@ private static function handleMagicReturn(
163166
TypeValidatorRegistry $registry,
164167
callable $wrapIterableCallback
165168
): mixed {
166-
$isMagicCall = str_ends_with($effectiveFunction, '::__call') || str_ends_with($effectiveFunction, '::__callStatic');
167-
if (! $isMagicCall || ! Config::isMagicMethodsEnabled()) {
169+
if (! Config::isMagicMethodsEnabled()) {
170+
return null;
171+
}
172+
173+
if (! str_ends_with($effectiveFunction, '::__call') && ! str_ends_with($effectiveFunction, '::__callStatic')) {
168174
return null;
169175
}
170176

0 commit comments

Comments
 (0)