Skip to content

Commit cb29a88

Browse files
committed
Enhance TemplateManager to allow specialization of 'mixed' type in return context and add tests for unspecialized generic default objects
1 parent 3fa8853 commit cb29a88

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

src/Resolver/TemplateManager.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,13 +622,19 @@ private static function bindSingleTemplateArgument(
622622
$templateName = $templateTag->name;
623623
$existingBindings = self::$instanceTemplateBindings[$instance] ?? [];
624624

625-
if (isset($existingBindings[$templateName])) {
625+
if (isset($existingBindings[$templateName])) {
626626
$existingTypeNode = $existingBindings[$templateName];
627627
$valid = self::checkVariance($existingTypeNode, $expectedTypeNode, $variance);
628628

629629
if (! $valid) {
630-
// In return context, allow methods to specialize broad bounds to narrower types
631-
// (e.g. @return static<int, TValue> specializing TKey of array-key to int)
630+
if ($existingTypeNode instanceof IdentifierTypeNode && strtolower($existingTypeNode->name) === 'mixed') {
631+
$bindings = self::$instanceTemplateBindings[$instance] ?? [];
632+
$bindings[$templateName] = $expectedTypeNode;
633+
self::$instanceTemplateBindings[$instance] = $bindings;
634+
635+
return null;
636+
}
637+
632638
if ($isReturnContext && self::checkVariance($expectedTypeNode, $existingTypeNode, GenericTypeNode::VARIANCE_COVARIANT)) {
633639
$bindings = self::$instanceTemplateBindings[$instance] ?? [];
634640
$bindings[$templateName] = $expectedTypeNode;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
6+
use TypePHP\Exception\TypeError;
7+
use TypePHP\Resolver\TemplateManager;
8+
use TypePHP\TypePHP;
9+
10+
interface SpecificAppMiddleware
11+
{
12+
}
13+
14+
class ConcreteAppMiddleware implements SpecificAppMiddleware
15+
{
16+
}
17+
18+
class IncompatibleOtherMiddleware
19+
{
20+
}
21+
22+
/**
23+
* Generic container with class template
24+
*
25+
* @template TMiddleware of object
26+
*/
27+
class GenericMiddlewareContainer
28+
{
29+
/**
30+
* @var array<int, TMiddleware>
31+
*/
32+
public array $middlewares = [];
33+
}
34+
35+
/**
36+
* Function accepting specialized generic container
37+
*
38+
* @param GenericMiddlewareContainer<SpecificAppMiddleware> $container
39+
*/
40+
function consumeMiddlewareContainer(GenericMiddlewareContainer $container): bool
41+
{
42+
return true;
43+
}
44+
45+
describe('Unspecialized Generic Default Objects & First-Use Parameter Binding', function () {
46+
test('reproduces tempest unspecialized mixed binding mismatch on un-annotated instance', function () {
47+
$rawContainer = new GenericMiddlewareContainer();
48+
49+
TemplateManager::bindTemplate('none', $rawContainer, 'TMiddleware', new IdentifierTypeNode('mixed'));
50+
51+
expect(consumeMiddlewareContainer($rawContainer))->toBeTrue()
52+
->and(TypePHP::getGenericType($rawContainer))->toBe(SpecificAppMiddleware::class)
53+
;
54+
});
55+
56+
test('strictly rejects explicitly annotated instance holding an incompatible generic type', function () {
57+
$badContainer = new GenericMiddlewareContainer();
58+
TemplateManager::bindTemplate('none', $badContainer, 'TMiddleware', new IdentifierTypeNode(IncompatibleOtherMiddleware::class));
59+
60+
expect(fn () => consumeMiddlewareContainer($badContainer))
61+
->toThrow(TypeError::class, 'expects GenericMiddlewareContainer<invariant SpecificAppMiddleware>, but GenericMiddlewareContainer<IncompatibleOtherMiddleware> was given')
62+
;
63+
});
64+
});

0 commit comments

Comments
 (0)