Skip to content

Commit 549f5e9

Browse files
committed
Refactor TemplateManager docblock and improve variance handling; add GenericReturnCovarianceTest for Liskov subtyping validation
1 parent 63aaa67 commit 549f5e9

3 files changed

Lines changed: 48 additions & 8 deletions

File tree

src/Resolver/TemplateManager.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
use WeakMap;
2828

2929
/**
30-
* @internal Manages generic template bindings for object instances (via WeakMap) and static/method call stack frames.
30+
* Manages generic template bindings for object instances (via WeakMap) and static/method call stack frames.
31+
*
32+
* @internal
3133
*/
3234
final class TemplateManager
3335
{
@@ -51,7 +53,7 @@ final class TemplateManager
5153
public static ?object $pendingCloneSource = null;
5254

5355
/**
54-
* Resets all static generic template bindings and call stack frames. Useful for test isolation.
56+
* Resets all static generic template bindings and call stack frames.
5557
*/
5658
public static function reset(): void
5759
{
@@ -358,9 +360,11 @@ private static function bindSingleTemplateArgument(
358360
$usageVariance = $typeNode->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
359361
$declaredVariance = $classVariances[$templateTag->name] ?? GenericTypeNode::VARIANCE_INVARIANT;
360362

363+
$isReturnContext = str_contains($context, 'Return value');
364+
361365
$variance = ($usageVariance !== GenericTypeNode::VARIANCE_INVARIANT)
362366
? $usageVariance
363-
: $declaredVariance;
367+
: ($isReturnContext ? GenericTypeNode::VARIANCE_COVARIANT : $declaredVariance);
364368

365369
$templateName = $templateTag->name;
366370
$existingBindings = self::$instanceTemplateBindings[$instance] ?? [];
@@ -795,9 +799,7 @@ private static function resolveTypeNodeAst(TypeNode $n, \ReflectionClass $ref):
795799
*/
796800
private static function getPhpDocParserComponents(): array
797801
{
798-
/** @var PhpDocParser|null $phpDocParser */
799802
static $phpDocParser = null;
800-
/** @var Lexer|null $lexer */
801803
static $lexer = null;
802804

803805
if ($phpDocParser === null || $lexer === null) {
@@ -818,9 +820,7 @@ private static function getPhpDocParserComponents(): array
818820
*/
819821
private static function getTypeParserComponents(): array
820822
{
821-
/** @var TypeParser|null $typeParser */
822823
static $typeParser = null;
823-
/** @var Lexer|null $lexer */
824824
static $lexer = null;
825825

826826
if ($typeParser === null || $lexer === null) {

tests/TypeChecking/Boundaries/LateStaticBindingReturnTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191

9292
test('throws TypeError when generic static factory returns instance violating generic template T', function () {
9393
expect(fn () => UserGenericFactory::ofBadItem(new Dog()))
94-
->toThrow(TypeError::class, 'UserGenericFactory<invariant TypePHP\Tests\Fixtures\Domain\Dog>, but TypePHP\Tests\Fixtures\Services\UserGenericFactory<stdClass> was returned')
94+
->toThrow(TypeError::class, 'UserGenericFactory<covariant TypePHP\Tests\Fixtures\Domain\Dog>, but TypePHP\Tests\Fixtures\Services\UserGenericFactory<stdClass> was returned')
9595
;
9696
});
9797

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use TypePHP\Tests\Fixtures\Domain\Animal;
6+
use TypePHP\Tests\Fixtures\Domain\Car;
7+
use TypePHP\Tests\Fixtures\Domain\Dog;
8+
use TypePHP\Tests\Fixtures\Generics\GenericCollection;
9+
10+
/**
11+
* Method declaring return type of GenericCollection<Animal>
12+
*
13+
* @return GenericCollection<Animal>
14+
*/
15+
function testReturnGenericCollection(GenericCollection $collection): GenericCollection
16+
{
17+
return $collection;
18+
}
19+
20+
describe('Generic Return Covariance (Liskov Subtyping on Return Types)', function () {
21+
test('accepts GenericCollection holding Dog subclass when return contract specifies Animal', function () {
22+
/** @var GenericCollection<Dog> $dogCollection */
23+
$dogCollection = new GenericCollection();
24+
$dogCollection->add(new Dog());
25+
26+
$result = testReturnGenericCollection($dogCollection);
27+
28+
expect($result)->toBe($dogCollection);
29+
});
30+
31+
test('throws TypeError when GenericCollection returned holds an unrelated type', function () {
32+
/** @var GenericCollection<Car> $carCollection */
33+
$carCollection = new GenericCollection();
34+
$carCollection->add(new Car());
35+
36+
expect(fn () => testReturnGenericCollection($carCollection))
37+
->toThrow(TypeError::class)
38+
;
39+
});
40+
});

0 commit comments

Comments
 (0)