From 53a323073397e4cb7bcf5ed7643b9cd3d2688d65 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:14:14 +0000 Subject: [PATCH 1/3] 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 --- src/Type/VerbosityLevel.php | 16 +++++++- .../Rules/Functions/ReturnTypeRuleTest.php | 12 ++++++ .../Rules/Functions/data/bug-13453.php | 40 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13453.php diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 32be9683a81..2b6e16ad272 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -221,7 +221,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc }); if (!$containsInvariantTemplateType) { - return $verbosity ?? self::typeOnly(); + $level = $verbosity ?? self::typeOnly(); + + if ($acceptingType->describe($level) === $acceptedType->describe($level)) { + return self::precise(); + } + + return $level; } /** @var bool $moreVerbose */ @@ -234,7 +240,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc return self::precise(); } - return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); + $level = $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); + + if ($acceptingType->describe($level) === $acceptedType->describe($level)) { + return self::precise(); + } + + return $level; } /** diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 6b63c91b88f..c5a1405d302 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -411,4 +411,16 @@ public function testBug12397(): void $this->analyse([__DIR__ . '/data/bug-12397.php'], []); } + public function testBug13453(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13453.php'], [ + [ + 'Function Bug13453\run() should return T of Bug13453\ResultA (function Bug13453\run(), argument) but returns T of Bug13453\ResultA (class Bug13453\I, parameter).', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13453.php b/tests/PHPStan/Rules/Functions/data/bug-13453.php new file mode 100644 index 00000000000..b3164230d3b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13453.php @@ -0,0 +1,40 @@ += 8.3 + +declare(strict_types = 1); + +namespace Bug13453; + +/** @template T of ResultA */ +interface I { + /** @var class-string */ + public const string ResultType = ResultA::class; +} + +class ResultA { + public function __construct(public string $value) {} +} + +class ResultB extends ResultA { + public function rot13(): string { return str_rot13($this->value); } +} + +/** @template-implements I */ +class In implements I { + public const string ResultType = ResultB::class; +} + +/** + * @template T of ResultA + * @param I $in + * @return T + */ +function run(I $in): ResultA { + $value = 'abc'; + return new ($in::ResultType)($value); +} + +function main(): void { + $in = new In(); + $ret = run($in); + print $ret->rot13(); +} From 73abb5ed0fe87de152bc98dfd208cbaaaa980d79 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 4 Apr 2026 21:19:30 +0000 Subject: [PATCH 2/3] Add test for identical description disambiguation with invariant template types Cover the second code path in VerbosityLevel::getRecommendedLevelByType() where containsInvariantTemplateType is true. The test uses a generic Container (invariant template) wrapping template types from different scopes that would otherwise produce identical descriptions. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Functions/ReturnTypeRuleTest.php | 12 ++++++ .../Functions/data/bug-13453-invariant.php | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13453-invariant.php diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index c5a1405d302..8679cc662c7 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -423,4 +423,16 @@ public function testBug13453(): void ]); } + public function testBug13453Invariant(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13453-invariant.php'], [ + [ + 'Function Bug13453Invariant\run() should return Bug13453Invariant\Container but returns Bug13453Invariant\Container.', + 37, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13453-invariant.php b/tests/PHPStan/Rules/Functions/data/bug-13453-invariant.php new file mode 100644 index 00000000000..133bfc4454c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13453-invariant.php @@ -0,0 +1,38 @@ += 8.3 + +declare(strict_types = 1); + +namespace Bug13453Invariant; + +/** @template T */ +class Container { + /** @param T $value */ + public function __construct(public mixed $value) {} +} + +class ResultA { + public function __construct(public string $value) {} +} + +class ResultB extends ResultA {} + +/** @template T of ResultA */ +interface I { + /** @var class-string */ + public const string ResultType = ResultA::class; +} + +/** @template-implements I */ +class In implements I { + public const string ResultType = ResultB::class; +} + +/** + * @template T of ResultA + * @param I $in + * @return Container + */ +function run(I $in): Container { + $value = 'abc'; + return new Container(new ($in::ResultType)($value)); +} From dfae0ee64116ae40e1be8d57c1301be6614309f1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 5 Apr 2026 21:11:02 +0000 Subject: [PATCH 3/3] Add #[RequiresPhp('>= 8.3')] to bug-13453 tests The test data files use PHP 8.3 typed constants (public const string), so the test methods need the RequiresPhp attribute to skip on older PHP. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 8679cc662c7..01227f12da0 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -411,6 +411,7 @@ public function testBug12397(): void $this->analyse([__DIR__ . '/data/bug-12397.php'], []); } + #[RequiresPhp('>= 8.3')] public function testBug13453(): void { $this->checkNullables = true; @@ -423,6 +424,7 @@ public function testBug13453(): void ]); } + #[RequiresPhp('>= 8.3')] public function testBug13453Invariant(): void { $this->checkNullables = true;