Skip to content

Commit e90b311

Browse files
authored
Internal improvements 10 (#40)
* Add tdd for event classes and tests for method-level and class-level generics * Enhance template handling in ContractParser and related classes; unify method and class templates for improved type resolution and error handling * Refactor ErrorFactory to streamline exception handling and improve type annotations for trace filtering * Add step to warm up TypePHP cache in CI workflow * Update Pest test suite command to include compact option for improved output
1 parent 086b56c commit e90b311

17 files changed

Lines changed: 386 additions & 130 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ jobs:
4242
run: ./vendor/bin/phpstan analyse --no-progress
4343
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'
4444

45+
- name: Warm Up TypePHP Cache
46+
run: php bin/typephp cache:warm
47+
4548
- name: Run Test Suite with Coverage (Pest)
46-
run: ./vendor/bin/pest --coverage-clover=clover.xml
49+
run: ./vendor/bin/pest --coverage-clover=clover.xml --compact
4750
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.4'
4851

4952
- name: Upload Coverage to Codecov

src/Contract/ContractParser.php

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class ContractParser
3030
/**
3131
* Cache for resolved contract metadata.
3232
*
33-
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}>
33+
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}>
3434
*/
3535
private static array $cache = [];
3636

@@ -64,7 +64,7 @@ public static function reset(): void
6464
/**
6565
* Parses PHPDoc contracts for a function or class method.
6666
*
67-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
67+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
6868
*/
6969
public static function parse(string $function): array
7070
{
@@ -83,25 +83,26 @@ public static function parse(string $function): array
8383
$ref = $refClass->getMethod($methodName);
8484
$contract = self::parseMethod($ref);
8585
} else {
86-
$templates = [];
86+
$classTemplates = [];
8787
$aliases = [];
88-
self::parseClassLevelDocs($refClass, $templates, $aliases);
88+
self::parseClassLevelDocs($refClass, $classTemplates, $aliases);
8989
$contract = [
9090
'types' => [],
91-
'templates' => $templates,
91+
'templates' => [],
92+
'classTemplates' => $classTemplates,
9293
'return' => null,
9394
'aliases' => $aliases,
9495
];
9596
}
9697
} else {
97-
$contract = ['types' => [], 'templates' => [], 'return' => null, 'aliases' => []];
98+
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
9899
}
99100
} else {
100101
$ref = new \ReflectionFunction($function);
101102
$contract = self::parseFunction($ref);
102103
}
103104
} catch (\ReflectionException $e) {
104-
$contract = ['types' => [], 'templates' => [], 'return' => null, 'aliases' => []];
105+
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
105106
}
106107

107108
return self::$cache[$function] = $contract;
@@ -161,8 +162,8 @@ public static function parseProperty(string $className, string $propertyName): ?
161162
}
162163

163164
$aliases = [];
164-
$templates = [];
165-
self::parseClassLevelDocs($declaringClass, $templates, $aliases);
165+
$classTemplates = [];
166+
self::parseClassLevelDocs($declaringClass, $classTemplates, $aliases);
166167

167168
if (! $isMagicProperty) {
168169
$phpDocNode = DocblockExtractor::parseDocString($doc);
@@ -276,8 +277,8 @@ public static function parseMagicMethod(string $className, string $methodName):
276277
}
277278

278279
$aliases = [];
279-
$templates = [];
280-
self::parseClassLevelDocs($declaringClass, $templates, $aliases);
280+
$classTemplates = [];
281+
self::parseClassLevelDocs($declaringClass, $classTemplates, $aliases);
281282

