Skip to content

Commit 4dc5cb6

Browse files
rvanvelzenclaude
andcommitted
Do not let a TemplateType member force Yes acceptance of an intersection
Closes phpstan/phpstan#13190 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 166ae33 commit 4dc5cb6

4 files changed

Lines changed: 102 additions & 1 deletion

File tree

src/Type/IntersectionType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,17 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsRes
339339
if ($isSuperType->no()) {
340340
return $isSuperType->toAcceptsResult();
341341
}
342+
343+
// A TemplateType member accepts eagerly, so lazyMaxMin's Yes may come solely from it.
344+
// A Maybe from the holistic isSuperTypeOf means no member is a definite subtype, so
345+
// when a TemplateType member is present the eager Yes is untrustworthy - trust the Maybe.
346+
if ($isSuperType->maybe()) {
347+
foreach ($this->types as $innerType) {
348+
if ($innerType instanceof TemplateType) {
349+
return $isSuperType->toAcceptsResult();
350+
}
351+
}
352+
}
342353
}
343354

344355
if ($this->isOversizedArray()->yes()) {

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,11 +451,34 @@ public function testBug13565(): void
451451
]);
452452
}
453453

454+
#[RequiresPhp('>= 8.0.0')]
455+
public function testBug13190TemplateGeneric(): void
456+
{
457+
$this->checkNullables = true;
458+
$this->checkExplicitMixed = false;
459+
// A raw TemplateType value returned as its generic application must not be a false positive.
460+
$this->analyse([__DIR__ . '/data/bug-13190-template-generic.php'], []);
461+
}
462+
463+
#[RequiresPhp('>= 8.0.0')]
454464
public function testBug13190(): void
455465
{
456466
$this->checkNullables = true;
457467
$this->checkExplicitMixed = false;
458-
$this->analyse([__DIR__ . '/data/bug-13190.php'], []);
468+
$this->analyse([__DIR__ . '/data/bug-13190.php'], [
469+
[
470+
'Function Bug13190\inbox() should return Bug13190\Box<T> but returns (Bug13190\Box&T)|Bug13190\Box<T>.',
471+
51,
472+
],
473+
[
474+
'Function Bug13190\inbox_concrete_impl() should return Bug13190\Box<T> but returns (Bug13190\Box<T>&Bug13190\IntBox)|(Bug13190\IntBox&T).',
475+
79,
476+
],
477+
[
478+
'Function Bug13190\inbox_concrete_impl() should return Bug13190\Box<T> but returns Bug13190\BoxImpl<Bug13190\Box<T>|T of mixed>.',
479+
81,
480+
],
481+
]);
459482
}
460483

461484
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php // lint >= 8.0
2+
declare(strict_types = 1);
3+
4+
namespace Bug13190TemplateGeneric;
5+
6+
class Promise
7+
{
8+
}
9+
10+
interface Result
11+
{
12+
}
13+
14+
class ResultA implements Result
15+
{
16+
}
17+
18+
/**
19+
* @template TypeConcurrent of bool
20+
*/
21+
abstract class Communicator
22+
{
23+
/** @param TypeConcurrent $concurrent */
24+
public function __construct(private bool $concurrent)
25+
{
26+
}
27+
}
28+
29+
/**
30+
* @template TypeCommunicator of Communicator
31+
* @template TypeConcurrent of bool
32+
* @param class-string<TypeCommunicator> $communicatorClass
33+
* @param TypeConcurrent $concurrent
34+
* @return TypeCommunicator<TypeConcurrent>
35+
*/
36+
function communicatorFactory(string $communicatorClass, bool $concurrent): Communicator
37+
{
38+
return new $communicatorClass($concurrent);
39+
}

tests/PHPStan/Rules/Functions/data/bug-13190.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,31 @@ function inbox($to_box): Box
5353
return new BoxImpl($to_box);
5454
}
5555
}
56+
57+
/**
58+
* @implements Box<int>
59+
*/
60+
final class IntBox implements Box
61+
{
62+
#[\Override]
63+
public function toInner(): int
64+
{
65+
return 0;
66+
}
67+
}
68+
69+
/**
70+
* @template T
71+
*
72+
* @param T|Box<T> $to_box
73+
*
74+
* @return Box<T>
75+
*/
76+
function inbox_concrete_impl($to_box): Box
77+
{
78+
if ($to_box instanceof IntBox) {
79+
return $to_box;
80+
} else {
81+
return new BoxImpl($to_box);
82+
}
83+
}

0 commit comments

Comments
 (0)