Skip to content

A method-level @template T is replaced by its bound when the argument's @implements binding is forwarded through an abstract class with an extra, unforwarded class template #77

Description

@klunejko

Observed under v0.10.3

/**
 * @template T of PBase
 * @template U of PBase
 */
interface Q10BoxInterface {}

/**
 * @template TSelf of Q10eAbstract
 * @template T of PBase
 * @template U of PBase
 * @implements Q10BoxInterface<T,U>
 */
abstract class Q10eAbstract implements Q10BoxInterface {}

/** @extends Q10eAbstract<Q10eConcrete,PA,PB> */
final class Q10eConcrete extends Q10eAbstract {}

final class Q10dGenericCreateShape
{
    /**
     * @template T of PBase
     * @template U of PBase
     * @param Q10BoxInterface<T,U> $dto
     */
    public function __construct(protected readonly Q10BoxInterface $dto) {}
}

new Q10dGenericCreateShape(new Q10eConcrete());

Actual:

Q10dGenericCreateShape::__construct(): Argument $dto expects Q10BoxInterface<invariant PBase>, but Q10BoxInterface<PA> was given

The given type is resolved with a single type argument (<PA>, U dropped) and the method template falls back to its bound, checked invariantly. The trigger is the extra leading TSelf template on the abstract class: the same chain without it (variant f, @extends Q10fAbstract<PA,PB>) passes, a direct @implements Q10BoxInterface<PA,PB> on the argument's class (variant d) passes, and the chained argument against a non-templated @param Q10BoxInterface<PA,PB> (variant g) passes.

Expected: T = PA, U = PB inferred from the argument, as PHPStan resolves the @extends → @implements chain; the call is accepted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions