Skip to content

Commit 7722d87

Browse files
committed
Refactor type handling in ContractParser, ParamChecker, ReturnChecker, and IdentifierValidator; introduce SpecialTypeResolver reset functionality and enhance identifier validation logic
1 parent b0126a2 commit 7722d87

6 files changed

Lines changed: 133 additions & 33 deletions

File tree

src/Contract/ContractParser.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static function reset(): void
6969
FileFilter::reset();
7070
TypeValidatorRegistry::reset();
7171
StubManager::reset();
72+
SpecialTypeResolver::reset();
7273
}
7374

7475
/**
@@ -811,10 +812,13 @@ private static function applyConstructorPromotionFallback(\ReflectionMethod $ref
811812
$propType = new ArrayTypeNode($propType);
812813
}
813814
$substitutedProp = self::substituteAliases($propType, []);
814-
if ($substitutedProp instanceof IdentifierTypeNode && strtolower($substitutedProp->name) === 'mixed') {
815+
$resolvedProp = SpecialTypeResolver::resolve($substitutedProp, $ref);
816+
817+
if ($resolvedProp instanceof IdentifierTypeNode && strtolower($resolvedProp->name) === 'mixed') {
815818
continue;
816819
}
817-
$types[$paramName] = $substitutedProp;
820+
821+
$types[$paramName] = $resolvedProp;
818822
}
819823
}
820824
}
@@ -947,4 +951,4 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo
947951

948952
return $node;
949953
}
950-
}
954+
}

src/Internal/Checker/ParamChecker.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
*/
2727
final class ParamChecker
2828
{
29+
/**
30+
* @var array<string, string>
31+
*/
32+
private static array $effectiveFunctionCache = [];
33+
2934
/**
3035
* Resets the effective function cache. Useful for test isolation.
3136
*/
@@ -34,11 +39,6 @@ public static function reset(): void
3439
self::$effectiveFunctionCache = [];
3540
}
3641