282283
$phpDocNode = DocblockExtractor::parseDocString($doc);
283284
DocblockExtractor::extractAliases($phpDocNode, $aliases, $declaringClass);
@@ -294,7 +295,7 @@ public static function parseMagicMethod(string $className, string $methodName):
294295
'return' => $resolvedReturn,
295296
'parameters' => $resolvedParams,
296297
'aliases' => $aliases,
297-
'templates' => $templates,
298+
'templates' => $classTemplates,
298299
];
299300
} catch (\Throwable $e) {
300301
return self::$magicMethodCache[$cacheKey] = null;
@@ -415,8 +416,8 @@ public static function parseClassAliases(string $className): array
415416
/** @var class-string<object> $className */
416417
$refClass = new \ReflectionClass($className);
417418
$aliases = [];
418-
$templates = [];
419-
self::parseClassLevelDocs($refClass, $templates, $aliases);
419+
$classTemplates = [];
420+
self::parseClassLevelDocs($refClass, $classTemplates, $aliases);
420421

421422
return $aliases;
422423
} catch (\Throwable $e) {
@@ -427,25 +428,27 @@ public static function parseClassAliases(string $className): array
427428
/**
428429
* Orchestrates parsing for class methods across the inheritance hierarchy.
429430
*
430-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
431+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
431432
*/
432433
private static function parseMethod(\ReflectionMethod $ref): array
433434
{
434435
$types = [];
435-
$templates = [];
436+
$methodTemplates = [];
437+
$classTemplates = [];
436438
$returnType = null;
437439
$aliases = [];
438440

439-
self::parseClassLevelDocs($ref->getDeclaringClass(), $templates, $aliases);
440-
self::parseMethodHierarchyDocs($ref, $types, $templates, $returnType, $aliases);
441+
self::parseClassLevelDocs($ref->getDeclaringClass(), $classTemplates, $aliases);
442+
self::parseMethodHierarchyDocs($ref, $types, $methodTemplates, $returnType, $aliases);
441443

442444
if ($ref->getName() === '__construct') {
443445
self::applyConstructorPromotionFallback($ref, $types);
444446
}
445447

446448
return [
447449
'types' => $types,
448-
'templates' => $templates,
450+
'templates' => $methodTemplates,
451+
'classTemplates' => $classTemplates,
449452
'return' => $returnType,
450453
'aliases' => $aliases,
451454
];
@@ -454,7 +457,7 @@ private static function parseMethod(\ReflectionMethod $ref): array
454457
/**
455458
* Orchestrates parsing for standalone global or namespaced functions.
456459
*
457-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
460+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
458461
*/
459462
private static function parseFunction(\ReflectionFunction $ref): array
460463
{
@@ -468,6 +471,7 @@ private static function parseFunction(\ReflectionFunction $ref): array
468471
return [
469472
'types' => [],
470473
'templates' => [],
474+
'classTemplates' => [],
471475
'return' => null,
472476
'aliases' => [],
473477
];
@@ -505,6 +509,7 @@ private static function parseFunction(\ReflectionFunction $ref): array
505509
return [
506510
'types' => $types,
507511
'templates' => $templates,
512+
'classTemplates' => [],
508513
'return' => $returnType,
509514
'aliases' => $aliases,
510515
];
@@ -543,7 +548,6 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar
543548

544549
/**
545550
* Resolves method-level docblocks (@param, @return, @template, aliases) up the method hierarchy.
546-
* Prioritizes @phpstan-param and @psalm-param over standard @param tags.
547551
*
548552
* @param \ReflectionMethod $ref
549553
* @param array<string, TypeNode> $types

src/Internal/Checker/GeneratorChecker.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ private static function resolveGeneratorReturnType(string $function, object|stri
9999
}
100100

101101
$thisObj = \is_object($thisOrClass) ? $thisOrClass : null;
102-
$templates = $contract['templates'] ?? [];
103-
$boundTemplates = TemplateManager::getBoundTemplates($function, $thisObj, $templates);
102+
$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
103+
$boundTemplates = TemplateManager::getBoundTemplates($function, $thisObj, $allTemplates);
104104

105-
if (\count($boundTemplates) > 0 || \count($templates) > 0) {
106-
$returnTypeNode = TemplateSubstitutor::substitute($returnTypeNode, $boundTemplates, $templates);
105+
if (\count($boundTemplates) > 0 || \count($allTemplates) > 0) {
106+
$returnTypeNode = TemplateSubstitutor::substitute($returnTypeNode, $boundTemplates, $allTemplates);
107107
$returnTypeNode = SpecialTypeResolver::resolve($returnTypeNode, $function, $thisObj);
108108
}
109109

src/Internal/Checker/ParamChecker.php

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,24 @@ public static function checkParams(
5252
return null;
5353
}
5454

55-
$templates = $contract['templates'];
55+
$methodTemplates = $contract['templates'];
56+
$classTemplates = $contract['classTemplates'] ?? [];
5657
$aliases = $contract['aliases'];
5758

58-
self::initializeCallContext($effectiveFunction, $thisObj, $templates);
59-
self::preInferGenericArrayTemplates($contract['types'], $vars, $effectiveFunction, $thisObj, $templates);
59+
if (\count($methodTemplates) > 0) {
60+
TemplateManager::clearCallBindings($effectiveFunction, $methodTemplates);
61+
}
6062

61-
$boundTemplates = TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $templates);
62-
$declaredTemplates = $templates;
63+
if ($thisObj !== null && \count($classTemplates) > 0 && str_contains($effectiveFunction, '::')) {
64+
$declaringClass = explode('::', $effectiveFunction, 2)[0];
65+
TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass);
66+
}
67+
68+
$allTemplates = [...$classTemplates, ...$methodTemplates];
69+
self::preInferGenericArrayTemplates($contract['types'], $vars, $effectiveFunction, $thisObj, $allTemplates);
70+
71+
$boundTemplates = TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates);
72+
$declaredTemplates = $allTemplates;
6373

6474
foreach ($contract['types'] as $paramName => $typeNode) {
6575
if (! \array_key_exists($paramName, $vars)) {
@@ -72,7 +82,7 @@ public static function checkParams(
7282
$vars[$paramName],
7383
$effectiveFunction,
7484
$thisObj,
75-
$templates,
85+
$allTemplates,
7686
$aliases,
7787
$boundTemplates,
7888
$declaredTemplates,
@@ -170,21 +180,6 @@ private static function handleMagicCall(
170180
return self::validateMagicArguments($magicContract, $magicArgs, $magicFunction, $thisObj, $registry);
171181
}
172182

173-
/**
174-
* Initializes call stack frames or resolves inherited template bounds.
175-
*
176-
* @param array<string, TemplateTagValueNode> $templates
177-
*/
178-
private static function initializeCallContext(string $effectiveFunction, ?object $thisObj, array $templates): void
179-
{
180-
if ($thisObj === null && \count($templates) > 0) {
181-
TemplateManager::clearCallBindings($effectiveFunction, $templates);
182-
} elseif ($thisObj !== null && str_contains($effectiveFunction, '::')) {
183-
$declaringClass = explode('::', $effectiveFunction, 2)[0];
184-
TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass);
185-
}
186-
}
187-
188183
/**
189184
* Pre-infers generic template parameters from array arguments before callback wrapping.
190185
*
@@ -227,7 +222,7 @@ private static function inferFromTypeNode(
227222
): void {
228223
if ($typeNode instanceof GenericTypeNode) {
229224
$baseType = strtolower($typeNode->type->name);
230-
if (! \in_array($baseType, ['array', 'list', 'iterable', 'traversable'], strict: true)) {
225+
if (! \in_array($baseType, ['array', 'list', 'iterable', 'traversable'], true)) {
231226
return;
232227
}
233228

@@ -259,8 +254,13 @@ private static function bindTemplateIfUnbound(
259254
?object $thisObj,
260255
array $templates
261256
): void {
262-
if (isset($templates[$templateName]) && ! TemplateManager::isBound($effectiveFunction, $thisObj, $templateName)) {
263-
TemplateManager::bindTemplate($effectiveFunction, $thisObj, $templateName, TemplateManager::inferTypeFromValue($sampleValue));
257+
$contract = ContractParser::parse($effectiveFunction);
258+
$classTemplates = $contract['classTemplates'] ?? [];
259+
$isClassLevelTemplate = isset($classTemplates[$templateName]);
260+
$targetObj = $isClassLevelTemplate ? $thisObj : null;
261+
262+
if (isset($templates[$templateName]) && ! TemplateManager::isBound($effectiveFunction, $targetObj, $templateName)) {
263+
TemplateManager::bindTemplate($effectiveFunction, $targetObj, $templateName, TemplateManager::inferTypeFromValue($sampleValue));
264264
}
265265
}
266266

@@ -332,8 +332,6 @@ private static function validateMagicArguments(
332332
$aliases = $magicContract['aliases'];
333333
$parameters = $magicContract['parameters'];
334334

335-
self::initializeCallContext($function, $thisObj, $templates);
336-
337335
$argValues = array_values($args);
338336
$argKeys = array_keys($args);
339337

@@ -428,7 +426,12 @@ private static function resolveClassStringTemplate(
428426
$templateName = $innerType->name;
429427
$templateNode = $templates[$templateName];
430428

431-
if (! TemplateManager::isBound($function, $thisObj, $templateName)) {
429+
$contract = ContractParser::parse($function);
430+
$classTemplates = $contract['classTemplates'] ?? [];
431+
$isClassLevelTemplate = isset($classTemplates[$templateName]);
432+
$targetObj = $isClassLevelTemplate ? $thisObj : null;
433+
434+
if (! TemplateManager::isBound($function, $targetObj, $templateName)) {
432435
if (! \is_string($val) || ! ClassNameValidator::isValid($val) || (! class_exists($val) && ! interface_exists($val) && ! trait_exists($val) && ! enum_exists($val))) {
433436
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' must be a valid class-string, ' . TypeFormatter::formatGivenValue($val) . ' given');
434437
}
@@ -438,17 +441,17 @@ private static function resolveClassStringTemplate(
438441
$boundName = $resolvedBound instanceof IdentifierTypeNode ? $resolvedBound->name : (string) $resolvedBound;
439442
$lowerBound = strtolower($boundName);
440443

441-
if ($lowerBound !== 'object' && $lowerBound !== 'mixed' && ! is_a($val, $boundName, allow_string: true)) {
444+
if ($lowerBound !== 'object' && $lowerBound !== 'mixed' && ! is_a($val, $boundName, true)) {
442445
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' (class-string<' . $templateName . '>) must be a class-string of ' . $boundName . ", '" . $val . "' given");
443446
}
444447
}
445448

446-
TemplateManager::bindTemplate($function, $thisObj, $templateName, new IdentifierTypeNode($val));
449+
TemplateManager::bindTemplate($function, $targetObj, $templateName, new IdentifierTypeNode($val));
447450
} else {
448-
$expectedTypeNode = TemplateManager::getBoundType($function, $thisObj, $templateName);
451+
$expectedTypeNode = TemplateManager::getBoundType($function, $targetObj, $templateName);
449452
$targetClass = $expectedTypeNode instanceof IdentifierTypeNode ? $expectedTypeNode->name : (string) $expectedTypeNode;
450453

451-
if (! \is_string($val) || ! is_a($val, $targetClass, allow_string: true)) {
454+
if (! \is_string($val) || ! is_a($val, $targetClass, true)) {
452455
$valStr = TypeFormatter::formatGivenValue($val);
453456

454457
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' must be a class-string of ' . $targetClass . ', ' . $valStr . ' given');
@@ -494,7 +497,12 @@ private static function resolveTemplateParam(
494497
$templateNode = $templates[$templateName];
495498
$isVariadic = $typeNode instanceof ArrayTypeNode;
496499

497-
if (! TemplateManager::isBound($function, $thisObj, $templateName)) {
500+
$contract = ContractParser::parse($function);
501+
$classTemplates = $contract['classTemplates'] ?? [];
502+
$isClassLevelTemplate = isset($classTemplates[$templateName]);
503+
$targetObj = $isClassLevelTemplate ? $thisObj : null;
504+
505+
if (! TemplateManager::isBound($function, $targetObj, $templateName)) {
498506
$sampleVal = ($isVariadic && \is_array($val)) ? ($val[0] ?? null) : $val;
499507
$inferredType = TemplateManager::inferTypeFromValue($sampleVal);
500508

@@ -506,7 +514,7 @@ private static function resolveTemplateParam(
506514
}
507515
}
508516

509-
TemplateManager::bindTemplate($function, $thisObj, $templateName, $inferredType);
517+
TemplateManager::bindTemplate($function, $targetObj, $templateName, $inferredType);
510518

511519
if ($isVariadic && \is_array($val)) {
512520
foreach ($val as $idx => $item) {
@@ -517,7 +525,7 @@ private static function resolveTemplateParam(
517525
}
518526
}
519527
} else {
520-
$expectedTypeNode = TemplateManager::getBoundType($function, $thisObj, $templateName);
528+
$expectedTypeNode = TemplateManager::getBoundType($function, $targetObj, $templateName);
521529
if ($expectedTypeNode === null) {
522530
return null;
523531
}

src/Internal/Checker/ReturnChecker.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ public static function checkReturn(
6464
return $value;
6565
}
6666

67+
$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
68+
6769
return self::evaluateReturn(
6870
$returnTypeNode,
6971
$value,
7072
$effectiveFunction,
7173
$thisObj,
7274
$vars,
7375
$contract['aliases'] ?? [],
74-
$contract['templates'] ?? [],
76+
$allTemplates,
7577
$registry,
7678
$wrapIterableCallback
7779
);
@@ -230,7 +232,7 @@ private static function evaluateReturn(
230232
}
231233

232234
$genericIterables = ['iterable', 'traversable', 'iterator', 'generator'];
233-
if (\in_array($baseName, $genericIterables, strict: true)) {
235+
if (\in_array($baseName, $genericIterables, true)) {
234236
return $wrapIterableCallback($function, 'return', $value);
235237
}
236238
}
@@ -314,7 +316,7 @@ private static function resolveTemplateConditional(
314316
$isTargetMatch = ClassNameValidator::isValid($subStr) && ClassNameValidator::isValid($targetStr) &&
315317
(class_exists($subStr) || interface_exists($subStr)) &&
316318
(class_exists($targetStr) || interface_exists($targetStr)) &&
317-
is_a($subStr, $targetStr, allow_string: true);
319+
is_a($subStr, $targetStr, true);
318320
}
319321

320322
if ($node->negated) {

0 commit comments

Comments
 (0)