Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ jobs:
run: ./vendor/bin/phpstan analyse --no-progress
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'

- name: Warm Up TypePHP Cache
run: php bin/typephp cache:warm

- name: Run Test Suite with Coverage (Pest)
run: ./vendor/bin/pest --coverage-clover=clover.xml
run: ./vendor/bin/pest --coverage-clover=clover.xml --compact
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.4'

- name: Upload Coverage to Codecov
Expand Down
46 changes: 25 additions & 21 deletions src/Contract/ContractParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class ContractParser
/**
* Cache for resolved contract metadata.
*
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}>
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}>
*/
private static array $cache = [];

Expand Down Expand Up @@ -64,7 +64,7 @@ public static function reset(): void
/**
* Parses PHPDoc contracts for a function or class method.
*
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
*/
public static function parse(string $function): array
{
Expand All @@ -83,25 +83,26 @@ public static function parse(string $function): array
$ref = $refClass->getMethod($methodName);
$contract = self::parseMethod($ref);
} else {
$templates = [];
$classTemplates = [];
$aliases = [];
self::parseClassLevelDocs($refClass, $templates, $aliases);
self::parseClassLevelDocs($refClass, $classTemplates, $aliases);
$contract = [
'types' => [],
'templates' => $templates,
'templates' => [],
'classTemplates' => $classTemplates,
'return' => null,
'aliases' => $aliases,
];
}
} else {
$contract = ['types' => [], 'templates' => [], 'return' => null, 'aliases' => []];
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
}
} else {
$ref = new \ReflectionFunction($function);
$contract = self::parseFunction($ref);
}
} catch (\ReflectionException $e) {
$contract = ['types' => [], 'templates' => [], 'return' => null, 'aliases' => []];
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
}

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

$aliases = [];
$templates = [];
self::parseClassLevelDocs($declaringClass, $templates, $aliases);
$classTemplates = [];
self::parseClassLevelDocs($declaringClass, $classTemplates, $aliases);

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

$aliases = [];
$templates = [];
self::parseClassLevelDocs($declaringClass, $templates, $aliases);
$classTemplates = [];
self::parseClassLevelDocs($declaringClass, $classTemplates, $aliases);