37-
/**
38-
* @var array<string, string>
39-
*/
40-
private static array $effectiveFunctionCache = [];
41-
4242
/**
4343
* @param array<string, mixed> $vars
4444
*/
@@ -69,6 +69,20 @@ public static function checkParams(
6969
$methodTemplates = $contract['templates'];
7070
$classTemplates = $contract['classTemplates'] ?? [];
7171
$aliases = $contract['aliases'];
72+
$hasGenerics = (\count($methodTemplates) > 0 || \count($classTemplates) > 0);
73+
74+
if (! $hasGenerics && \count($aliases) === 0) {
75+
foreach ($contract['types'] as $paramName => $typeNode) {
76+
if (\array_key_exists($paramName, $vars)) {
77+
$err = $registry->validate($vars[$paramName], $typeNode, $effectiveFunction . '(): Argument $' . $paramName);
78+
if ($err !== null) {
79+
return $err;
80+
}
81+
}
82+
}
83+
84+
return null;
85+
}
7286

7387
if (\count($methodTemplates) > 0) {
7488
TemplateManager::clearCallBindings($effectiveFunction, $methodTemplates);
@@ -80,7 +94,9 @@ public static function checkParams(
8094
}
8195

8296
$allTemplates = [...$classTemplates, ...$methodTemplates];
83-
self::preInferGenericArrayTemplates($contract['types'], $vars, $effectiveFunction, $thisObj, $allTemplates);
97+
if (\count($allTemplates) > 0) {
98+
self::preInferGenericArrayTemplates($contract['types'], $vars, $effectiveFunction, $thisObj, $allTemplates);
99+
}
84100

85101
$boundTemplates = TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates);
86102
$declaredTemplates = $allTemplates;

src/Internal/Checker/ReturnChecker.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
*/
2727
final class ReturnChecker
2828
{
29+
/**
30+
* @var array<string, string>
31+
*/
32+
private static array $effectiveFunctionCache = [];
33+
2934
/**
3035
* Resets the effective function cache. Useful for test isolation.
3136
*/
@@ -34,11 +39,6 @@ public static function reset(): void
3439
self::$effectiveFunctionCache = [];
3540
}
3641

37-
/**
38-
* @var array<string, string>
39-
*/
40-
private static array $effectiveFunctionCache = [];
41-
4242
/**
4343
* @param array<string, mixed> $vars
4444
*/
@@ -222,6 +222,34 @@ private static function evaluateReturn(
222222
return $err;
223223
}
224224

225+
$hasGenerics = (\count($templates) > 0);
226+
$hasAliases = (\count($aliases) > 0);
227+
$isConditional = ($returnTypeNode instanceof ConditionalTypeForParameterNode || $returnTypeNode instanceof ConditionalTypeNode);
228+
229+
if (! $hasGenerics && ! $hasAliases && ! $isConditional && ! ($returnTypeNode instanceof CallableTypeNode)) {
230+
$resolvedType = SpecialTypeResolver::resolve($returnTypeNode, $function, $thisObj);
231+
$err = $registry->validate($value, $resolvedType, $function . '(): Return value');
232+
if ($err !== null) {
233+
return $err;
234+
}
235+
236+
if ($value instanceof \Traversable) {
237+
$baseName = '';
238+
if ($resolvedType instanceof IdentifierTypeNode) {
239+
$baseName = strtolower(ltrim($resolvedType->name, '\\'));
240+
} elseif ($resolvedType instanceof GenericTypeNode) {
241+
$baseName = strtolower(ltrim($resolvedType->type->name, '\\'));
242+
}
243+
244+
$genericIterables = ['iterable', 'traversable', 'iterator', 'generator'];
245+
if (\in_array($baseName, $genericIterables, true)) {
246+
return $wrapIterableCallback($function, 'return', $value);
247+
}
248+
}
249+
250+
return $value;
251+
}
252+
225253
$resolvedType = SpecialTypeResolver::resolve($returnTypeNode, $function, $thisObj);
226254

227255
if ($resolvedType instanceof IdentifierTypeNode && isset($aliases[$resolvedType->name])) {

src/Internal/Config.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use TypePHP\Extension\ExtensionManager;
1212
use TypePHP\Internal\Checker\ParamChecker;
1313
use TypePHP\Internal\Checker\ReturnChecker;
14+
use TypePHP\Resolver\SpecialTypeResolver;
1415
use TypePHP\Resolver\TemplateManager;
1516

1617
/**
@@ -264,6 +265,7 @@ public static function set(array $config): void
264265
PathMatcher::reset();
265266
StreamWrapper::reset();
266267
StubManager::reset();
268+
SpecialTypeResolver::reset();
267269
}
268270

269271
/**
@@ -322,6 +324,7 @@ public static function reset(): void
322324
PathMatcher::reset();
323325
StreamWrapper::reset();
324326
StubManager::reset();
327+
SpecialTypeResolver::reset();
325328
}
326329

327330
/**

src/Resolver/SpecialTypeResolver.php

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ final class SpecialTypeResolver
110110
'closed-resource' => true,
111111
];
112112

113+
/**
114+
* In-memory cache for Reflection instances per context string.
115+
*
116+
* @var array<string, \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod>
117+
*/
118+
private static array $reflectionContextCache = [];
119+
113120
/**
114121
* In-memory cache of file import maps keyed by filename.
115122
*
@@ -131,6 +138,17 @@ final class SpecialTypeResolver
131138
*/
132139
private static array $classTraitUseDocs = [];
133140

141+
/**
142+
* Resets internal reflection and file caches. Useful for test isolation.
143+
*/
144+
public static function reset(): void
145+
{
146+
self::$reflectionContextCache = [];
147+
self::$fileUseImports = [];
148+
self::$fileNamespaces = [];
149+
self::$classTraitUseDocs = [];
150+
}
151+
134152
/**
135153
* Validates strict object identity ($value === $thisObj) when the return type node specifies $this.
136154
*/
@@ -170,7 +188,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct
170188

171189
if ($node instanceof GenericTypeNode) {
172190
$genericType = self::resolve($node->type, $context, $thisObj);
173-
$innerTypes = array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->genericTypes);
191+
$innerTypes = array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->genericTypes);
174192

175193
return new GenericTypeNode(
176194
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
@@ -224,11 +242,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct
224242
}
225243

226244
if ($node instanceof UnionTypeNode) {
227-
return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->types));
245+
return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->types));
228246
}
229247

230248
if ($node instanceof IntersectionTypeNode) {
231-
return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->types));
249+
return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->types));
232250
}
233251

234252
return $node;
@@ -262,7 +280,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
262280

263281
if ($node instanceof GenericTypeNode) {
264282
$genericType = self::resolveForFile($node->type, $file);
265-
$innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes);
283+
$innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes);
266284

267285
return new GenericTypeNode(
268286
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
@@ -316,11 +334,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
316334
}
317335

318336
if ($node instanceof UnionTypeNode) {
319-
return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
337+
return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
320338
}
321339

322340
if ($node instanceof IntersectionTypeNode) {
323-
return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
341+
return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
324342
}
325343

