Skip to content

Commit 89ab39f

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix phpstan/phpstan#11776: Preserve narrowed template bounds during resolution
- When resolveTemplateTypes() replaces a template type with a standin of the same name and scope, preserve the narrower bound from PHPDoc intersections like (int|string)&TOperation - New regression test in tests/PHPStan/Analyser/nsrt/bug-11776.php - Root cause: ResolvedPropertyReflection called resolveTemplateTypes() which replaced TOperation of int|string with TOperation of scalar, losing the intersection narrowing from the PHPDoc type annotation
1 parent c36922b commit 89ab39f

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

src/Type/Generic/TemplateTypeHelper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ public static function resolveTemplateTypes(
5454

5555
$callSiteVariance = $callSiteVariances->getVariance($type->getName());
5656
if ($callSiteVariance === null || $callSiteVariance->invariant()) {
57+
if (
58+
$newType instanceof TemplateType
59+
&& $newType->getName() === $type->getName()
60+
&& $newType->getScope()->equals($type->getScope())
61+
&& !$type->getBound()->equals($newType->getBound())
62+
&& $newType->getBound()->isSuperTypeOf($type->getBound())->yes()
63+
) {
64+
return TemplateTypeFactory::create(
65+
$newType->getScope(),
66+
$newType->getName(),
67+
$type->getBound(),
68+
$newType->getVariance(),
69+
$newType->getStrategy(),
70+
$newType->getDefault(),
71+
);
72+
}
5773
return $newType;
5874
}
5975

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug11776;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TOperation of int|string
11+
*/
12+
interface EnumAsFilterInterface {}
13+
14+
/**
15+
* @template TOperation of scalar
16+
*/
17+
final readonly class ScalarableChoice
18+
{
19+
/**
20+
* @param class-string<EnumAsFilterInterface<(int|string)&TOperation>> $choiceClassName
21+
*/
22+
public function __construct(private string $choiceClassName) {}
23+
24+
/**
25+
* @return class-string<EnumAsFilterInterface<(int|string)&TOperation>>
26+
*/
27+
public function getChoiceClassName(): string
28+
{
29+
assertType('class-string<Bug11776\EnumAsFilterInterface<TOperation of int|string (class Bug11776\ScalarableChoice, argument)>>', $this->choiceClassName);
30+
return $this->choiceClassName;
31+
}
32+
}

0 commit comments

Comments
 (0)