Skip to content

Commit 88fcb12

Browse files
authored
Internal improvements 36 (#96)
* Add conditional type tests for literal templates and refine inferred types in ParamChecker * fix code styling * Enhance TemplateManager with support for ArrayShape, ObjectShape, CallableType, and OffsetAccess types; add comprehensive tests for inherited generic shapes and namespace resolution
1 parent cc351b6 commit 88fcb12

7 files changed

Lines changed: 670 additions & 2 deletions

File tree

‎src/Internal/Checker/ConditionalChecker.php‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
2424
use ReflectionClass;
2525
use Throwable;
26+
use TypePHP\Internal\Docblock\DocblockParser;
2627
use TypePHP\Internal\Generics\TemplateManager;
2728
use TypePHP\Internal\Resolver\HierarchyResolver;
2829
use TypePHP\Internal\Validator\TypeValidatorRegistry;
@@ -293,6 +294,28 @@ public static function resolveTemplateConditional(
293294
}
294295

295296
$isTargetMatch = TemplateManager::checkVariance($subjectTypeNode, $node->targetType, GenericTypeNode::VARIANCE_COVARIANT);
297+
298+
if (! $isTargetMatch && $node->targetType instanceof \PHPStan\PhpDocParser\Ast\Type\ConstTypeNode && $function !== '' && \count($vars) > 0) {
299+
$contract = DocblockParser::parse($function);
300+
foreach ($contract['types'] as $pName => $pType) {
301+
$pTemplateName = ($pType instanceof IdentifierTypeNode) ? $pType->name : null;
302+
$matchesSubject = false;
303+
304+
if ($pTemplateName !== null && $node->subjectType instanceof IdentifierTypeNode) {
305+
$matchesSubject = ($pTemplateName === $node->subjectType->name)
306+
|| (isset($boundTemplates[$pTemplateName]) && (string) $boundTemplates[$pTemplateName] === (string) $node->subjectType);
307+
}
308+
309+
if ($matchesSubject && (isset($vars[$pName]) || \array_key_exists($pName, $vars))) {
310+
if ($registry->validate($vars[$pName], $node->targetType, 'condition') === null) {
311+
$isTargetMatch = true;
312+
313+
break;
314+
}
315+
}
316+
}
317+
}
318+
296319
if ($node->negated) {
297320
$isTargetMatch = ! $isTargetMatch;
298321
}

‎src/Internal/Checker/ParamChecker.php‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,35 @@ public static function checkParams(
220220
);
221221
}
222222