326344
return clone $node;
@@ -334,25 +352,36 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
334352
private static function getReflectionContext(\ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context): \ReflectionClass|\ReflectionFunction|\ReflectionMethod
335353
{
336354
if (\is_string($context)) {
355+
if (isset(self::$reflectionContextCache[$context])) {
356+
return self::$reflectionContextCache[$context];
357+
}
358+
337359
if (str_contains($context, '::')) {
338360
[$className, $methodName] = explode('::', $context, 2);
339361

340362
if (class_exists($className) || interface_exists($className) || trait_exists($className) || enum_exists($className)) {
341363
/** @var class-string<object> $className */
342364
try {
343-
return new \ReflectionMethod($className, $methodName);
365+
return self::$reflectionContextCache[$context] = new \ReflectionMethod($className, $methodName);
344366
} catch (\ReflectionException $e) {
345-
return new \ReflectionClass($className);
367+
return self::$reflectionContextCache[$context] = new \ReflectionClass($className);
346368
}
347369
}
348370

349371
/** @var class-string<object> $fallbackClass */
350372
$fallbackClass = \stdClass::class;
351373

352-
return new \ReflectionClass($fallbackClass);
374+
return self::$reflectionContextCache[$context] = new \ReflectionClass($fallbackClass);
353375
}
354376

355-
return new \ReflectionFunction($context);
377+
try {
378+
return self::$reflectionContextCache[$context] = new \ReflectionFunction($context);
379+
} catch (\ReflectionException $e) {
380+
/** @var class-string<object> $fallbackClass */
381+
$fallbackClass = \stdClass::class;
382+
383+
return self::$reflectionContextCache[$context] = new \ReflectionClass($fallbackClass);
384+
}
356385
}
357386

358387
return $context;
@@ -690,8 +719,6 @@ private static function resolveCallableForFile(CallableTypeNode $node, string $f
690719
return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes);
691720
}
692721

693-
// --- Shared Utilities ---
694-
695722
private static function extractOffsetKey(TypeNode $offsetType): string|int|null
696723
{
697724
if ($offsetType instanceof ConstTypeNode) {
@@ -1046,6 +1073,7 @@ private static function parseFileMetadata(string $fileName, string $source): voi
10461073
}
10471074
} elseif ($stmt instanceof Stmt\Class_ && $stmt->name !== null) {
10481075
$className = $namespace !== '' ? $namespace . '\\' . $stmt->name->toString() : $stmt->name->toString();
1076+
self::$classTraitUseDocs[$className] = [];
10491077
foreach ($stmt->stmts as $classStmt) {
10501078
if ($classStmt instanceof Stmt\TraitUse) {
10511079
$doc = $classStmt->getDocComment();

src/Validator/IdentifierValidator.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
2020
{
2121
/** @var IdentifierTypeNode $identifierNode */
2222
$identifierNode = $node;
23-
$lower = strtolower($identifierNode->name);
23+
$name = $identifierNode->name;
2424

25-
$ok = match ($lower) {
25+
$ok = match ($name) {
2626
'int', 'integer' => \is_int($value),
2727
'string' => \is_string($value),
2828
'float', 'double' => \is_float($value) || \is_int($value),
@@ -75,7 +75,7 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
7575
'open-resource' => \is_resource($value),
7676
'closed-resource' => ! \is_resource($value) && get_debug_type($value) === 'resource (closed)',
7777

78-
default => $this->validateClassOrIgnore($value, $identifierNode->name),
78+
default => $this->validateCaseInsensitiveOrClass($value, $name),
7979
};
8080

8181
if (! $ok) {
@@ -85,10 +85,31 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
8585
return null;
8686
}
8787

88-
/**
89-
* Enforces strict object/class checks for valid PHP class identifiers (e.g. User, NonExistentClass),
90-
* but gracefully ignores invalid class syntax (e.g. madeup-type, custom-tag-name).
91-
*/
88+
private function validateCaseInsensitiveOrClass(mixed $value, string $name): bool
89+
{
90+
$lower = strtolower($name);
91+
92+
return match ($lower) {
93+
'int', 'integer' => \is_int($value),
94+
'string' => \is_string($value),
95+
'float', 'double' => \is_float($value) || \is_int($value),
96+
'bool', 'boolean' => \is_bool($value),
97+
'array' => \is_array($value),
98+
'list' => \is_array($value) && (\count($value) === 0 || array_is_list($value)),
99+
'object', 'self', 'static', 'parent', '$this' => \is_object($value),
100+
'callable', 'pure-callable' => \is_callable($value),
101+
'iterable' => is_iterable($value),
102+
'resource' => \is_resource($value),
103+
'null' => $value === null,
104+
'true' => $value === true,
105+
'false' => $value === false,
106+
'mixed' => true,
107+
'scalar' => \is_scalar($value),
108+
'void' => $value === null,
109+
default => $this->validateClassOrIgnore($value, $name),
110+
};
111+
}
112+
92113
private function validateClassOrIgnore(mixed $value, string $name): bool
93114
{
94115
if (! ClassNameValidator::isValid($name)) {

0 commit comments

Comments
 (0)