Skip to content

[6.x] Resolve self/static template args against implementing class before bound check#11833

Open
alies-dev wants to merge 3 commits into
vimeo:6.xfrom
alies-dev:fix/self-template-arg-not-expanded
Open

[6.x] Resolve self/static template args against implementing class before bound check#11833
alies-dev wants to merge 3 commits into
vimeo:6.xfrom
alies-dev:fix/self-template-arg-not-expanded

Conversation

@alies-dev
Copy link
Copy Markdown
Contributor

@alies-dev alies-dev commented May 6, 2026

Fixes #11199.

Currently @implements I<self> errors with Extended template param ... type self given, even when the implementing class clearly satisfies the bound. Replacing self with the FQN makes the error go away, and PHPStan accepts both forms.

The cause: ClassLikeAnalyzer::checkTemplateParams was passing $extended_type straight from template_extended_params into UnionTypeComparator::isContainedBy, without first running it through TypeExpander::expandUnion. So self/static survived as a raw TNamedObject and the bound check had no way to know it should resolve to the implementing class. Other call sites (Codebase\ClassLikes:1078, Codebase\Methods:750) already do this expansion; this site was just missing it.

The fix is the missing expandUnion call. Same code path is used for @extends, @template-implements, @template-extends, and trait @use, so all of those are covered too.

Tests exercise the full matrix (self/static across the four annotation forms plus trait @use) plus two negative cases proving the bound check still fires when it should.

…ound check

`ClassLikeAnalyzer::checkTemplateParams` was comparing the raw `$extended_type`
against the parent's template bound without first running it through
`TypeExpander::expandUnion`. As a result `self`/`static` survived as an
unresolved `TNamedObject('self'|'static')` and `UnionTypeComparator::isContainedBy`
correctly returned false against any class bound, producing a false-positive
`InvalidTemplateParam` on annotations like

    /** @implements Holder<self> */
    final class C extends Base implements Holder {}

even when the implementing class clearly satisfied the bound. Replacing `self`
with the FQN made the error go away. The fix runs `expandUnion` against the
implementing class up-front, matching the call shape in
`Codebase\ClassLikes::1078` and `Codebase\Methods::750`.

Covers all four `@implements`/`@extends`/`@template-implements`/
`@template-extends` surfaces, plus trait `@use` (which routes through the same
`checkTemplateParams`). Resolves vimeo#11199.
@alies-dev alies-dev changed the title Resolve self/static template args against implementing class before bound check [6.x] Resolve self/static template args against implementing class before bound check May 6, 2026
alies-dev added 2 commits May 7, 2026 10:33
…* coverage

Matches the more cautious precedent in `Codebase\Methods::750`. For final
classes, `static` and `self` are equivalent and the resulting type should not
carry `is_static = true`; passing `$storage->final` preserves that.

Adds 5 regression tests:
- nestedStaticInGenericTemplateArgFinalClass (Holder<Box<static>> on a final class)
- nestedStaticInGenericTemplateArgNonFinalClass
- nestedSelfInGenericTemplateArg
- staticAsTemplateArgInTemplateImplementsRespectsParentBound
- staticAsTemplateArgInTemplateExtendsRespectsParentBound

The nested-generic cases were already accepted by the previous patch (the
expansion recurses through `TGenericObject::type_params`); they are committed
as documentation that the bound check stays correct in those shapes.
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.

False positive InvalidTemplateParam when use "static" as Template Param

1 participant