Skip to content

Fix type narrowed too much after identical comparison with never-typed generic method#5491

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qziy8hq
Open

Fix type narrowed too much after identical comparison with never-typed generic method#5491
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qziy8hq

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#14281

When a generic class like Collection<TElement> is constructed with an empty array default (new TestCollection()), the template TElement resolves to never. This causes method return types like TElement|null to collapse to null. Subsequent assert($data[0] === $collection->get(0)) comparisons are then seen as always-false (e.g. 0 === null), making code unreachable and narrowing $data to *NEVER*.

Root cause

In ResolvedFunctionVariantWithOriginal::resolveResolvableTemplateTypes(), class-level template types that resolve to never were used literally in method return types. For the object type itself (TestCollection<never>), this is correct — never is the bottom type and passes covariance checks. But for method return types, it produces overly narrow types that break type narrowing in the caller.

Fix

When resolving class-level template types in method return types, if the resolved type is never, replace it with the template's declared bound (e.g. mixed for unbounded, stdClass for @template T of stdClass). This is skipped when the return type contains ConditionalType nodes, since conditional return types like (T is never ? false : bool) need the actual never value to evaluate correctly.

Changed files

  • src/Reflection/ResolvedFunctionVariantWithOriginal.php — the fix
  • tests/PHPStan/Analyser/nsrt/bug-14281.php — regression test
  • tests/PHPStan/Analyser/nsrt/generics.php — updated assertion: empty StdClassCollection now returns array<stdClass> from getAll() instead of array{}, reflecting the template bound instead of never

Impact

  • Collection<never>::get() now returns mixed (bound) instead of null (never|null)
  • StdClassCollection<never, never>::getAll() now returns array<stdClass> instead of array{}
  • Conditional return types like (T is never ? false : bool) are preserved unchanged
  • All 11,869 tests pass, PHPStan self-analysis clean

…d generic method

When a generic class is constructed with an empty array default parameter,
the template type resolves to `never`. Method return types using this template
(e.g. `TElement|null`) then collapse to `null`, causing `===` comparisons
against non-null values to be seen as always-false, which makes subsequent
code unreachable and narrows variable types to `*NEVER*`.

The fix replaces `never` with the template's declared bound when resolving
class-level template types in method return types, but only when the return
type does not contain conditional types (which need the actual `never` value
to evaluate correctly, e.g. `T is never ? false : bool`).

Fixes phpstan/phpstan#14281
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants