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
153 changes: 153 additions & 0 deletions src/Internal/Checker/ConditionalChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

namespace TypePHP\Internal\Checker;

use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use ReflectionClass;
use Throwable;
use TypePHP\Internal\Generics\TemplateManager;
use TypePHP\Internal\Resolver\HierarchyResolver;
use TypePHP\Internal\Validator\TypeValidatorRegistry;

/**
* @internal Evaluates parameter-based and template-based conditional types for parameters and returns.
*/
final class ConditionalChecker
{
/**
* Recursively resolves multi-branch nested conditional types ($param is Target ? A : B or T is Target ? A : B).
*
* @param array<int|string, mixed> $vars
* @param array<string, TypeNode> $boundTemplates
*/
public static function resolve(
TypeNode $typeNode,
array $vars,
array $boundTemplates,
TypeValidatorRegistry $registry,
string $function = ''
): TypeNode {
if ($typeNode instanceof ConditionalTypeForParameterNode) {
return self::resolveParameterConditional($typeNode, $vars, $boundTemplates, $registry, $function);
}

if ($typeNode instanceof ConditionalTypeNode) {
return self::resolveTemplateConditional($typeNode, $vars, $boundTemplates, $registry, $function);
}

return $typeNode;
}

/**
* Resolves parameter-based conditional types ($param is Target ? If : Else).
*
* @param array<int|string, mixed> $vars
* @param array<string, TypeNode> $boundTemplates
*/
public static function resolveParameterConditional(
ConditionalTypeForParameterNode $node,
array $vars,
array $boundTemplates,
TypeValidatorRegistry $registry,
string $function = ''
): TypeNode {
$paramName = ltrim($node->parameterName, '$');
$paramValue = null;

if (isset($vars[$paramName]) || \array_key_exists($paramName, $vars)) {
$paramValue = $vars[$paramName];
} elseif (\count($vars) > 0 && $function !== '' && str_contains($function, '::')) {
$paramValue = self::resolveRenamedParamValue($function, $paramName, $vars);
}

$targetErr = $registry->validate($paramValue, $node->targetType, 'condition');
$isTargetMatch = ($targetErr === null);
if ($node->negated) {
$isTargetMatch = ! $isTargetMatch;
}

$selectedBranch = $isTargetMatch ? $node->if : $node->else;

return self::resolve($selectedBranch, $vars, $boundTemplates, $registry, $function);
}

/**
* Resolves template-based conditional types (T is Target ? If : Else).
*
* @param array<int|string, mixed> $vars
* @param array<string, TypeNode> $boundTemplates
*/
public static function resolveTemplateConditional(
ConditionalTypeNode $node,
array $vars,
array $boundTemplates,
TypeValidatorRegistry $registry,
string $function = ''
): TypeNode {
$subjectTypeNode = $node->subjectType;
if ($subjectTypeNode instanceof IdentifierTypeNode && isset($boundTemplates[$subjectTypeNode->name])) {
$subjectTypeNode = $boundTemplates[$subjectTypeNode->name];
}

$isTargetMatch = TemplateManager::checkVariance($subjectTypeNode, $node->targetType, GenericTypeNode::VARIANCE_COVARIANT);
if ($node->negated) {
$isTargetMatch = ! $isTargetMatch;
}

$selectedBranch = $isTargetMatch ? $node->if : $node->else;

return self::resolve($selectedBranch, $vars, $boundTemplates, $registry, $function);
}

/**
* Disambiguates parameter value by positional index in method hierarchy when renamed in child class.
*
* @param array<int|string, mixed> $vars
*/
private static function resolveRenamedParamValue(string $function, string $paramName, array $vars): mixed
{
[$className, $methodName] = explode('::', $function, 2);
if (! class_exists($className) && ! interface_exists($className) && ! trait_exists($className) && ! enum_exists($className)) {
return null;
}

try {
/** @var class-string<object> $className */
$refClass = new ReflectionClass($className);
if (! $refClass->hasMethod($methodName)) {
return null;
}

$refMethod = $refClass->getMethod($methodName);
$hierarchy = HierarchyResolver::getMethodHierarchy($refMethod);

$targetIndex = null;
foreach ($hierarchy as $hierMethod) {
foreach ($hierMethod->getParameters() as $idx => $p) {
if ($p->getName() === $paramName) {
$targetIndex = $idx;

break 2;
}
}
}

if ($targetIndex !== null) {
$values = array_values($vars);
if (isset($values[$targetIndex]) || \array_key_exists($targetIndex, $values)) {
return $values[$targetIndex];
}
}
} catch (Throwable $e) {
// Silently ignore reflection errors
}

return null;
}
}
43 changes: 38 additions & 5 deletions src/Internal/Checker/ParamChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
Expand Down Expand Up @@ -209,6 +211,13 @@ private static function validateSimpleParams(
): ?ErrorMessage {
foreach ($types as $paramName => $typeNode) {
if (isset($vars[$paramName]) || \array_key_exists($paramName, $vars)) {
if (
$typeNode instanceof ConditionalTypeForParameterNode ||
$typeNode instanceof ConditionalTypeNode
) {
$typeNode = ConditionalChecker::resolve($typeNode, $vars, [], $registry, $effectiveFunction);
}

if (self::isUnconstrained($typeNode)) {
continue;
}
Expand Down Expand Up @@ -307,6 +316,10 @@ private static function validateAllParameters(
continue;
}

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

$err = self::validateSingleParam(
$paramName,
$baseTypes[$paramName],
Expand All @@ -315,10 +328,11 @@ private static function validateAllParameters(
$thisObj,
$allTemplates,
$aliases,
$boundTemplates,
$currentBoundTemplates,
$declaredTemplates,
$registry,
$classTemplates
$classTemplates,
$vars
);

if ($err !== null) {
Expand Down Expand Up @@ -738,7 +752,9 @@ private static function bindTemplateIfUnbound(
* @param array<string, TypeNode> $aliases
* @param array<string, TypeNode> $boundTemplates
* @param array<string, TemplateTagValueNode> $declaredTemplates
* @param TypeValidatorRegistry $registry
* @param array<string, TemplateTagValueNode> $classTemplates
* @param array<int|string, mixed> $vars
*/
private static function validateSingleParam(
string $paramName,
Expand All @@ -751,8 +767,16 @@ private static function validateSingleParam(
array $boundTemplates,
array $declaredTemplates,
TypeValidatorRegistry $registry,
array $classTemplates = []
array $classTemplates = [],
array $vars = []
): ?ErrorMessage {
if (
$typeNode instanceof ConditionalTypeForParameterNode ||
$typeNode instanceof ConditionalTypeNode
) {
$typeNode = ConditionalChecker::resolve($typeNode, $vars, $boundTemplates, $registry, $effectiveFunction);
}

if (self::isUnconstrained($typeNode)) {
return null;
}
Expand All @@ -766,6 +790,13 @@ private static function validateSingleParam(
$typeNode = SpecialTypeResolver::resolve($typeNode, $effectiveFunction, $thisObj);
}

if (
$typeNode instanceof ConditionalTypeForParameterNode ||
$typeNode instanceof ConditionalTypeNode
) {
$typeNode = ConditionalChecker::resolve($typeNode, $vars, $boundTemplates, $registry, $effectiveFunction);
}

if ($typeNode instanceof GenericTypeNode && self::isClassStringTemplate($typeNode, $templates)) {
return self::resolveClassStringTemplate($typeNode, $val, $paramName, $effectiveFunction, $thisObj, $templates, $classTemplates);
}
Expand Down Expand Up @@ -825,7 +856,9 @@ private static function validateMagicArguments(
$aliases,
$boundTemplates,
$declaredTemplates,
$registry
$registry,
[],
$args
);

if ($err !== null) {
Expand Down Expand Up @@ -1251,7 +1284,7 @@ private static function tryWidenTemplate(
return null;
}

return $boundErr;
return $originalError;
}

/**
Expand Down
Loading
Loading