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
23 changes: 23 additions & 0 deletions src/Internal/Checker/ConditionalChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use ReflectionClass;
use Throwable;
use TypePHP\Internal\Docblock\DocblockParser;
use TypePHP\Internal\Generics\TemplateManager;
use TypePHP\Internal\Resolver\HierarchyResolver;
use TypePHP\Internal\Validator\TypeValidatorRegistry;
Expand Down Expand Up @@ -293,6 +294,28 @@ public static function resolveTemplateConditional(
}

$isTargetMatch = TemplateManager::checkVariance($subjectTypeNode, $node->targetType, GenericTypeNode::VARIANCE_COVARIANT);

if (! $isTargetMatch && $node->targetType instanceof \PHPStan\PhpDocParser\Ast\Type\ConstTypeNode && $function !== '' && \count($vars) > 0) {
$contract = DocblockParser::parse($function);
foreach ($contract['types'] as $pName => $pType) {
$pTemplateName = ($pType instanceof IdentifierTypeNode) ? $pType->name : null;
$matchesSubject = false;

if ($pTemplateName !== null && $node->subjectType instanceof IdentifierTypeNode) {
$matchesSubject = ($pTemplateName === $node->subjectType->name)
|| (isset($boundTemplates[$pTemplateName]) && (string) $boundTemplates[$pTemplateName] === (string) $node->subjectType);
}

if ($matchesSubject && (isset($vars[$pName]) || \array_key_exists($pName, $vars))) {
if ($registry->validate($vars[$pName], $node->targetType, 'condition') === null) {
$isTargetMatch = true;

break;
}
}
}
}

if ($node->negated) {
$isTargetMatch = ! $isTargetMatch;
}
Expand Down
31 changes: 31 additions & 0 deletions src/Internal/Checker/ParamChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,35 @@ public static function checkParams(
);
}

private static function refineInferredTypeFromBound(mixed $val, TypeNode $bound, TypeNode $defaultInferred): TypeNode
{
if ($bound instanceof \PHPStan\PhpDocParser\Ast\Type\ConstTypeNode) {
$expectedVal = match (true) {
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode => $bound->constExpr->value,
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode => (int) $bound->constExpr->value,
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode => true,
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode => false,
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode => null,
default => null,
};

if ($expectedVal !== null && $val === $expectedVal) {
return $bound;
}
}

if ($bound instanceof UnionTypeNode) {
foreach ($bound->types as $member) {
$refined = self::refineInferredTypeFromBound($val, $member, $defaultInferred);
if ($refined !== $defaultInferred) {
return $refined;
}
}
}

return $defaultInferred;
}

/**
* Validates simple parameters when no generics/aliases are involved.
*
Expand Down Expand Up @@ -1442,6 +1471,8 @@ private static function bindInitialTemplate(

if ($templateNode->bound !== null) {
$resolvedBound = SpecialTypeResolver::resolve($templateNode->bound, $function, $thisObj);
$inferredType = self::refineInferredTypeFromBound($sampleVal, $resolvedBound, $inferredType);

$err = $registry->validate($sampleVal, $resolvedBound, $function . '(): Argument $' . $paramName . ' (template ' . $templateName . ')');
if ($err !== null) {
return $err;
Expand Down
8 changes: 7 additions & 1 deletion src/Internal/Checker/ReturnChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ private static function evaluateReturn(
$resolvedType = $aliases[$resolvedType->name];
}

if (ConditionalChecker::containsConditional($resolvedType)) {
$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
}

if (\count($boundTemplates) === 0 && \count($templates) > 0 && ! $hasConditionals && $thisObj === null) {
$resolvedType = self::$unboundReturnCache[$function] ??= SpecialTypeResolver::resolve(
TemplateSubstitutor::substitute($returnTypeNode, [], $templates),
Expand All @@ -419,7 +423,9 @@ private static function evaluateReturn(
$resolvedType = SpecialTypeResolver::resolve($resolvedType, $function, $thisObj);
}

$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
if (ConditionalChecker::containsConditional($resolvedType)) {
$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
}

if ($sig !== null) {
self::$substitutedReturnCache[$function][$sig] = $resolvedType;
Expand Down
70 changes: 70 additions & 0 deletions src/Internal/Generics/TemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
namespace TypePHP\Internal\Generics;

use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
Expand Down Expand Up @@ -1573,6 +1580,69 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref):
return new IntersectionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types));
}

if ($n instanceof ArrayShapeNode) {
$items = array_map(function ($item) use ($ref) {
return new ArrayShapeItemNode(
$item->keyName,
$item->optional,
self::resolveTypeNodeAst($item->valueType, $ref)
);
}, $n->items);

$unsealed = null;
if ($n->unsealedType !== null) {
$unsealedKey = $n->unsealedType->keyType !== null
? self::resolveTypeNodeAst($n->unsealedType->keyType, $ref)
: null;
$unsealedVal = self::resolveTypeNodeAst($n->unsealedType->valueType, $ref);
$unsealed = new ArrayShapeUnsealedTypeNode($unsealedVal, $unsealedKey);
}

if ($n->sealed) {
return ArrayShapeNode::createSealed($items, $n->kind);
}

return ArrayShapeNode::createUnsealed($items, $unsealed, $n->kind);
}

if ($n instanceof ObjectShapeNode) {
$items = array_map(function ($item) use ($ref) {
return new ObjectShapeItemNode(
$item->keyName,
$item->optional,
self::resolveTypeNodeAst($item->valueType, $ref)
);
}, $n->items);

return new ObjectShapeNode($items);
}

if ($n instanceof CallableTypeNode) {
$params = array_map(function ($p) use ($ref) {
return new CallableTypeParameterNode(
self::resolveTypeNodeAst($p->type, $ref),
$p->isReference,
$p->isVariadic,
$p->parameterName,
$p->isOptional
);
}, $n->parameters);

return new CallableTypeNode(
$n->identifier,
$params,
self::resolveTypeNodeAst($n->returnType, $ref),
$n->templateTypes
);
}

if ($n instanceof OffsetAccessTypeNode) {
return new OffsetAccessTypeNode(
self::resolveTypeNodeAst($n->type, $ref),
self::resolveTypeNodeAst($n->offset, $ref)
);
}

return $n;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ function testGlobalConstantIntRange(int $n): int
;

expect(fn () => testGlobalConstantIntRange(0))
->toThrow(TypeError::class, 'Argument $n must be >= 1, 0 given');
->toThrow(TypeError::class, 'Argument $n must be >= 1, 0 given')
;
});
});
});
Loading
Loading