Skip to content

Fix phpstan/phpstan#9045: Narrowed template on an interface is ignored when generics is not specified#5339

Merged
staabm merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-znh23kc
Mar 30, 2026
Merged

Fix phpstan/phpstan#9045: Narrowed template on an interface is ignored when generics is not specified#5339
staabm merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-znh23kc

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented Mar 29, 2026

Summary

When a child interface narrows a template bound and passes it to a parent interface via @extends, using the child without explicit generic parameters incorrectly resolved the template to the parent's wider bound instead of the child's narrowed bound.

For example:

/** @template T of TranslationInterface */
interface TranslatableInterface {
    /** @return T */
    public function getTranslation(): TranslationInterface;
}

/** @template T of TransportTranslationInterface @extends TranslatableInterface<T> */
interface TransportInterface extends TranslatableInterface {}

// $transport->getTranslation() was incorrectly typed as TranslationInterface
// instead of TransportTranslationInterface
function foo(TransportInterface $transport): void {
    $transport->getTranslation(); // should be TransportTranslationInterface
}

Changes

  • Added getActiveTemplateTypeMapForAncestorResolution() method in src/Reflection/ClassReflection.php that resolves ErrorType entries to their template bounds, but only when the bound is not mixed. This preserves the existing behavior for mixed-bounded templates (like Iterator<TKey>) while correctly resolving narrowed bounds.
  • Updated getImmediateInterfaces() and getParentClass() in ClassReflection to use this new method when resolving @extends and @implements type declarations
  • Updated tests/PHPStan/Analyser/nsrt/bug-2676.php: Collection's TKey of array-key now correctly resolves to (int|string) instead of mixed when not explicitly specified

Root cause

When a plain ObjectType (without generics) referred to a generic class, ObjectType::getClassReflection() created a ClassReflection with all template types set to ErrorType. In getImmediateInterfaces(), when resolving @extends TranslatableInterface<T>, the ErrorType for T was propagated to the parent, where it was resolved using the parent's own (wider) template bound (TranslationInterface) instead of the child's narrower bound (TransportTranslationInterface).

The fix intercepts this propagation: before passing template types to ancestor resolution, ErrorType entries with non-mixed bounds are resolved to their template bounds. This ensures the narrowed bound is correctly propagated through the inheritance chain.

Test

Added tests/PHPStan/Analyser/nsrt/bug-9045.php that verifies TransportInterface::getTranslation() correctly returns TransportTranslationInterface when the interface is used without explicit generic parameters.

Fixes phpstan/phpstan#9045
Fixes phpstan/phpstan#7185
Fixes phpstan/phpstan#13204

@VincentLanglet VincentLanglet requested a review from staabm March 30, 2026 10:12
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a regression test for

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added regression tests for both issues:

  • tests/PHPStan/Analyser/nsrt/bug-7185.php — Tests that iterator_to_array() on a Collection (extending IteratorAggregate<TKey, TValue of object>) without explicit generics correctly resolves values as object instead of mixed.

  • tests/PHPStan/Analyser/nsrt/bug-13204.php — Tests that instanceof ParentNode (extending ArrayAccess<int, TChild|null> with TChild of object) correctly resolves offset access as object|null instead of mixed.

Both tests pass, and the full test suite (11691 tests) and phpstan analysis are clean.

@staabm staabm requested a review from VincentLanglet March 30, 2026 11:40
github-actions bot and others added 2 commits March 30, 2026 13:50
… generics not specified

- When a child interface narrows a template bound (e.g. `@template T of SpecificType`)
  and passes it to a parent via `@extends Parent<T>`, using the child without specifying
  generics now correctly resolves to the narrowed bound instead of the parent's wider bound
- Added `getActiveTemplateTypeMapForAncestorResolution()` in ClassReflection that resolves
  ErrorType entries to template bounds when the bound is not mixed
- Applied this resolution in both `getImmediateInterfaces()` and `getParentClass()`
- Updated bug-2676 test expectation: Collection's TKey now correctly resolves to
  `(int|string)` (array-key bound) instead of `mixed` when not explicitly specified
- New regression test in tests/PHPStan/Analyser/nsrt/bug-9045.php
Both issues involve template bounds not being correctly resolved when
generic types are used without explicit type parameters. These are now
fixed by the getActiveTemplateTypeMapForAncestorResolution() change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-znh23kc branch from adab3f5 to f015f24 Compare March 30, 2026 11:50
@staabm staabm merged commit 11f55d4 into phpstan:2.1.x Mar 30, 2026
651 of 655 checks passed
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 30, 2026

thank you!

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.

3 participants