Skip to content

Commit 813d15e

Browse files
authored
Fix implicit @phpstan-assert PHPDoc inheritance with generics
1 parent a7ff362 commit 813d15e

File tree

6 files changed

+274
-2
lines changed

6 files changed

+274
-2
lines changed

src/PhpDoc/ResolvedPhpDocBlock.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -931,8 +931,12 @@ private static function mergeAssertTags(array $assertTags, array $parents, array
931931
$phpDocBlock = $parentPhpDocBlocks[$i];
932932

933933
return array_map(
934-
static fn (AssertTag $assertTag) => $assertTag->withParameter(
935-
$phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
934+
static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag(
935+
$assertTag->withParameter(
936+
$phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
937+
),
938+
$phpDocBlock,
939+
TemplateTypeVariance::createCovariant(),
936940
),
937941
$result,
938942
);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+3
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,9 @@ public function dataFileAsserts(): iterable
14271427
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php');
14281428
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php');
14291429
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php');
1430+
yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-inheritance.php');
1431+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9123.php');
1432+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10037.php');
14301433
}
14311434

14321435
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace AssertInheritance;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
*/
10+
interface WrapperInterface
11+
{
12+
/**
13+
* @phpstan-assert T $param
14+
*/
15+
public function assert(mixed $param): void;
16+
17+
/**
18+
* @phpstan-assert-if-true T $param
19+
*/
20+
public function supports(mixed $param): bool;
21+
22+
/**
23+
* @phpstan-assert-if-false T $param
24+
*/
25+
public function notSupports(mixed $param): bool;
26+
}
27+
28+
/**
29+
* @implements WrapperInterface<int>
30+
*/
31+
class IntWrapper implements WrapperInterface
32+
{
33+
public function assert(mixed $param): void
34+
{
35+
}
36+
37+
public function supports(mixed $param): bool
38+
{
39+
return is_int($param);
40+
}
41+
42+
public function notSupports(mixed $param): bool
43+
{
44+
return !is_int($param);
45+
}
46+
}
47+
48+
/**
49+
* @template T of object
50+
* @implements WrapperInterface<T>
51+
*/
52+
abstract class ObjectWrapper implements WrapperInterface
53+
{
54+
}
55+
56+
/**
57+
* @extends ObjectWrapper<\DateTimeInterface>
58+
*/
59+
class DateTimeInterfaceWrapper extends ObjectWrapper
60+
{
61+
public function assert(mixed $param): void
62+
{
63+
}
64+
65+
public function supports(mixed $param): bool
66+
{
67+
return $param instanceof \DateTimeInterface;
68+
}
69+
70+
public function notSupports(mixed $param): bool
71+
{
72+
return !$param instanceof \DateTimeInterface;
73+
}
74+
}
75+
76+
function (IntWrapper $test, $val) {
77+
if ($test->supports($val)) {
78+
assertType('int', $val);
79+
} else {
80+
assertType('mixed~int', $val);
81+
}
82+
83+
if ($test->notSupports($val)) {
84+
assertType('mixed~int', $val);
85+
} else {
86+
assertType('int', $val);
87+
}
88+
89+
assertType('mixed', $val);
90+
$test->assert($val);
91+
assertType('int', $val);
92+
};
93+
94+
function (DateTimeInterfaceWrapper $test, $val) {
95+
if ($test->supports($val)) {
96+
assertType('DateTimeInterface', $val);
97+
} else {
98+
assertType('mixed~DateTimeInterface', $val);
99+
}
100+
101+
if ($test->notSupports($val)) {
102+
assertType('mixed~DateTimeInterface', $val);
103+
} else {
104+
assertType('DateTimeInterface', $val);
105+
}
106+
107+
assertType('mixed', $val);
108+
$test->assert($val);
109+
assertType('DateTimeInterface', $val);
110+
};
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug10037;
4+
5+
interface Identifier
6+
{}
7+
8+
interface Document
9+
{}
10+
11+
/** @template T of Identifier */
12+
interface Fetcher
13+
{
14+
/** @phpstan-assert-if-true T $identifier */
15+
public function supports(Identifier $identifier): bool;
16+
17+
/** @param T $identifier */
18+
public function fetch(Identifier $identifier): Document;
19+
}
20+
21+
/** @implements Fetcher<PostIdentifier> */
22+
final readonly class PostFetcher implements Fetcher
23+
{
24+
public function supports(Identifier $identifier): bool
25+
{
26+
return $identifier instanceof PostIdentifier;
27+
}
28+
29+
public function fetch(Identifier $identifier): Document
30+
{
31+
// SA knows $identifier is instance of PostIdentifier here
32+
return $identifier->foo();
33+
}
34+
}
35+
36+
class PostIdentifier implements Identifier
37+
{
38+
public function foo(): Document
39+
{
40+
return new class implements Document{};
41+
}
42+
}
43+
44+
function (Identifier $i): void {
45+
$fetcher = new PostFetcher();
46+
\PHPStan\Testing\assertType('Bug10037\Identifier', $i);
47+
if ($fetcher->supports($i)) {
48+
\PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i);
49+
$fetcher->fetch($i);
50+
} else {
51+
$fetcher->fetch($i);
52+
}
53+
};
54+
55+
class Post
56+
{
57+
}
58+
59+
/** @template T */
60+
abstract class Voter
61+
{
62+
63+
/** @phpstan-assert-if-true T $subject */
64+
abstract function supports(string $attribute, mixed $subject): bool;
65+
66+
/** @param T $subject */
67+
abstract function voteOnAttribute(string $attribute, mixed $subject): bool;
68+
69+
}
70+
71+
/** @extends Voter<Post> */
72+
class PostVoter extends Voter
73+
{
74+
75+
/** @phpstan-assert-if-true Post $subject */
76+
function supports(string $attribute, mixed $subject): bool
77+
{
78+
79+
}
80+
81+
function voteOnAttribute(string $attribute, mixed $subject): bool
82+
{
83+
\PHPStan\Testing\assertType('Bug10037\Post', $subject);
84+
}
85+
}
86+
87+
function ($subject): void {
88+
$voter = new PostVoter();
89+
\PHPStan\Testing\assertType('mixed', $subject);
90+
if ($voter->supports('aaa', $subject)) {
91+
\PHPStan\Testing\assertType('Bug10037\Post', $subject);
92+
$voter->voteOnAttribute('aaa', $subject);
93+
} else {
94+
$voter->voteOnAttribute('aaa', $subject);
95+
}
96+
};
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug9123;
4+
5+
interface Event {}
6+
7+
class MyEvent implements Event {}
8+
9+
/** @template T of Event */
10+
interface EventListener
11+
{
12+
/** @phpstan-assert-if-true T $event */
13+
public function canBeListen(Event $event): bool;
14+
15+
public function listen(Event $event): void;
16+
}
17+
18+
/** @implements EventListener<MyEvent> */
19+
final class Implementation implements EventListener
20+
{
21+
public function canBeListen(Event $event): bool
22+
{
23+
return $event instanceof MyEvent;
24+
}
25+
26+
public function listen(Event $event): void
27+
{
28+
if (! $this->canBeListen($event)) {
29+
return;
30+
}
31+
32+
\PHPStan\Testing\assertType('Bug9123\MyEvent', $event);
33+
}
34+
}
35+
36+
/** @implements EventListener<MyEvent> */
37+
final class Implementation2 implements EventListener
38+
{
39+
/** @phpstan-assert-if-true MyEvent $event */
40+
public function canBeListen(Event $event): bool
41+
{
42+
return $event instanceof MyEvent;
43+
}
44+
45+
public function listen(Event $event): void
46+
{
47+
if (! $this->canBeListen($event)) {
48+
return;
49+
}
50+
51+
\PHPStan\Testing\assertType('Bug9123\MyEvent', $event);
52+
}
53+
}

tests/PHPStan/Analyser/data/self-out.php

+6
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,10 @@ function () {
8888

8989
$i->addData(321);
9090
assertType('SelfOut\\a<int>', $i);
91+
92+
$i->addData(random_bytes(3));
93+
assertType('SelfOut\\a<int|non-empty-string>', $i);
94+
95+
$i->setData(true);
96+
assertType('SelfOut\\a<true>', $i);
9197
};

0 commit comments

Comments
 (0)