Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/Type/VerbosityLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
40 changes: 40 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-13453.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.3

declare(strict_types = 1);

namespace Bug13453;

/** @template T of ResultA */
interface I {
/** @var class-string<T> */
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<ResultB> */
class In implements I {
public const string ResultType = ResultB::class;
}

/**
* @template T of ResultA
* @param I<T> $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();
}
Loading