223+
private static function refineInferredTypeFromBound(mixed $val, TypeNode $bound, TypeNode $defaultInferred): TypeNode
224+
{
225+
if ($bound instanceof \PHPStan\PhpDocParser\Ast\Type\ConstTypeNode) {
226+
$expectedVal = match (true) {
227+
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode => $bound->constExpr->value,
228+
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode => (int) $bound->constExpr->value,
229+
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode => true,
230+
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode => false,
231+
$bound->constExpr instanceof \PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode => null,
232+
default => null,
233+
};
234+
235+
if ($expectedVal !== null && $val === $expectedVal) {
236+
return $bound;
237+
}
238+
}
239+
240+
if ($bound instanceof UnionTypeNode) {
241+
foreach ($bound->types as $member) {
242+
$refined = self::refineInferredTypeFromBound($val, $member, $defaultInferred);
243+
if ($refined !== $defaultInferred) {
244+
return $refined;
245+
}
246+
}
247+
}
248+
249+
return $defaultInferred;
250+
}
251+
223252
/**
224253
* Validates simple parameters when no generics/aliases are involved.
225254
*
@@ -1442,6 +1471,8 @@ private static function bindInitialTemplate(
14421471

14431472
if ($templateNode->bound !== null) {
14441473
$resolvedBound = SpecialTypeResolver::resolve($templateNode->bound, $function, $thisObj);
1474+
$inferredType = self::refineInferredTypeFromBound($sampleVal, $resolvedBound, $inferredType);
1475+
14451476
$err = $registry->validate($sampleVal, $resolvedBound, $function . '(): Argument $' . $paramName . ' (template ' . $templateName . ')');
14461477
if ($err !== null) {
14471478
return $err;

‎src/Internal/Checker/ReturnChecker.php‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ private static function evaluateReturn(
408408
$resolvedType = $aliases[$resolvedType->name];
409409
}
410410

411+
if (ConditionalChecker::containsConditional($resolvedType)) {
412+
$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
413+
}
414+
411415
if (\count($boundTemplates) === 0 && \count($templates) > 0 && ! $hasConditionals && $thisObj === null) {
412416
$resolvedType = self::$unboundReturnCache[$function] ??= SpecialTypeResolver::resolve(
413417
TemplateSubstitutor::substitute($returnTypeNode, [], $templates),
@@ -419,7 +423,9 @@ private static function evaluateReturn(
419423
$resolvedType = SpecialTypeResolver::resolve($resolvedType, $function, $thisObj);
420424
}
421425

422-
$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
426+
if (ConditionalChecker::containsConditional($resolvedType)) {
427+
$resolvedType = ConditionalChecker::resolve($resolvedType, $vars, $boundTemplates, $registry, $function);
428+
}
423429

424430
if ($sig !== null) {
425431
self::$substitutedReturnCache[$function][$sig] = $resolvedType;

‎src/Internal/Generics/TemplateManager.php‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
namespace TypePHP\Internal\Generics;
66

77
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
8+
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
89
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
911
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
13+
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
1014
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1115
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1216
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
1317
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
18+
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
19+
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
20+
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
1421
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
1522
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1623
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
@@ -1573,6 +1580,69 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref):
15731580
return new IntersectionTypeNode(array_map(fn ($t) => self::resolveTypeNodeAst($t, $ref), $n->types));
15741581
}
15751582

1583+
if ($n instanceof ArrayShapeNode) {
1584+
$items = array_map(function ($item) use ($ref) {
1585+
return new ArrayShapeItemNode(
1586+
$item->keyName,
1587+
$item->optional,
1588+
self::resolveTypeNodeAst($item->valueType, $ref)
1589+
);
1590+
}, $n->items);
1591+
1592+
$unsealed = null;
1593+
if ($n->unsealedType !== null) {
1594+
$unsealedKey = $n->unsealedType->keyType !== null
1595+
? self::resolveTypeNodeAst($n->unsealedType->keyType, $ref)
1596+
: null;
1597+
$unsealedVal = self::resolveTypeNodeAst($n->unsealedType->valueType, $ref);
1598+
$unsealed = new ArrayShapeUnsealedTypeNode($unsealedVal, $unsealedKey);
1599+
}
1600+
1601+
if ($n->sealed) {
1602+
return ArrayShapeNode::createSealed($items, $n->kind);
1603+
}
1604+
1605+
return ArrayShapeNode::createUnsealed($items, $unsealed, $n->kind);
1606+
}
1607+
1608+
if ($n instanceof ObjectShapeNode) {
1609+
$items = array_map(function ($item) use ($ref) {
1610+
return new ObjectShapeItemNode(
1611+
$item->keyName,
1612+
$item->optional,
1613+
self::resolveTypeNodeAst($item->valueType, $ref)
1614+
);
1615+
}, $n->items);
1616+
1617+
return new ObjectShapeNode($items);
1618+
}
1619+
1620+
if ($n instanceof CallableTypeNode) {
1621+
$params = array_map(function ($p) use ($ref) {
1622+
return new CallableTypeParameterNode(
1623+
self::resolveTypeNodeAst($p->type, $ref),
1624+
$p->isReference,
1625+
$p->isVariadic,
1626+
$p->parameterName,
1627+
$p->isOptional
1628+
);
1629+
}, $n->parameters);
1630+
1631+
return new CallableTypeNode(
1632+
$n->identifier,
1633+
$params,
1634+
self::resolveTypeNodeAst($n->returnType, $ref),
1635+
$n->templateTypes
1636+
);
1637+
}
1638+
1639+
if ($n instanceof OffsetAccessTypeNode) {
1640+
return new OffsetAccessTypeNode(
1641+
self::resolveTypeNodeAst($n->type, $ref),
1642+
self::resolveTypeNodeAst($n->offset, $ref)
1643+
);
1644+
}
1645+
15761646
return $n;
15771647
}
15781648
}

‎tests/TypeChecking/ArraysAndShapes/GlobalConstantKeyOfValueOfTest.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ function testGlobalConstantIntRange(int $n): int
214214
;
215215

216216
expect(fn () => testGlobalConstantIntRange(0))
217-
->toThrow(TypeError::class, 'Argument $n must be >= 1, 0 given');
217+
->toThrow(TypeError::class, 'Argument $n must be >= 1, 0 given')
218+
;
218219
});
219220
});
220221
});

0 commit comments

Comments
 (0)