From 8ad7003d2b7e761f9de9ffb37b56a3daddc41185 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Fri, 18 Sep 2026 11:00:39 +0800 Subject: [PATCH 1/5] Fix method-level @template T used as a type argument of a generic parameter is replaced by its bound --- src/Internal/Checker/ParamChecker.php | 106 +++++++++++++++- ...odTemplateGenericArgumentInferenceTest.php | 113 ++++++++++++++++++ 2 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php diff --git a/src/Internal/Checker/ParamChecker.php b/src/Internal/Checker/ParamChecker.php index de36847..97ff190 100644 --- a/src/Internal/Checker/ParamChecker.php +++ b/src/Internal/Checker/ParamChecker.php @@ -440,7 +440,7 @@ private static function handleMagicCall( } /** - * Pre-infers generic template parameters from closure typehints and array arguments. + * Pre-infers generic template parameters from closure typehints, array arguments, and generic object arguments. * * @param array $types * @param array $vars @@ -468,8 +468,110 @@ private static function preInferGenericTemplates( self::inferTemplatesFromClosures($types, $vars, $effectiveFunction, $thisObj, $templates, $classTemplates); } - if (\count($types) > 1) { + if (\count($types) > 0) { self::inferTemplatesFromArrays($types, $vars, $effectiveFunction, $thisObj, $templates); + self::inferTemplatesFromGenericObjects($types, $vars, $effectiveFunction, $thisObj, $templates, $classTemplates); + } + } + + /** + * Pre-infers generic template parameters from generic object arguments (e.g. PBox or array>). + * + * @param array $types + * @param array $vars + * @param array $templates + * @param array $classTemplates + */ + private static function inferTemplatesFromGenericObjects( + array $types, + array $vars, + string $effectiveFunction, + ?object $thisObj, + array $templates, + array $classTemplates = [] + ): void { + foreach ($types as $paramName => $typeNode) { + if (! isset($vars[$paramName])) { + continue; + } + + $value = $vars[$paramName]; + self::inferGenericObjectNode($typeNode, $value, $effectiveFunction, $thisObj, $templates, $classTemplates); + } + } + + /** + * @param array $templates + * @param array $classTemplates + */ + private static function inferGenericObjectNode( + TypeNode $typeNode, + mixed $value, + string $effectiveFunction, + ?object $thisObj, + array $templates, + array $classTemplates = [] + ): void { + if ($typeNode instanceof NullableTypeNode) { + $typeNode = $typeNode->type; + } + + if ($typeNode instanceof GenericTypeNode && \is_object($value)) { + $baseName = strtolower($typeNode->type->name); + if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { + return; + } + + $boundOnInstance = TemplateManager::getBoundTemplatesForInstance($value); + if ($boundOnInstance === []) { + return; + } + + $instanceBoundTypes = array_values($boundOnInstance); + + foreach ($typeNode->genericTypes as $idx => $gtNode) { + if ($gtNode instanceof IdentifierTypeNode && isset($templates[$gtNode->name])) { + $tName = $gtNode->name; + $isClassLevel = ! TemplateManager::isMethodTemplate($effectiveFunction, $tName) && isset($classTemplates[$tName]); + $targetObj = $isClassLevel ? $thisObj : null; + + if (! TemplateManager::isBound($effectiveFunction, $targetObj, $tName)) { + $inferredCandidate = $instanceBoundTypes[$idx] ?? null; + + if ($inferredCandidate !== null) { + $templateTag = $templates[$tName]; + $satisfiesBound = true; + + if ($templateTag->bound !== null) { + $resolvedBound = SpecialTypeResolver::resolve($templateTag->bound, $effectiveFunction, $thisObj); + $satisfiesBound = TemplateManager::checkVariance($inferredCandidate, $resolvedBound, GenericTypeNode::VARIANCE_COVARIANT); + } + + if ($satisfiesBound) { + TemplateManager::bindTemplate($effectiveFunction, $targetObj, $tName, $inferredCandidate); + } + } + } + } + } + } elseif (\is_array($value)) { + $innerType = null; + if ($typeNode instanceof ArrayTypeNode) { + $innerType = $typeNode->type; + } elseif ($typeNode instanceof GenericTypeNode) { + $baseName = strtolower($typeNode->type->name); + if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { + $innerType = $typeNode->genericTypes[1] ?? $typeNode->genericTypes[0] ?? null; + } + } + + if ($innerType !== null) { + foreach ($value as $item) { + if (\is_object($item)) { + self::inferGenericObjectNode($innerType, $item, $effectiveFunction, $thisObj, $templates, $classTemplates); + } + } + } } } diff --git a/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php b/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php new file mode 100644 index 0000000..fb94f02 --- /dev/null +++ b/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php @@ -0,0 +1,113 @@ + $box + * + * @return T + */ + public function unwrap(MethodTemplateBox $box): MethodTemplateBase + { + return $box->value; + } + + /** + * @template T of MethodTemplateBase + * + * @param array> $items + * + * @return list + */ + public function unwrapAll(array $items): array + { + $result = []; + foreach ($items as $box) { + $result[] = $box->value; + } + + return $result; + } +} + +describe('Method-Level Template Inference from Generic Parameter Objects', function () { + test('infers method template T from generic argument PBox instead of falling back to upper bound PBase', function () { + $consumer = new MethodTemplateConsumer(); + $boxA = new MethodTemplateBox(new MethodTemplateA('Alpha')); + + $result = $consumer->unwrap($boxA); + + expect($result)->toBeInstanceOf(MethodTemplateA::class) + ->and($result->name)->toBe('Alpha') + ; + }); + + test('allows multiple calls to unwrap() with different generic arguments without cross-call template locking', function () { + $consumer = new MethodTemplateConsumer(); + + $boxA = new MethodTemplateBox(new MethodTemplateA('Alpha')); + $resultA = $consumer->unwrap($boxA); + expect($resultA)->toBeInstanceOf(MethodTemplateA::class); + + $boxB = new MethodTemplateBox(new MethodTemplateB('Beta')); + $resultB = $consumer->unwrap($boxB); + expect($resultB)->toBeInstanceOf(MethodTemplateB::class); + }); + + test('infers template T from array of generic objects array>', function () { + $consumer = new MethodTemplateConsumer(); + + $items = [ + 'first' => new MethodTemplateBox(new MethodTemplateA('A1')), + 'second' => new MethodTemplateBox(new MethodTemplateA('A2')), + ]; + + $result = $consumer->unwrapAll($items); + + expect($result)->toHaveCount(2) + ->and($result[0])->toBeInstanceOf(MethodTemplateA::class) + ->and($result[1])->toBeInstanceOf(MethodTemplateA::class) + ; + }); +}); \ No newline at end of file From 92fb3d6168e2064268d294b6bfdfe158a3bfffd6 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Fri, 18 Sep 2026 11:22:59 +0800 Subject: [PATCH 2/5] Fix Static methods ignore the @extends binding of the called subclass --- src/Internal/Checker/ParamChecker.php | 264 +++++++++--------- src/Internal/Generics/TemplateManager.php | 86 +++++- ...icMethodSubclassGenericInheritanceTest.php | 144 ++++++++++ 3 files changed, 358 insertions(+), 136 deletions(-) create mode 100644 tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php diff --git a/src/Internal/Checker/ParamChecker.php b/src/Internal/Checker/ParamChecker.php index 97ff190..6bc6a50 100644 --- a/src/Internal/Checker/ParamChecker.php +++ b/src/Internal/Checker/ParamChecker.php @@ -165,7 +165,7 @@ public static function checkParams( return self::validateSimpleParams($contract['types'], $vars, $effectiveFunction, $registry); } - self::prepareGenericBindings($effectiveFunction, $methodTemplates, $thisObj, $classTemplates); + self::prepareGenericBindings($effectiveFunction, $methodTemplates, $thisObj, $classTemplates, $thisOrClass); /** @var array $allTemplates */ $allTemplates = [...$classTemplates, ...$methodTemplates]; @@ -175,7 +175,7 @@ public static function checkParams( } $boundTemplates = (\count($allTemplates) > 0) - ? TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates) + ? TemplateManager::getBoundTemplates($effectiveFunction, $thisOrClass, $allTemplates) : []; $declaredTemplates = $allTemplates; @@ -192,7 +192,8 @@ public static function checkParams( $boundTemplates, $declaredTemplates, $registry, - $classTemplates + $classTemplates, + $thisOrClass ); } @@ -240,15 +241,21 @@ private static function prepareGenericBindings( string $effectiveFunction, array $methodTemplates, ?object $thisObj, - array $classTemplates + array $classTemplates, + object|string|null $thisOrClass = null ): void { if (\count($methodTemplates) > 0) { TemplateManager::clearCallBindings($effectiveFunction, $methodTemplates); } - if ($thisObj !== null && \count($classTemplates) > 0 && ! TemplateManager::hasInstanceBindings($thisObj) && str_contains($effectiveFunction, '::')) { + if (\count($classTemplates) > 0 && str_contains($effectiveFunction, '::')) { $declaringClass = explode('::', $effectiveFunction, 2)[0]; - TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass); + if ($thisObj !== null && ! TemplateManager::hasInstanceBindings($thisObj)) { + TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass); + } elseif ($thisObj === null) { + $targetClass = \is_string($thisOrClass) && $thisOrClass !== '' ? $thisOrClass : $declaringClass; + TemplateManager::getClassInheritedBindings($targetClass); + } } } @@ -288,13 +295,10 @@ private static function resolveBaseTypes( * @param array $contractTypes * @param array $baseTypes * @param array $vars - * @param string $effectiveFunction - * @param object|null $thisObj * @param array $allTemplates * @param array $aliases * @param array $boundTemplates * @param array $declaredTemplates - * @param TypeValidatorRegistry $registry * @param array $classTemplates */ private static function validateAllParameters( @@ -308,7 +312,8 @@ private static function validateAllParameters( array $boundTemplates, array $declaredTemplates, TypeValidatorRegistry $registry, - array $classTemplates + array $classTemplates, + object|string|null $thisOrClass = null ): ?ErrorMessage { foreach ($contractTypes as $paramName => $_) { if (! isset($vars[$paramName]) && ! \array_key_exists($paramName, $vars)) { @@ -316,7 +321,7 @@ private static function validateAllParameters( } $currentBoundTemplates = (\count($allTemplates) > 0) - ? TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates) + ? TemplateManager::getBoundTemplates($effectiveFunction, $thisOrClass, $allTemplates) : $boundTemplates; $err = self::validateSingleParam( @@ -331,7 +336,8 @@ private static function validateAllParameters( $declaredTemplates, $registry, $classTemplates, - $vars + $vars, + $thisOrClass ); if ($err !== null) { @@ -474,107 +480,6 @@ private static function preInferGenericTemplates( } } - /** - * Pre-infers generic template parameters from generic object arguments (e.g. PBox or array>). - * - * @param array $types - * @param array $vars - * @param array $templates - * @param array $classTemplates - */ - private static function inferTemplatesFromGenericObjects( - array $types, - array $vars, - string $effectiveFunction, - ?object $thisObj, - array $templates, - array $classTemplates = [] - ): void { - foreach ($types as $paramName => $typeNode) { - if (! isset($vars[$paramName])) { - continue; - } - - $value = $vars[$paramName]; - self::inferGenericObjectNode($typeNode, $value, $effectiveFunction, $thisObj, $templates, $classTemplates); - } - } - - /** - * @param array $templates - * @param array $classTemplates - */ - private static function inferGenericObjectNode( - TypeNode $typeNode, - mixed $value, - string $effectiveFunction, - ?object $thisObj, - array $templates, - array $classTemplates = [] - ): void { - if ($typeNode instanceof NullableTypeNode) { - $typeNode = $typeNode->type; - } - - if ($typeNode instanceof GenericTypeNode && \is_object($value)) { - $baseName = strtolower($typeNode->type->name); - if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { - return; - } - - $boundOnInstance = TemplateManager::getBoundTemplatesForInstance($value); - if ($boundOnInstance === []) { - return; - } - - $instanceBoundTypes = array_values($boundOnInstance); - - foreach ($typeNode->genericTypes as $idx => $gtNode) { - if ($gtNode instanceof IdentifierTypeNode && isset($templates[$gtNode->name])) { - $tName = $gtNode->name; - $isClassLevel = ! TemplateManager::isMethodTemplate($effectiveFunction, $tName) && isset($classTemplates[$tName]); - $targetObj = $isClassLevel ? $thisObj : null; - - if (! TemplateManager::isBound($effectiveFunction, $targetObj, $tName)) { - $inferredCandidate = $instanceBoundTypes[$idx] ?? null; - - if ($inferredCandidate !== null) { - $templateTag = $templates[$tName]; - $satisfiesBound = true; - - if ($templateTag->bound !== null) { - $resolvedBound = SpecialTypeResolver::resolve($templateTag->bound, $effectiveFunction, $thisObj); - $satisfiesBound = TemplateManager::checkVariance($inferredCandidate, $resolvedBound, GenericTypeNode::VARIANCE_COVARIANT); - } - - if ($satisfiesBound) { - TemplateManager::bindTemplate($effectiveFunction, $targetObj, $tName, $inferredCandidate); - } - } - } - } - } - } elseif (\is_array($value)) { - $innerType = null; - if ($typeNode instanceof ArrayTypeNode) { - $innerType = $typeNode->type; - } elseif ($typeNode instanceof GenericTypeNode) { - $baseName = strtolower($typeNode->type->name); - if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { - $innerType = $typeNode->genericTypes[1] ?? $typeNode->genericTypes[0] ?? null; - } - } - - if ($innerType !== null) { - foreach ($value as $item) { - if (\is_object($item)) { - self::inferGenericObjectNode($innerType, $item, $effectiveFunction, $thisObj, $templates, $classTemplates); - } - } - } - } - } - /** * Infers generic template parameters from closure parameter typehints. * @@ -845,6 +750,107 @@ private static function bindTemplateIfUnbound( } } + /** + * Pre-infers generic template parameters from generic object arguments (e.g. PBox or array>). + * + * @param array $types + * @param array $vars + * @param array $templates + * @param array $classTemplates + */ + private static function inferTemplatesFromGenericObjects( + array $types, + array $vars, + string $effectiveFunction, + ?object $thisObj, + array $templates, + array $classTemplates = [] + ): void { + foreach ($types as $paramName => $typeNode) { + if (! isset($vars[$paramName])) { + continue; + } + + $value = $vars[$paramName]; + self::inferGenericObjectNode($typeNode, $value, $effectiveFunction, $thisObj, $templates, $classTemplates); + } + } + + /** + * @param array $templates + * @param array $classTemplates + */ + private static function inferGenericObjectNode( + TypeNode $typeNode, + mixed $value, + string $effectiveFunction, + ?object $thisObj, + array $templates, + array $classTemplates = [] + ): void { + if ($typeNode instanceof NullableTypeNode) { + $typeNode = $typeNode->type; + } + + if ($typeNode instanceof GenericTypeNode && \is_object($value)) { + $baseName = strtolower($typeNode->type->name); + if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { + return; + } + + $boundOnInstance = TemplateManager::getBoundTemplatesForInstance($value); + if ($boundOnInstance === []) { + return; + } + + $instanceBoundTypes = array_values($boundOnInstance); + + foreach ($typeNode->genericTypes as $idx => $gtNode) { + if ($gtNode instanceof IdentifierTypeNode && isset($templates[$gtNode->name])) { + $tName = $gtNode->name; + $isClassLevel = ! TemplateManager::isMethodTemplate($effectiveFunction, $tName) && isset($classTemplates[$tName]); + $targetObj = $isClassLevel ? $thisObj : null; + + if (! TemplateManager::isBound($effectiveFunction, $targetObj, $tName)) { + $inferredCandidate = $instanceBoundTypes[$idx] ?? null; + + if ($inferredCandidate !== null) { + $templateTag = $templates[$tName]; + $satisfiesBound = true; + + if ($templateTag->bound !== null) { + $resolvedBound = SpecialTypeResolver::resolve($templateTag->bound, $effectiveFunction, $thisObj); + $satisfiesBound = TemplateManager::checkVariance($inferredCandidate, $resolvedBound, GenericTypeNode::VARIANCE_COVARIANT); + } + + if ($satisfiesBound) { + TemplateManager::bindTemplate($effectiveFunction, $targetObj, $tName, $inferredCandidate); + } + } + } + } + } + } elseif (\is_array($value)) { + $innerType = null; + if ($typeNode instanceof ArrayTypeNode) { + $innerType = $typeNode->type; + } elseif ($typeNode instanceof GenericTypeNode) { + $baseName = strtolower($typeNode->type->name); + if (\in_array($baseName, ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true)) { + $innerType = $typeNode->genericTypes[1] ?? $typeNode->genericTypes[0] ?? null; + } + } + + if ($innerType !== null) { + foreach ($value as $item) { + if (\is_object($item)) { + self::inferGenericObjectNode($innerType, $item, $effectiveFunction, $thisObj, $templates, $classTemplates); + } + } + } + } + } + /** * Unified single-parameter validation pipeline. * @@ -868,7 +874,8 @@ private static function validateSingleParam( array $declaredTemplates, TypeValidatorRegistry $registry, array $classTemplates = [], - array $vars = [] + array $vars = [], + object|string|null $thisOrClass = null ): ?ErrorMessage { if ( $typeNode instanceof ConditionalTypeForParameterNode || @@ -902,7 +909,7 @@ private static function validateSingleParam( } if (self::getTemplateName($typeNode, $templates) !== null) { - return self::resolveTemplateParam($typeNode, $val, $paramName, $effectiveFunction, $thisObj, $templates, $registry, $classTemplates); + return self::resolveTemplateParam($typeNode, $val, $paramName, $effectiveFunction, $thisObj, $templates, $registry, $classTemplates, $thisOrClass); } return $registry->validate($val, $typeNode, $effectiveFunction . '(): Argument $' . $paramName); @@ -1157,7 +1164,8 @@ private static function resolveTemplateParam( ?object $thisObj, array $templates, TypeValidatorRegistry $registry, - array $classTemplates = [] + array $classTemplates = [], + object|string|null $thisOrClass = null ): ?ErrorMessage { $templateName = self::getTemplateName($typeNode, $templates); if ($templateName === null || ! isset($templates[$templateName])) { @@ -1168,24 +1176,24 @@ private static function resolveTemplateParam( $isVariadic = $typeNode instanceof ArrayTypeNode; $isNullable = ($typeNode instanceof NullableTypeNode) || ($typeNode instanceof UnionTypeNode && self::typeContainsNull($typeNode)); $isClassLevelTemplate = ! TemplateManager::isMethodTemplate($function, $templateName) && isset($classTemplates[$templateName]); - $targetObj = $isClassLevelTemplate ? $thisObj : null; + $targetObjOrClass = $isClassLevelTemplate ? ($thisObj ?? $thisOrClass) : null; $allowsNullInBound = ($templateNode->bound !== null && self::typeContainsNull($templateNode->bound)); if ($isNullable && $val === null) { - if ($allowsNullInBound && ! TemplateManager::isBound($function, $targetObj, $templateName)) { - TemplateManager::bindTemplate($function, $targetObj, $templateName, new IdentifierTypeNode('null')); + if ($allowsNullInBound && ! TemplateManager::isBound($function, $targetObjOrClass, $templateName)) { + TemplateManager::bindTemplate($function, $thisObj, $templateName, new IdentifierTypeNode('null')); } return null; } - if (! TemplateManager::isBound($function, $targetObj, $templateName)) { + if (! TemplateManager::isBound($function, $targetObjOrClass, $templateName)) { return self::bindInitialTemplate( $val, $paramName, $function, $thisObj, - $targetObj, + $thisObj, $templateName, $templateNode, $isVariadic, @@ -1199,7 +1207,7 @@ private static function resolveTemplateParam( $paramName, $function, $thisObj, - $targetObj, + $targetObjOrClass, $templateName, $templateNode, $isVariadic, @@ -1256,7 +1264,7 @@ private static function validateBoundTemplate( string $paramName, string $function, ?object $thisObj, - ?object $targetObj, + object|string|null $targetObj, string $templateName, TemplateTagValueNode $templateNode, bool $isVariadic, @@ -1268,9 +1276,11 @@ private static function validateBoundTemplate( return null; } + $targetObjOnly = \is_object($targetObj) ? $targetObj : null; + if ($expectedTypeNode instanceof IdentifierTypeNode && $expectedTypeNode->name === $templateName) { $inferredType = TemplateManager::inferTypeFromValue($val); - TemplateManager::bindTemplate($function, $targetObj, $templateName, $inferredType); + TemplateManager::bindTemplate($function, $targetObjOnly, $templateName, $inferredType); return null; } @@ -1281,7 +1291,7 @@ private static function validateBoundTemplate( $paramName, $function, $thisObj, - $targetObj, + $targetObjOnly, $templateName, $templateNode, $expectedTypeNode, @@ -1301,7 +1311,7 @@ private static function validateBoundTemplate( $isClassLevelTemplate, $function, $thisObj, - $targetObj, + $targetObjOnly, $templateName, $context, $err, @@ -1444,4 +1454,4 @@ private static function typeContainsNull(TypeNode $node): bool return false; } -} +} \ No newline at end of file diff --git a/src/Internal/Generics/TemplateManager.php b/src/Internal/Generics/TemplateManager.php index 4685911..57d11a0 100644 --- a/src/Internal/Generics/TemplateManager.php +++ b/src/Internal/Generics/TemplateManager.php @@ -424,6 +424,24 @@ public static function applyPendingInstantiation(object $instance): void } } + /** + * Retrieves inherited class-level template bindings for a class name. + * + * @return array + */ + public static function getClassInheritedBindings(string $className): array + { + if (isset(self::$classInheritedBindingsCache[$className])) { + return self::$classInheritedBindingsCache[$className]; + } + + if (! class_exists($className, false) && ! class_exists($className) && ! interface_exists($className) && ! trait_exists($className)) { + return self::$classInheritedBindingsCache[$className] = []; + } + + return self::$classInheritedBindingsCache[$className] = self::computeClassInheritedBindings($className); + } + /** * Normalizes generic type arguments when a single type argument is supplied * for a 2-template collection/map whose first template is a key type (TKey of array-key). @@ -498,15 +516,16 @@ public static function clearCallBindings(string $function, array $templates): vo } /** - * Retrieves currently bound template types for a function call or object instance. + * Retrieves currently bound template types for a function call or object instance / static class. * * @param array $templates * * @return array */ - public static function getBoundTemplates(string $function, ?object $thisObj, array $templates): array + public static function getBoundTemplates(string $function, object|string|null $thisOrClass, array $templates): array { $bindings = []; + $thisObj = \is_object($thisOrClass) ? $thisOrClass : null; if ($thisObj !== null) { self::ensureInstanceInherited($thisObj); @@ -521,6 +540,22 @@ public static function getBoundTemplates(string $function, ?object $thisObj, arr unset($bindings[$methodTName]); } } + } else { + $className = \is_string($thisOrClass) && $thisOrClass !== '' + ? $thisOrClass + : (str_contains($function, '::') ? explode('::', $function, 2)[0] : ''); + + if ($className !== '' && (class_exists($className, false) || class_exists($className) || interface_exists($className) || trait_exists($className))) { + $classBindings = self::getClassInheritedBindings($className); + $bindings = $classBindings; + + $methodTemplates = self::getMethodTemplates($function); + if ($methodTemplates !== []) { + foreach ($methodTemplates as $methodTName => $_) { + unset($bindings[$methodTName]); + } + } + } } $topFrame = self::getTopCallFrame($function); @@ -617,15 +652,17 @@ public static function getMethodTemplates(string $function): array } /** - * Checks if a template name is bound in the current instance or call stack frame. + * Checks if a template name is bound in the current instance, static class, or call stack frame. */ - public static function isBound(string $function, ?object $thisObj, string $templateName): bool + public static function isBound(string $function, object|string|null $thisOrClass, string $templateName): bool { $topFrame = self::getTopCallFrame($function); if ($topFrame !== null && isset($topFrame[$templateName])) { return true; } + $thisObj = \is_object($thisOrClass) ? $thisOrClass : null; + if ($thisObj !== null) { self::ensureInstanceInherited($thisObj); @@ -636,19 +673,36 @@ public static function isBound(string $function, ?object $thisObj, string $templ return isset(self::$instanceTemplateBindings[$thisObj][$templateName]); } + $className = \is_string($thisOrClass) && $thisOrClass !== '' + ? $thisOrClass + : (str_contains($function, '::') ? explode('::', $function, 2)[0] : ''); + + if ($className !== '' && (class_exists($className, false) || class_exists($className) || interface_exists($className) || trait_exists($className))) { + if (self::isMethodTemplate($function, $templateName)) { + return false; + } + + $classBindings = self::getClassInheritedBindings($className); + + return isset($classBindings[$templateName]); + } + return false; } + /** - * Retrieves the bound TypeNode for a template name from instance or call stack context. + * Retrieves the bound TypeNode for a template name from instance, static class, or call stack context. */ - public static function getBoundType(string $function, ?object $thisObj, string $templateName): ?TypeNode + public static function getBoundType(string $function, object|string|null $thisOrClass, string $templateName): ?TypeNode { $topFrame = self::getTopCallFrame($function); if ($topFrame !== null && isset($topFrame[$templateName])) { return $topFrame[$templateName]; } + $thisObj = \is_object($thisOrClass) ? $thisOrClass : null; + if ($thisObj !== null) { self::ensureInstanceInherited($thisObj); @@ -659,6 +713,20 @@ public static function getBoundType(string $function, ?object $thisObj, string $ return self::$instanceTemplateBindings[$thisObj][$templateName] ?? null; } + $className = \is_string($thisOrClass) && $thisOrClass !== '' + ? $thisOrClass + : (str_contains($function, '::') ? explode('::', $function, 2)[0] : ''); + + if ($className !== '' && (class_exists($className, false) || class_exists($className) || interface_exists($className) || trait_exists($className))) { + if (self::isMethodTemplate($function, $templateName)) { + return null; + } + + $classBindings = self::getClassInheritedBindings($className); + + return $classBindings[$templateName] ?? null; + } + return null; } @@ -1427,7 +1495,7 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref): }; $base = new IdentifierTypeNode($baseName); - $generics = array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->genericTypes); + $generics = array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->genericTypes); return new GenericTypeNode($base, $generics, $n->variances); } @@ -1441,11 +1509,11 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref): } if ($n instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); + return new UnionTypeNode(array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); } if ($n instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); + return new IntersectionTypeNode(array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); } return $n; diff --git a/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php b/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php new file mode 100644 index 0000000..18e6f39 --- /dev/null +++ b/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php @@ -0,0 +1,144 @@ + $value + * + * @return static + */ + public static function from(string|StaticGenericPBase|StaticGenericIdBase $value): static + { + return new static(); + } + + /** + * @param TEntity $entity + * + * @return static + */ + public static function fromEntity(StaticGenericPBase $entity): static + { + return new static(); + } + + /** + * @return TEntity + */ + public static function produceA(): StaticGenericPBase + { + return new StaticGenericPA(); + } + + /** + * @return TEntity + */ + public static function produceB(): StaticGenericPBase + { + return new StaticGenericPB(); + } + + /** + * @return list + */ + public static function produceList(): array + { + return [new StaticGenericPA()]; + } +} + +/** + * @extends StaticGenericIdBase + */ +final class StaticGenericAId extends StaticGenericIdBase +{ +} + +/** + * @extends StaticGenericIdBase + */ +final class StaticGenericAnyId extends StaticGenericIdBase +{ +} + +describe('Static Methods Resolving Subclass @extends Generic Bindings', function () { + describe('Static Parameter Contracts', function () { + test('resolves TEntity as PA from @extends StaticGenericIdBase on static method calls (Case a)', function () { + $first = StaticGenericAId::from('sample_id'); + expect($first)->toBeInstanceOf(StaticGenericAId::class); + + $second = StaticGenericAId::from($first); + expect($second)->toBeInstanceOf(StaticGenericAId::class); + }); + + test('resolves TEntity as PBase from @extends StaticGenericIdBase and accepts multiple valid subclasses across calls (Case b)', function () { + $first = StaticGenericAnyId::fromEntity(new StaticGenericPA()); + expect($first)->toBeInstanceOf(StaticGenericAnyId::class); + + $second = StaticGenericAnyId::fromEntity(new StaticGenericPB()); + expect($second)->toBeInstanceOf(StaticGenericAnyId::class); + }); + + test('strictly rejects entity not matching the subclass @extends binding', function () { + expect(fn () => StaticGenericAId::fromEntity(new StaticGenericPB())) + ->toThrow(TypeError::class) + ; + }); + }); + + describe('Static Return Type Contracts', function () { + test('validates static method @return TEntity matches subclass @extends binding', function () { + $result = StaticGenericAId::produceA(); + expect($result)->toBeInstanceOf(StaticGenericPA::class); + expect(StaticGenericAnyId::produceA())->toBeInstanceOf(StaticGenericPA::class); + expect(StaticGenericAnyId::produceB())->toBeInstanceOf(StaticGenericPB::class); + }); + + test('throws TypeError when static method @return TEntity violates subclass @extends binding', function () { + expect(fn () => StaticGenericAId::produceB()) + ->toThrow( + TypeError::class, + 'Return value must be of type ' . StaticGenericPA::class . ', ' . StaticGenericPB::class . ' returned' + ) + ; + }); + + test('validates list return types list on static methods', function () { + $list = StaticGenericAId::produceList(); + expect($list)->toHaveCount(1) + ->and($list[0])->toBeInstanceOf(StaticGenericPA::class) + ; + }); + }); +}); \ No newline at end of file From d3f636f13f808daa2cca0f81757a0ba60b58c44b Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Fri, 18 Sep 2026 11:46:07 +0800 Subject: [PATCH 3/5] Fix bug in An interface's template bound is resolved in the namespace of the trait that supplies the method --- src/Internal/Docblock/DocblockParser.php | 79 +++++++++++++++---- tests/Fixtures/Probe/Route.php | 18 +++++ tests/Fixtures/Probe/RouteClass.php | 20 +++++ tests/Fixtures/Probe/RouteInterface.php | 16 ++++ tests/Fixtures/Probe/Sub/HasRoute.php | 13 +++ tests/Fixtures/Probe/UnparameterizedRoute.php | 15 ++++ ...InterfaceMethodNamespaceResolutionTest.php | 29 +++++++ 7 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 tests/Fixtures/Probe/Route.php create mode 100644 tests/Fixtures/Probe/RouteClass.php create mode 100644 tests/Fixtures/Probe/RouteInterface.php create mode 100644 tests/Fixtures/Probe/Sub/HasRoute.php create mode 100644 tests/Fixtures/Probe/UnparameterizedRoute.php create mode 100644 tests/TypeChecking/Generics/TraitSuppliedInterfaceMethodNamespaceResolutionTest.php diff --git a/src/Internal/Docblock/DocblockParser.php b/src/Internal/Docblock/DocblockParser.php index 198c22b..7dbc510 100644 --- a/src/Internal/Docblock/DocblockParser.php +++ b/src/Internal/Docblock/DocblockParser.php @@ -820,16 +820,29 @@ public static function parseClassAliases(string $className): array */ private static function parseMethod(\ReflectionMethod $ref): array { + /** @var array $types */ $types = []; + /** @var array $paramOuts */ $paramOuts = []; + /** @var array $methodTemplates */ $methodTemplates = []; + /** @var array $classTemplates */ $classTemplates = []; $returnType = null; $selfOut = null; + /** @var array $aliases */ $aliases = []; - self::parseClassLevelDocs($ref->getDeclaringClass(), $classTemplates, $aliases); - self::parseMethodHierarchyDocs($ref, $types, $methodTemplates, $returnType, $aliases, $paramOuts, $selfOut); + $targetClass = (class_exists($ref->class, false) || class_exists($ref->class) || interface_exists($ref->class) || enum_exists($ref->class) || trait_exists($ref->class)) + ? new \ReflectionClass($ref->class) + : $ref->getDeclaringClass(); + + self::parseClassLevelDocs($targetClass, $classTemplates, $aliases); + if ($targetClass->getName() !== $ref->getDeclaringClass()->getName()) { + self::parseClassLevelDocs($ref->getDeclaringClass(), $classTemplates, $aliases); + } + + self::parseMethodHierarchyDocs($ref, $types, $methodTemplates, $returnType, $aliases, $paramOuts, $selfOut, $classTemplates); if ($ref->getName() === '__construct') { self::applyConstructorPromotionFallback($ref, $types, $classTemplates, $aliases); @@ -1116,13 +1129,25 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar $className = $declaringClass->getName(); if (isset(self::$classLevelDocCache[$className])) { $cached = self::$classLevelDocCache[$className]; - $templates = $cached['templates']; - $aliases = $cached['aliases']; + foreach ($cached['templates'] as $name => $tag) { + if (! isset($templates[$name])) { + $templates[$name] = $tag; + } + } + foreach ($cached['aliases'] as $name => $alias) { + if (! isset($aliases[$name])) { + $aliases[$name] = $alias; + } + } return; } $classHierarchy = HierarchyResolver::getClassHierarchy($declaringClass); + /** @var array $localTemplates */ + $localTemplates = []; + /** @var array $localAliases */ + $localAliases = []; foreach ($classHierarchy as $hierClass) { $hierClassName = $hierClass->getName(); @@ -1138,18 +1163,37 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar $classPhpDocNode = DocblockExtractor::parseDocString($classDoc); foreach (DocblockExtractor::extractTemplates($classPhpDocNode) as $name => $tag) { - if (! isset($templates[$name])) { - $templates[$name] = $tag; + if (! isset($localTemplates[$name])) { + if ($tag->bound !== null || $tag->default !== null) { + $tag = new TemplateTagValueNode( + $tag->name, + $tag->bound !== null ? SpecialTypeResolver::resolve($tag->bound, $hierClass) : null, + $tag->description, + $tag->default !== null ? SpecialTypeResolver::resolve($tag->default, $hierClass) : null + ); + } + $localTemplates[$name] = $tag; } } - DocblockExtractor::extractAliases($classPhpDocNode, $aliases, $hierClass); + DocblockExtractor::extractAliases($classPhpDocNode, $localAliases, $hierClass); } } self::$classLevelDocCache[$className] = [ - 'templates' => $templates, - 'aliases' => $aliases, + 'templates' => $localTemplates, + 'aliases' => $localAliases, ]; + + foreach ($localTemplates as $name => $tag) { + if (! isset($templates[$name])) { + $templates[$name] = $tag; + } + } + foreach ($localAliases as $name => $alias) { + if (! isset($aliases[$name])) { + $aliases[$name] = $alias; + } + } } /** @@ -1162,6 +1206,7 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar * @param array $aliases * @param array $paramOuts * @param TypeNode|null $selfOut + * @param array $classTemplates */ private static function parseMethodHierarchyDocs( \ReflectionMethod $ref, @@ -1170,7 +1215,8 @@ private static function parseMethodHierarchyDocs( ?TypeNode &$returnType, array &$aliases, array &$paramOuts = [], - ?TypeNode &$selfOut = null + ?TypeNode &$selfOut = null, + array &$classTemplates = [] ): void { $hierarchy = HierarchyResolver::getMethodHierarchy($ref); $baseParams = $ref->getParameters(); @@ -1188,8 +1234,11 @@ private static function parseMethodHierarchyDocs( } foreach ($hierarchy as $hierRef) { + $hierDeclaringClass = $hierRef->getDeclaringClass(); + self::parseClassLevelDocs($hierDeclaringClass, $classTemplates, $aliases); + $isOriginal = ($hierRef === $ref); - $declaringClass = $hierRef->getDeclaringClass()->getName(); + $declaringClass = $hierDeclaringClass->getName(); $methodName = $hierRef->getName(); $stubDoc = StubManager::getMethodDoc($declaringClass, $methodName); @@ -1579,7 +1628,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof CallableTypeNode) { $parameters = array_map( - fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode( + fn(CallableTypeParameterNode $param) => new CallableTypeParameterNode( self::substituteAliases($param->type, $aliases), $param->isReference, $param->isVariadic, @@ -1613,7 +1662,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof GenericTypeNode) { $genericType = self::substituteAliases($node->type, $aliases); $genericTypes = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->genericTypes ); @@ -1630,7 +1679,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof UnionTypeNode) { $types = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->types ); @@ -1645,7 +1694,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof IntersectionTypeNode) { $types = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->types ); diff --git a/tests/Fixtures/Probe/Route.php b/tests/Fixtures/Probe/Route.php new file mode 100644 index 0000000..2a5413e --- /dev/null +++ b/tests/Fixtures/Probe/Route.php @@ -0,0 +1,18 @@ + + */ +enum Route: string implements RouteInterface +{ + use HasRoute; + + case A = 'a'; + case B = 'b'; +} \ No newline at end of file diff --git a/tests/Fixtures/Probe/RouteClass.php b/tests/Fixtures/Probe/RouteClass.php new file mode 100644 index 0000000..c573da2 --- /dev/null +++ b/tests/Fixtures/Probe/RouteClass.php @@ -0,0 +1,20 @@ + + */ +class RouteClass implements RouteInterface +{ + use HasRoute; + + public static function from(string $name): self + { + return new self(); + } +} \ No newline at end of file diff --git a/tests/Fixtures/Probe/RouteInterface.php b/tests/Fixtures/Probe/RouteInterface.php new file mode 100644 index 0000000..7985fe2 --- /dev/null +++ b/tests/Fixtures/Probe/RouteInterface.php @@ -0,0 +1,16 @@ +toBe(Route::A); + }); + + test('resolves interface template on standard classes fulfilling interface via trait', function () { + $route = RouteClass::fromName('x'); + + expect($route)->toBeInstanceOf(RouteClass::class); + }); + + test('resolves interface template bound when class implements interface via trait without explicit @implements tag', function () { + $route = UnparameterizedRoute::fromName('a'); + + expect($route)->toBe(UnparameterizedRoute::A); + }); +}); \ No newline at end of file From dcc873011aea33cfb5265b5b88d4d37d08aa5e2a Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Fri, 18 Sep 2026 12:23:09 +0800 Subject: [PATCH 4/5] Enhance FunctionContractInjector to support promoted properties for callable constructors --- src/Internal/Ast/FunctionContractInjector.php | 35 +++-- .../PromotedPropertyCallableWrappingTest.php | 126 ++++++++++++++++++ 2 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/TypeChecking/CallablesAndIterators/PromotedPropertyCallableWrappingTest.php diff --git a/src/Internal/Ast/FunctionContractInjector.php b/src/Internal/Ast/FunctionContractInjector.php index 7a6d95d..dff14da 100644 --- a/src/Internal/Ast/FunctionContractInjector.php +++ b/src/Internal/Ast/FunctionContractInjector.php @@ -464,22 +464,33 @@ private static function buildParamWrappers( foreach ($params as $param) { if ($predicate($param, $docText) && $param->var instanceof Node\Expr\Variable && \is_string($param->var->name)) { $paramName = $param->var->name; - $expr = new Node\Stmt\Expression( - new Node\Expr\Assign( - new Node\Expr\Variable($paramName), - new Node\Expr\FuncCall( - new Node\Name\FullyQualified("TypePHP\Internal\RuntimeTypeChecker::{$wrapperMethod}"), - [ - new Node\Arg(new Node\Scalar\MagicConst\Method()), - new Node\Arg(new Node\Scalar\String_($paramName)), - new Node\Arg(new Node\Expr\Variable($paramName)), - new Node\Arg($thisArg), - ] - ) + $assignExpr = new Node\Expr\Assign( + new Node\Expr\Variable($paramName), + new Node\Expr\FuncCall( + new Node\Name\FullyQualified("TypePHP\Internal\RuntimeTypeChecker::{$wrapperMethod}"), + [ + new Node\Arg(new Node\Scalar\MagicConst\Method()), + new Node\Arg(new Node\Scalar\String_($paramName)), + new Node\Arg(new Node\Expr\Variable($paramName)), + new Node\Arg($thisArg), + ] ) ); + + $expr = new Node\Stmt\Expression($assignExpr); $expr->setAttribute('typephp_injected', true); $wrappers[] = $expr; + + if ($param->isPromoted()) { + $propAssign = new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $paramName), + new Node\Expr\Variable($paramName) + ) + ); + $propAssign->setAttribute('typephp_injected', true); + $wrappers[] = $propAssign; + } } } diff --git a/tests/TypeChecking/CallablesAndIterators/PromotedPropertyCallableWrappingTest.php b/tests/TypeChecking/CallablesAndIterators/PromotedPropertyCallableWrappingTest.php new file mode 100644 index 0000000..af2facf --- /dev/null +++ b/tests/TypeChecking/CallablesAndIterators/PromotedPropertyCallableWrappingTest.php @@ -0,0 +1,126 @@ +factory = $factory; + } +} + +final class PromotedClosureClass +{ + /** + * @param Closure(?PromotedA=): PromotedBase $factory + */ + public function __construct(public Closure $factory) + { + } +} + +final class PromotedCallableClass +{ + /** + * @param callable(?PromotedA=): PromotedBase $formatter + */ + public function __construct(public mixed $formatter) + { + } +} + +describe('Promoted Property Callable Contracts & Consistency', function () { + test('enforces parameter type validation when invoking closure on promoted constructor property', function () { + $closure = static fn (?PromotedA $a = null): PromotedBase => new PromotedA(); + + $promoted = new PromotedClosureClass($closure); + + expect(($promoted->factory)(new PromotedA()))->toBeInstanceOf(PromotedBase::class); + + expect(($promoted->factory)())->toBeInstanceOf(PromotedBase::class); + + expect(fn () => ($promoted->factory)(new PromotedUnrelated())) + ->toThrow(TypeError::class) + ; + }); + + test('enforces return type validation when invoking closure on promoted constructor property', function () { + $badClosure = static fn (?PromotedA $a = null): mixed => new PromotedUnrelated(); + + $promoted = new PromotedClosureClass($badClosure); + + expect(fn () => ($promoted->factory)(new PromotedA())) + ->toThrow(TypeError::class) + ; + }); + + test('behaves consistently between standard parameter assignment and promoted property', function () { + $closure = static fn (?PromotedA $a = null): PromotedBase => new PromotedA(); + + $stored = new StoredClosureClass($closure); + $promoted = new PromotedClosureClass($closure); + + expect(fn () => ($stored->factory)(new PromotedUnrelated())) + ->toThrow(TypeError::class) + ; + + expect(fn () => ($promoted->factory)(new PromotedUnrelated())) + ->toThrow(TypeError::class) + ; + }); + + test('enforces contracts when callable is promoted in constructor (Closure, Array Callable, Invokable)', function () { + $helper = new PromotedHelper(); + + $promotedArrayCallable = new PromotedCallableClass([$helper, 'format']); + + expect(($promotedArrayCallable->formatter)(new PromotedA()))->toBeInstanceOf(PromotedBase::class); + + expect(fn () => ($promotedArrayCallable->formatter)(new PromotedUnrelated())) + ->toThrow(TypeError::class) + ; + + $closure = static fn (?PromotedA $a = null): PromotedBase => new PromotedA(); + $promotedClosure = new PromotedCallableClass($closure); + + expect(($promotedClosure->formatter)(new PromotedA()))->toBeInstanceOf(PromotedBase::class); + + expect(fn () => ($promotedClosure->formatter)(new PromotedUnrelated())) + ->toThrow(TypeError::class) + ; + }); +}); From 945f823e587691433ef6fcbaf99669f440dcbfd6 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Fri, 18 Sep 2026 12:23:19 +0800 Subject: [PATCH 5/5] fix code styling --- src/Internal/Checker/ParamChecker.php | 2 +- src/Internal/Docblock/DocblockParser.php | 8 ++++---- src/Internal/Generics/TemplateManager.php | 7 +++---- tests/Fixtures/Probe/Route.php | 2 +- tests/Fixtures/Probe/RouteClass.php | 2 +- tests/Fixtures/Probe/RouteInterface.php | 2 +- tests/Fixtures/Probe/Sub/HasRoute.php | 2 +- tests/Fixtures/Probe/UnparameterizedRoute.php | 2 +- .../MethodTemplateGenericArgumentInferenceTest.php | 4 ++-- .../StaticMethodSubclassGenericInheritanceTest.php | 2 +- ...raitSuppliedInterfaceMethodNamespaceResolutionTest.php | 2 +- 11 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Internal/Checker/ParamChecker.php b/src/Internal/Checker/ParamChecker.php index 6bc6a50..3d57bf6 100644 --- a/src/Internal/Checker/ParamChecker.php +++ b/src/Internal/Checker/ParamChecker.php @@ -1454,4 +1454,4 @@ private static function typeContainsNull(TypeNode $node): bool return false; } -} \ No newline at end of file +} diff --git a/src/Internal/Docblock/DocblockParser.php b/src/Internal/Docblock/DocblockParser.php index 7dbc510..9cb3be9 100644 --- a/src/Internal/Docblock/DocblockParser.php +++ b/src/Internal/Docblock/DocblockParser.php @@ -1628,7 +1628,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof CallableTypeNode) { $parameters = array_map( - fn(CallableTypeParameterNode $param) => new CallableTypeParameterNode( + fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode( self::substituteAliases($param->type, $aliases), $param->isReference, $param->isVariadic, @@ -1662,7 +1662,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof GenericTypeNode) { $genericType = self::substituteAliases($node->type, $aliases); $genericTypes = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->genericTypes ); @@ -1679,7 +1679,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof UnionTypeNode) { $types = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->types ); @@ -1694,7 +1694,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof IntersectionTypeNode) { $types = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->types ); diff --git a/src/Internal/Generics/TemplateManager.php b/src/Internal/Generics/TemplateManager.php index 57d11a0..41ba807 100644 --- a/src/Internal/Generics/TemplateManager.php +++ b/src/Internal/Generics/TemplateManager.php @@ -690,7 +690,6 @@ public static function isBound(string $function, object|string|null $thisOrClass return false; } - /** * Retrieves the bound TypeNode for a template name from instance, static class, or call stack context. */ @@ -1495,7 +1494,7 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref): }; $base = new IdentifierTypeNode($baseName); - $generics = array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->genericTypes); + $generics = array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->genericTypes); return new GenericTypeNode($base, $generics, $n->variances); } @@ -1509,11 +1508,11 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref): } if ($n instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); + return new UnionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); } if ($n instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types)); } return $n; diff --git a/tests/Fixtures/Probe/Route.php b/tests/Fixtures/Probe/Route.php index 2a5413e..99d910f 100644 --- a/tests/Fixtures/Probe/Route.php +++ b/tests/Fixtures/Probe/Route.php @@ -15,4 +15,4 @@ enum Route: string implements RouteInterface case A = 'a'; case B = 'b'; -} \ No newline at end of file +} diff --git a/tests/Fixtures/Probe/RouteClass.php b/tests/Fixtures/Probe/RouteClass.php index c573da2..6dcffd3 100644 --- a/tests/Fixtures/Probe/RouteClass.php +++ b/tests/Fixtures/Probe/RouteClass.php @@ -17,4 +17,4 @@ public static function from(string $name): self { return new self(); } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Probe/RouteInterface.php b/tests/Fixtures/Probe/RouteInterface.php index 7985fe2..2f1e755 100644 --- a/tests/Fixtures/Probe/RouteInterface.php +++ b/tests/Fixtures/Probe/RouteInterface.php @@ -13,4 +13,4 @@ interface RouteInterface * @return TRoute */ public static function fromName(string $name): self; -} \ No newline at end of file +} diff --git a/tests/Fixtures/Probe/Sub/HasRoute.php b/tests/Fixtures/Probe/Sub/HasRoute.php index ca4b920..6f4a7ca 100644 --- a/tests/Fixtures/Probe/Sub/HasRoute.php +++ b/tests/Fixtures/Probe/Sub/HasRoute.php @@ -10,4 +10,4 @@ public static function fromName(string $name): self { return self::from($name); } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Probe/UnparameterizedRoute.php b/tests/Fixtures/Probe/UnparameterizedRoute.php index d75abf5..886dd16 100644 --- a/tests/Fixtures/Probe/UnparameterizedRoute.php +++ b/tests/Fixtures/Probe/UnparameterizedRoute.php @@ -12,4 +12,4 @@ enum UnparameterizedRoute: string implements RouteInterface case A = 'a'; case B = 'b'; -} \ No newline at end of file +} diff --git a/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php b/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php index fb94f02..135998a 100644 --- a/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php +++ b/tests/TypeChecking/Generics/MethodTemplateGenericArgumentInferenceTest.php @@ -75,7 +75,7 @@ public function unwrapAll(array $items): array test('infers method template T from generic argument PBox instead of falling back to upper bound PBase', function () { $consumer = new MethodTemplateConsumer(); $boxA = new MethodTemplateBox(new MethodTemplateA('Alpha')); - + $result = $consumer->unwrap($boxA); expect($result)->toBeInstanceOf(MethodTemplateA::class) @@ -110,4 +110,4 @@ public function unwrapAll(array $items): array ->and($result[1])->toBeInstanceOf(MethodTemplateA::class) ; }); -}); \ No newline at end of file +}); diff --git a/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php b/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php index 18e6f39..7a1c3c6 100644 --- a/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php +++ b/tests/TypeChecking/Generics/StaticMethodSubclassGenericInheritanceTest.php @@ -141,4 +141,4 @@ final class StaticGenericAnyId extends StaticGenericIdBase ; }); }); -}); \ No newline at end of file +}); diff --git a/tests/TypeChecking/Generics/TraitSuppliedInterfaceMethodNamespaceResolutionTest.php b/tests/TypeChecking/Generics/TraitSuppliedInterfaceMethodNamespaceResolutionTest.php index 9e8386b..896737f 100644 --- a/tests/TypeChecking/Generics/TraitSuppliedInterfaceMethodNamespaceResolutionTest.php +++ b/tests/TypeChecking/Generics/TraitSuppliedInterfaceMethodNamespaceResolutionTest.php @@ -26,4 +26,4 @@ expect($route)->toBe(UnparameterizedRoute::A); }); -}); \ No newline at end of file +});