Skip to content

Commit 63ad626

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix false positive invariance check for template types with same identity
- TemplateMixedType and TemplateStrictMixedType with same scope+name represent the same template parameter but fail equals() due to different concrete classes - At level 9+ transformCommonType converts TemplateMixedType to TemplateStrictMixedType in accepting type but not in accepted closure params - Added fallback check in isValidVariance() for invariant templates: if both sides are TemplateType with matching scope and name, treat as equal - New regression test in tests/PHPStan/Rules/Classes/data/bug-13440.php Closes phpstan/phpstan#13440
1 parent ba4fe14 commit 63ad626

File tree

3 files changed

+56
-1
lines changed

3 files changed

+56
-1
lines changed

src/Type/Generic/TemplateTypeVariance.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,15 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b): I
177177

178178
if ($this->invariant()) {
179179
$result = $a->equals($b);
180+
if (
181+
!$result
182+
&& $a instanceof TemplateType
183+
&& $b instanceof TemplateType
184+
&& $a->getScope()->equals($b->getScope())
185+
&& $a->getName() === $b->getName()
186+
) {
187+
$result = true;
188+
}
180189
$reasons = [];
181190
if (!$result) {
182191
if (

tests/PHPStan/Rules/Classes/InstantiationRuleTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
class InstantiationRuleTest extends RuleTestCase
2121
{
2222

23+
private bool $checkExplicitMixed = false;
24+
2325
protected function getRule(): Rule
2426
{
2527
$reflectionProvider = self::createReflectionProvider();
2628
$container = self::getContainer();
2729
return new InstantiationRule(
2830
$container,
2931
$reflectionProvider,
30-
new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true),
32+
new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true),
3133
new ClassNameCheck(
3234
new ClassCaseSensitivityCheck($reflectionProvider, true),
3335
new ClassForbiddenNameCheck($container),
@@ -570,6 +572,13 @@ public function testBug14097(): void
570572
$this->analyse([__DIR__ . '/data/bug-14097.php'], []);
571573
}
572574

575+
#[RequiresPhp('>= 8.0')]
576+
public function testBug13440(): void
577+
{
578+
$this->checkExplicitMixed = true;
579+
$this->analyse([__DIR__ . '/data/bug-13440.php'], []);
580+
}
581+
573582
public function testNewStaticWithConsistentConstructor(): void
574583
{
575584
$this->analyse([__DIR__ . '/data/instantiation-new-static-consistent-constructor.php'], [
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13440;
6+
7+
use Closure;
8+
9+
/** @template T */
10+
interface Foo {}
11+
12+
/**
13+
* @template TVal
14+
* @template TReturn
15+
*/
16+
class Box
17+
{
18+
/**
19+
* @param TVal $val
20+
* @param Closure(Foo<TVal>): TReturn $cb
21+
*/
22+
public function __construct(
23+
private mixed $val,
24+
private Closure $cb,
25+
) {
26+
}
27+
28+
/**
29+
* @template TNewReturn
30+
* @param Closure(Foo<TVal>): TNewReturn $cb
31+
* @return self<TVal, TNewReturn>
32+
*/
33+
public function test(Closure $cb): self
34+
{
35+
return new self($this->val, $cb);
36+
}
37+
}

0 commit comments

Comments
 (0)