#15331·phpstan

2.3.x-dev: internal error "The initial type of an unresolved template argument is never itself unresolved" (unresolvedTemplateArguments)

Author: gnutixCreated Sep 28, 2026Updated Sep 28, 2026

Bug report

[!NOTE] This only happens on unreleased 2.3.x-dev (7c94037). Every released version is fine. I ran the 2.3.x branch on our codebase early out of curiosity about phpstan/phpstan-src#6604, phpstan/phpstan-src#6610, phpstan/phpstan-src#6617 and phpstan/phpstan-src#6622, so this isn't blocking anything; I'm reporting it early in case it helps before 2.3.0.

With featureToggles.unresolvedTemplateArguments enabled (part of bleeding edge on 2.3.x), the following crashes PHPStan with an internal error, even at level 0:

Internal error: The initial type of an unresolved template argument is never itself unresolved.
php
/**
 * @template-covariant T
 */
final class Box
{
    /**
     * @template U
     * @param U $value
     * @return self<U>
     */
    public static function of(mixed $value): self
    {
        return new self();
    }

    /**
     * @template U
     * @param self<U> $a
     * @param self<U> $b
     * @return self<T|U>
     */
    public function zip(self $a, self $b): self
    {
        return new self();
    }

    public function get(): void
    {
    }
}

function test(): void
{
    Box::of(1)->zip(Box::of(2), Box::of(3))->get(); // 
}

It takes all three of these; removing any one of them makes it pass:

  • the method template U inferred from two arguments (the same literal twice crashes too, a single argument doesn't);
  • a return type mixing it with the class template: self<T|U> crashes, self<U> doesn't;
  • an impure method called on the result: SimpleImpurePoint::createFromVariant() asks for the class display name, which describes the T|U union and calls generalize() on the unresolved argument.

Turning off unresolvedTemplateArguments makes the error go away.

Stack trace
PHPStan\ShouldNotHappenException: The initial type of an unresolved template argument is never itself unresolved.
  in src/Type/Generic/UnresolvedTemplateArgumentType.php:64
#0 src/Type/Generic/UnresolvedTemplateArgumentType.php(93): PHPStan\Type\Generic\UnresolvedTemplateArgumentType->__construct()
#1 src/Type/Generic/UnresolvedTemplateArgumentType.php(224): PHPStan\Type\Generic\UnresolvedTemplateArgumentType->withInitialType()
#2 src/Type/UnionType.php(462): PHPStan\Type\Generic\UnresolvedTemplateArgumentType->generalize()
#3 [internal function]: PHPStan\Type\UnionType::{closure:{closure:PHPStan\Type\UnionType::describe():459}:460}()
#4 src/Type/UnionType.php(460): array_map()
#5 src/Type/VerbosityLevel.php(218): PHPStan\Type\UnionType->{closure:PHPStan\Type\UnionType::describe():459}()
#6 src/Type/UnionType.php(459): PHPStan\Type\VerbosityLevel->handle()
#7 src/Type/Generic/TypeProjectionHelper.php(15): PHPStan\Type\UnionType->describe()
#8 src/Reflection/ClassReflection.php(330): PHPStan\Type\Generic\TypeProjectionHelper::describe()
#9 src/Reflection/Callables/SimpleImpurePoint.php(108): PHPStan\Reflection\ClassReflection->getDisplayName()
#10 src/Analyser/ExprHandler/MethodCallHandler.php(194): PHPStan\Reflection\Callables\SimpleImpurePoint::createFromVariant()
#11 src/Analyser/NodeScopeResolver.php(638): PHPStan\Analyser\ExprHandler\MethodCallHandler->processExpr()
#12 src/Analyser/NodeScopeResolver.php(591): PHPStan\Analyser\NodeScopeResolver->processExprNodeInternal()
#13 src/Analyser/StmtHandler/ExpressionHandler.php(63): PHPStan\Analyser\NodeScopeResolver->processExprNode()
#14 src/Analyser/NodeScopeResolver.php(333): PHPStan\Analyser\StmtHandler\ExpressionHandler->processStmt()
#15 src/Analyser/StatementsHandler.php(308): PHPStan\Analyser\NodeScopeResolver->processStmtNode()
#16 src/Analyser/StatementsHandler.php(398): PHPStan\Analyser\StatementsHandler->processStatementStep()

In our codebase it came from a generic Timeline<T> with zip(self<U> ...$others): self<list<T|U|null>>, followed by ->values().

Code snippet that reproduces the problem

The playground runs released versions, so here is a standalone repository instead: https://github.com/gnutix/phpstan-unresolved-template-argument-crash (composer install, then analyse). Its CI run shows the crash.

Expected output

No internal error: the snippet has nothing to report at level 0.

Did PHPStan help you today? Did it make you happy in any way?

The closure-signature and static-variable inference merged this weekend is impressive. On our codebase (level max), it already makes a handful of @phpstan-ignores and baseline entries obsolete and finds a couple of dead branches. Thanks!