$phpDocNode = DocblockExtractor::parseDocString($doc);
DocblockExtractor::extractAliases($phpDocNode, $aliases, $declaringClass);
Expand All @@ -294,7 +295,7 @@ public static function parseMagicMethod(string $className, string $methodName):
'return' => $resolvedReturn,
'parameters' => $resolvedParams,
'aliases' => $aliases,
'templates' => $templates,
'templates' => $classTemplates,
];
} catch (\Throwable $e) {
return self::$magicMethodCache[$cacheKey] = null;
Expand Down Expand Up @@ -415,8 +416,8 @@ public static function parseClassAliases(string $className): array
/** @var class-string<object> $className */
$refClass = new \ReflectionClass($className);
$aliases = [];
$templates = [];
self::parseClassLevelDocs($refClass, $templates, $aliases);
$classTemplates = [];
self::parseClassLevelDocs($refClass, $classTemplates, $aliases);

return $aliases;
} catch (\Throwable $e) {
Expand All @@ -427,25 +428,27 @@ public static function parseClassAliases(string $className): array
/**
* Orchestrates parsing for class methods across the inheritance hierarchy.
*
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
*/
private static function parseMethod(\ReflectionMethod $ref): array
{
$types = [];
$templates = [];
$methodTemplates = [];
$classTemplates = [];
$returnType = null;
$aliases = [];

self::parseClassLevelDocs($ref->getDeclaringClass(), $templates, $aliases);
self::parseMethodHierarchyDocs($ref, $types, $templates, $returnType, $aliases);
self::parseClassLevelDocs($ref->getDeclaringClass(), $classTemplates, $aliases);
self::parseMethodHierarchyDocs($ref, $types, $methodTemplates, $returnType, $aliases);

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

return [
'types' => $types,
'templates' => $templates,
'templates' => $methodTemplates,
'classTemplates' => $classTemplates,
'return' => $returnType,
'aliases' => $aliases,
];
Expand All @@ -454,7 +457,7 @@ private static function parseMethod(\ReflectionMethod $ref): array
/**
* Orchestrates parsing for standalone global or namespaced functions.
*
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
*/
private static function parseFunction(\ReflectionFunction $ref): array
{
Expand All @@ -468,6 +471,7 @@ private static function parseFunction(\ReflectionFunction $ref): array
return [
'types' => [],
'templates' => [],
'classTemplates' => [],
'return' => null,
'aliases' => [],
];
Expand Down Expand Up @@ -505,6 +509,7 @@ private static function parseFunction(\ReflectionFunction $ref): array
return [
'types' => $types,
'templates' => $templates,
'classTemplates' => [],
'return' => $returnType,
'aliases' => $aliases,
];
Expand Down Expand Up @@ -543,7 +548,6 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar

/**
* Resolves method-level docblocks (@param, @return, @template, aliases) up the method hierarchy.
* Prioritizes @phpstan-param and @psalm-param over standard @param tags.
*
* @param \ReflectionMethod $ref
* @param array<string, TypeNode> $types
Expand Down
8 changes: 4 additions & 4 deletions src/Internal/Checker/GeneratorChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ private static function resolveGeneratorReturnType(string $function, object|stri
}

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

if (\count($boundTemplates) > 0 || \count($templates) > 0) {
$returnTypeNode = TemplateSubstitutor::substitute($returnTypeNode, $boundTemplates, $templates);
if (\count($boundTemplates) > 0 || \count($allTemplates) > 0) {
$returnTypeNode = TemplateSubstitutor::substitute($returnTypeNode, $boundTemplates, $allTemplates);
$returnTypeNode = SpecialTypeResolver::resolve($returnTypeNode, $function, $thisObj);
}

Expand Down
76 changes: 42 additions & 34 deletions src/Internal/Checker/ParamChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,24 @@ public static function checkParams(
return null;
}

$templates = $contract['templates'];
$methodTemplates = $contract['templates'];
$classTemplates = $contract['classTemplates'] ?? [];
$aliases = $contract['aliases'];

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

$boundTemplates = TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $templates);
$declaredTemplates = $templates;
if ($thisObj !== null && \count($classTemplates) > 0 && str_contains($effectiveFunction, '::')) {
$declaringClass = explode('::', $effectiveFunction, 2)[0];
TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass);
}

$allTemplates = [...$classTemplates, ...$methodTemplates];
self::preInferGenericArrayTemplates($contract['types'], $vars, $effectiveFunction, $thisObj, $allTemplates);

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

foreach ($contract['types'] as $paramName => $typeNode) {
if (! \array_key_exists($paramName, $vars)) {
Expand All @@ -72,7 +82,7 @@ public static function checkParams(
$vars[$paramName],
$effectiveFunction,
$thisObj,
$templates,
$allTemplates,
$aliases,
$boundTemplates,
$declaredTemplates,
Expand Down Expand Up @@ -170,21 +180,6 @@ private static function handleMagicCall(
return self::validateMagicArguments($magicContract, $magicArgs, $magicFunction, $thisObj, $registry);
}

/**
* Initializes call stack frames or resolves inherited template bounds.
*
* @param array<string, TemplateTagValueNode> $templates
*/
private static function initializeCallContext(string $effectiveFunction, ?object $thisObj, array $templates): void
{
if ($thisObj === null && \count($templates) > 0) {
TemplateManager::clearCallBindings($effectiveFunction, $templates);
} elseif ($thisObj !== null && str_contains($effectiveFunction, '::')) {
$declaringClass = explode('::', $effectiveFunction, 2)[0];
TemplateManager::resolveInheritedTemplates($thisObj, $declaringClass);
}
}

/**
* Pre-infers generic template parameters from array arguments before callback wrapping.
*
Expand Down Expand Up @@ -227,7 +222,7 @@ private static function inferFromTypeNode(
): void {
if ($typeNode instanceof GenericTypeNode) {
$baseType = strtolower($typeNode->type->name);
if (! \in_array($baseType, ['array', 'list', 'iterable', 'traversable'], strict: true)) {
if (! \in_array($baseType, ['array', 'list', 'iterable', 'traversable'], true)) {
return;
}

Expand Down Expand Up @@ -259,8 +254,13 @@ private static function bindTemplateIfUnbound(
?object $thisObj,
array $templates
): void {
if (isset($templates[$templateName]) && ! TemplateManager::isBound($effectiveFunction, $thisObj, $templateName)) {
TemplateManager::bindTemplate($effectiveFunction, $thisObj, $templateName, TemplateManager::inferTypeFromValue($sampleValue));
$contract = ContractParser::parse($effectiveFunction);
$classTemplates = $contract['classTemplates'] ?? [];
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;

if (isset($templates[$templateName]) && ! TemplateManager::isBound($effectiveFunction, $targetObj, $templateName)) {
TemplateManager::bindTemplate($effectiveFunction, $targetObj, $templateName, TemplateManager::inferTypeFromValue($sampleValue));
}
}

Expand Down Expand Up @@ -332,8 +332,6 @@ private static function validateMagicArguments(
$aliases = $magicContract['aliases'];
$parameters = $magicContract['parameters'];

self::initializeCallContext($function, $thisObj, $templates);

$argValues = array_values($args);
$argKeys = array_keys($args);

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

if (! TemplateManager::isBound($function, $thisObj, $templateName)) {
$contract = ContractParser::parse($function);
$classTemplates = $contract['classTemplates'] ?? [];
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;

if (! TemplateManager::isBound($function, $targetObj, $templateName)) {
if (! \is_string($val) || ! ClassNameValidator::isValid($val) || (! class_exists($val) && ! interface_exists($val) && ! trait_exists($val) && ! enum_exists($val))) {
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' must be a valid class-string, ' . TypeFormatter::formatGivenValue($val) . ' given');
}
Expand All @@ -438,17 +441,17 @@ private static function resolveClassStringTemplate(
$boundName = $resolvedBound instanceof IdentifierTypeNode ? $resolvedBound->name : (string) $resolvedBound;
$lowerBound = strtolower($boundName);

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

TemplateManager::bindTemplate($function, $thisObj, $templateName, new IdentifierTypeNode($val));
TemplateManager::bindTemplate($function, $targetObj, $templateName, new IdentifierTypeNode($val));
} else {
$expectedTypeNode = TemplateManager::getBoundType($function, $thisObj, $templateName);
$expectedTypeNode = TemplateManager::getBoundType($function, $targetObj, $templateName);
$targetClass = $expectedTypeNode instanceof IdentifierTypeNode ? $expectedTypeNode->name : (string) $expectedTypeNode;

if (! \is_string($val) || ! is_a($val, $targetClass, allow_string: true)) {
if (! \is_string($val) || ! is_a($val, $targetClass, true)) {
$valStr = TypeFormatter::formatGivenValue($val);

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

if (! TemplateManager::isBound($function, $thisObj, $templateName)) {
$contract = ContractParser::parse($function);
$classTemplates = $contract['classTemplates'] ?? [];
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;

if (! TemplateManager::isBound($function, $targetObj, $templateName)) {
$sampleVal = ($isVariadic && \is_array($val)) ? ($val[0] ?? null) : $val;
$inferredType = TemplateManager::inferTypeFromValue($sampleVal);

Expand All @@ -506,7 +514,7 @@ private static function resolveTemplateParam(
}
}

TemplateManager::bindTemplate($function, $thisObj, $templateName, $inferredType);
TemplateManager::bindTemplate($function, $targetObj, $templateName, $inferredType);

if ($isVariadic && \is_array($val)) {
foreach ($val as $idx => $item) {
Expand All @@ -517,7 +525,7 @@ private static function resolveTemplateParam(
}
}
} else {
$expectedTypeNode = TemplateManager::getBoundType($function, $thisObj, $templateName);
$expectedTypeNode = TemplateManager::getBoundType($function, $targetObj, $templateName);
if ($expectedTypeNode === null) {
return null;
}
Expand Down
8 changes: 5 additions & 3 deletions src/Internal/Checker/ReturnChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ public static function checkReturn(
return $value;
}

$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];

return self::evaluateReturn(
$returnTypeNode,
$value,
$effectiveFunction,
$thisObj,
$vars,
$contract['aliases'] ?? [],
$contract['templates'] ?? [],
$allTemplates,
$registry,
$wrapIterableCallback
);
Expand Down Expand Up @@ -230,7 +232,7 @@ private static function evaluateReturn(
}

$genericIterables = ['iterable', 'traversable', 'iterator', 'generator'];
if (\in_array($baseName, $genericIterables, strict: true)) {
if (\in_array($baseName, $genericIterables, true)) {
return $wrapIterableCallback($function, 'return', $value);
}
}
Expand Down Expand Up @@ -314,7 +316,7 @@ private static function resolveTemplateConditional(
$isTargetMatch = ClassNameValidator::isValid($subStr) && ClassNameValidator::isValid($targetStr) &&
(class_exists($subStr) || interface_exists($subStr)) &&
(class_exists($targetStr) || interface_exists($targetStr)) &&
is_a($subStr, $targetStr, allow_string: true);
is_a($subStr, $targetStr, true);
}

if ($node->negated) {
Expand Down
Loading
Loading