Skip to content

Commit 53a3230

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix phpstan/phpstan#13453: Disambiguate identical template type descriptions in error messages
- When two different template types produce the same description at the chosen verbosity level, escalate to precise verbosity to include scope information - This fixes self-contradictory messages like "should return T of ResultA but returns T of ResultA" by showing "T of ResultA (function run(), argument) but returns T of ResultA (class I, parameter)" - New regression test in tests/PHPStan/Rules/Functions/data/bug-13453.php
1 parent c36922b commit 53a3230

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

src/Type/VerbosityLevel.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
221221
});
222222

223223
if (!$containsInvariantTemplateType) {
224-
return $verbosity ?? self::typeOnly();
224+
$level = $verbosity ?? self::typeOnly();
225+
226+
if ($acceptingType->describe($level) === $acceptedType->describe($level)) {
227+
return self::precise();
228+
}
229+
230+
return $level;
225231
}
226232

227233
/** @var bool $moreVerbose */
@@ -234,7 +240,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
234240
return self::precise();
235241
}
236242

237-
return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly();
243+
$level = $moreVerbose ? self::value() : $verbosity ?? self::typeOnly();
244+
245+
if ($acceptingType->describe($level) === $acceptedType->describe($level)) {
246+
return self::precise();
247+
}
248+
249+
return $level;
238250
}
239251

240252
/**

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,16 @@ public function testBug12397(): void
411411
$this->analyse([__DIR__ . '/data/bug-12397.php'], []);
412412
}
413413

414+
public function testBug13453(): void
415+
{
416+
$this->checkNullables = true;
417+
$this->checkExplicitMixed = true;
418+
$this->analyse([__DIR__ . '/data/bug-13453.php'], [
419+
[
420+
'Function Bug13453\run() should return T of Bug13453\ResultA (function Bug13453\run(), argument) but returns T of Bug13453\ResultA (class Bug13453\I, parameter).',
421+
33,
422+
],
423+
]);
424+
}
425+
414426
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php // lint >= 8.3
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13453;
6+
7+
/** @template T of ResultA */
8+
interface I {
9+
/** @var class-string<T> */
10+
public const string ResultType = ResultA::class;
11+
}
12+
13+
class ResultA {
14+
public function __construct(public string $value) {}
15+
}
16+
17+
class ResultB extends ResultA {
18+
public function rot13(): string { return str_rot13($this->value); }
19+
}
20+
21+
/** @template-implements I<ResultB> */
22+
class In implements I {
23+
public const string ResultType = ResultB::class;
24+
}
25+
26+
/**
27+
* @template T of ResultA
28+
* @param I<T> $in
29+
* @return T
30+
*/
31+
function run(I $in): ResultA {
32+
$value = 'abc';
33+
return new ($in::ResultType)($value);
34+
}
35+
36+
function main(): void {
37+
$in = new In();
38+
$ret = run($in);
39+
print $ret->rot13();
40+
}

0 commit comments

Comments
 (0)