Fix phpstan/phpstan#11430: Two unbounded generics in conditional return are assumed to be always the same#5336
Conversation
There was a problem hiding this comment.
That make sens to me
NB: the bot talk about a snippet of the issue phpstan/phpstan#11038 but that does not close this issue since it's not the original snippet of the issue.
…rn assumed same - Don't subtract TemplateType from another TemplateType's bound in tryRemove - This prevented `T of mixed~S` from being created, which couldn't match the plain `T` in invariant generic argument checks - New regression test in tests/PHPStan/Rules/TooWideTypehints/data/bug-11430.php
486684f to
9e9f82d
Compare
while this PR does not fix the origin problem, it still seem to fix a false positive error (we should add a regression test for). what about the 2 failling integration test jobs? |
This failure are related to the ObjectType::equals change, and I'm unsured they are already fix in Rector codebase |
|
I will update rector in phpstan/phpstan#14435 |
VincentLanglet
left a comment
There was a problem hiding this comment.
Add a non regression test for
<?php declare(strict_types = 1);
/**
* @template T
*
* @implements IteratorAggregate<T>
*/
abstract class Option implements IteratorAggregate
{
/**
* @template S
* @template U
*
* @param S $value The actual return value.
* @param U $noneValue The value which should be considered "None"; null by
* default.
*
* @return (S is U ? None : Option<S>)
*/
public static function fromValue($value, $noneValue = null)
{
if ($value === $noneValue) {
return None::create();
}
return new Some($value);
}
}
/**
* @extends Option<mixed>
*/
final class None extends Option
{
/** @var None|null */
private static $instance;
/**
* @return None
*/
public static function create(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function getIterator(): EmptyIterator
{
return new EmptyIterator();
}
}
/**
* @template T
*
* @extends Option<T>
*/
final class Some extends Option
{
/** @var T */
private $value;
/**
* @param T $value
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* @return ArrayIterator<int, T>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator([$this->value]);
}
}
class Test
{
/** @var Option<string> */
public Option $name;
}
$test = new Test();
/** @var ?string $foo */
$foo = null;
$test->name = Option::fromValue($foo);
PHPStan\testing\assertType('Option<string>', $test->name);
Ensuring there is no
Method Option::fromValue() should return Option<S of mixed> but returns Some<S>.
message
Add test for the original issue snippet with Option::fromValue() conditional return type to ensure no "should return ... but returns" false positive. Also add type inference test for the same scenario. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
All done — both background tasks completed successfully. The commit with the additional regression tests has been pushed. |
VincentLanglet
left a comment
There was a problem hiding this comment.
Do not duplicate Bug11430Methods and Bug11430Nsrt files and use a single one for both assertions in nsrt and Rules/Methods
Use a single test data file in nsrt/ for both the ReturnTypeRule test and the NodeScopeResolver assertType test, instead of maintaining two nearly identical copies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
PHPStan also completed successfully. Both checks passed and the commit is already pushed. |
…rn are assumed to be always the same (phpstan#5336) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
When a method had two unbounded template types (e.g.
TandS) used in a conditional return type like(T is S ? None : Some<T>), PHPStan incorrectly reported "Method never returns Some so it can be removed from the return type." This was a false positive — the method can clearly return either branch depending on the arguments.Changes
TemplateTypeTrait::tryRemove()insrc/Type/Generic/TemplateTypeTrait.phpto returnnullwhen asked to remove aTemplateType, preventing meaningless template bound narrowingtests/PHPStan/Rules/TooWideTypehints/data/bug-11430.phptestBug11430intests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.phpRoot cause
When resolving the conditional return type
(T is S ? None : Some<T>),ConditionalType::getNormalizedElse()replaces the subjectTin the else branch withTypeCombinator::remove(T, S). This calledTemplateTypeTrait::tryRemove(S), which in turn calledTypeCombinator::remove(mixed, S). Since plainMixedType::tryRemoveconsidersmixeda supertype of any template type, it succeeded and returnedmixed~S(mixed minus S). This created a new template typeT of mixed~S.The problem: when
narrowTypelater compared the declared return typeNone|Some<T of mixed~S>against the actual return typeNone|Some<T>, the invariant generic variance check usedequals()to compareT of mixed~SwithT. Since these aren't equal (different bounds),isValidVariancereturnedNo, causingSome<T of mixed~S>to be reported as never returned.The fix prevents subtracting a TemplateType from another TemplateType's bound, since template types represent unknown types and their subtraction doesn't produce useful narrowing information.
Test
Added a regression test reproducing the exact scenario from the issue: a final class with a static method using two unbounded generics in a conditional return type. The test expects no errors (the false positive is eliminated).
Fixes phpstan/phpstan#11430