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
35 changes: 23 additions & 12 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
166 changes: 139 additions & 27 deletions src/Internal/Checker/ParamChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TemplateTagValueNode> $allTemplates */
$allTemplates = [...$classTemplates, ...$methodTemplates];
Expand All @@ -175,7 +175,7 @@ public static function checkParams(
}

$boundTemplates = (\count($allTemplates) > 0)
? TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates)
? TemplateManager::getBoundTemplates($effectiveFunction, $thisOrClass, $allTemplates)
: [];
$declaredTemplates = $allTemplates;

Expand All @@ -192,7 +192,8 @@ public static function checkParams(
$boundTemplates,
$declaredTemplates,
$registry,
$classTemplates
$classTemplates,
$thisOrClass
);
}

Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -288,13 +295,10 @@ private static function resolveBaseTypes(
* @param array<string, TypeNode> $contractTypes
* @param array<string, TypeNode> $baseTypes
* @param array<string, mixed> $vars
* @param string $effectiveFunction
* @param object|null $thisObj
* @param array<string, TemplateTagValueNode> $allTemplates
* @param array<string, TypeNode> $aliases
* @param array<string, TypeNode> $boundTemplates
* @param array<string, TemplateTagValueNode> $declaredTemplates
* @param TypeValidatorRegistry $registry
* @param array<string, TemplateTagValueNode> $classTemplates
*/
private static function validateAllParameters(
Expand All @@ -308,15 +312,16 @@ 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)) {
continue;
}

$currentBoundTemplates = (\count($allTemplates) > 0)
? TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates)
? TemplateManager::getBoundTemplates($effectiveFunction, $thisOrClass, $allTemplates)
: $boundTemplates;

$err = self::validateSingleParam(
Expand All @@ -331,7 +336,8 @@ private static function validateAllParameters(
$declaredTemplates,
$registry,
$classTemplates,
$vars
$vars,
$thisOrClass
);

if ($err !== null) {
Expand Down Expand Up @@ -440,7 +446,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<string, TypeNode> $types
* @param array<string, mixed> $vars
Expand Down Expand Up @@ -468,8 +474,9 @@ 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);
}
}

Expand Down Expand Up @@ -743,6 +750,107 @@ private static function bindTemplateIfUnbound(
}
}

/**
* Pre-infers generic template parameters from generic object arguments (e.g. PBox<T> or array<K, PBox<T>>).
*
* @param array<string, TypeNode> $types
* @param array<string, mixed> $vars
* @param array<string, TemplateTagValueNode> $templates
* @param array<string, TemplateTagValueNode> $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<string, TemplateTagValueNode> $templates
* @param array<string, TemplateTagValueNode> $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.
*
Expand All @@ -766,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 ||
Expand Down Expand Up @@ -800,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);
Expand Down Expand Up @@ -1055,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])) {
Expand All @@ -1066,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,
Expand All @@ -1097,7 +1207,7 @@ private static function resolveTemplateParam(
$paramName,
$function,
$thisObj,
$targetObj,
$targetObjOrClass,
$templateName,
$templateNode,
$isVariadic,
Expand Down Expand Up @@ -1154,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,
Expand All @@ -1166,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;
}
Expand All @@ -1179,7 +1291,7 @@ private static function validateBoundTemplate(
$paramName,
$function,
$thisObj,
$targetObj,
$targetObjOnly,
$templateName,
$templateNode,
$expectedTypeNode,
Expand All @@ -1199,7 +1311,7 @@ private static function validateBoundTemplate(
$isClassLevelTemplate,
$function,
$thisObj,
$targetObj,
$targetObjOnly,
$templateName,
$context,
$err,
Expand Down
Loading
Loading