Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 83 additions & 34 deletions src/Analyser/Generics/TemplateArgumentObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,8 @@ public function collectCall(Expr $site, ParametersAcceptor $acceptor, array $arg

/**
* Replaces the call's own template types by markers of the call site. A
* marker behaves as its delegate (mixed for a bare @template), so normalizing
* a union lets a naked marker absorb its siblings: T|null becomes the marker,
* marker behaves as its delegate (mixed for a bare @template), so a naked
* marker absorbs the siblings its delegate covers: T|null becomes the marker,
* which is what links Collection<T|null> to a Collection<unresolved> argument.
* A sibling that itself carries a marker of the call is kept next to the
* naked one instead - Foo<T>|T has to keep both members for the argument to
Expand All @@ -375,9 +375,24 @@ private function replaceInferableTemplates(Type $type, Expr $site, TemplateTypeM
$structural[] = $member;
}
}
if ($naked === [] || $structural === []) {
if ($naked === []) {
return TypeCombinator::union(...$members);
}
if ($structural === []) {
// a union keeps markers apart from everything else (see
// TypeCombinator::union()) - the absorption is this method's own
return TypeCombinator::union(...array_filter($members, static function (Type $member) use ($naked): bool {
if ($member instanceof UnresolvedTemplateArgumentType) {
return true;
}
foreach ($naked as $marker) {
if ($marker->getDelegate()->isSuperTypeOf($member)->yes()) {
return false;
}
}
return true;
}));
}

return new UnionType([...$naked, ...$structural]);
}
Expand Down Expand Up @@ -461,39 +476,9 @@ private function observeSend(TemplateArgumentConstraints $constraints, Type $dec
if (!$template instanceof TemplateType || !isset($declaredArguments[$i])) {
continue;
}
$declaredArgument = $declaredArguments[$i];
if (!$argument instanceof UnresolvedTemplateArgumentType) {
$constraints = $this->observeSend($constraints, $declaredArgument, $argument, $isCallArgument);
continue;
}
if (
$isCallArgument
&& ($argument->getInitialType() === null || $argument->getInitialType() instanceof NeverType)
&& self::hasOnlyInferableTemplates($declaredArgument)
) {
$declaredArgument = TemplateTypeHelper::resolveToDefaults($declaredArgument);
}
if (self::isUninformativeSendTarget($declaredArgument)) {
// An unresolved call parameter, like mixed, uses the object without
// constraining it. Return/property templates are fixed by their
// declaration and must keep an empty argument compatible with them.
if (($isCallArgument && self::hasOnlyInferableTemplates($declaredArgument)) || ($declaredArgument instanceof MixedType && !$declaredArgument instanceof TemplateType)) {
$constraints = $constraints->withUnconstrainingSend($argument);
}

continue;
}

$callSiteVariance = $declaredVariances->getVariance($template->getName()) ?? TemplateTypeVariance::createInvariant();
$effectiveVariance = $callSiteVariance->invariant() ? $template->getVariance() : $callSiteVariance;
$constraints = $constraints->withSend($argument, $declaredArgument, $effectiveVariance);

// a site whose inferred argument itself carries markers (wrap(new Foo(1)))
$initial = $argument->getInitialType();
if ($initial === null) {
continue;
}
$constraints = $this->observeSend($constraints, $declaredArgument, $initial, $isCallArgument);
$constraints = $this->observeArgumentSend($constraints, $declaredArguments[$i], $argument, $effectiveVariance, $isCallArgument);
}

return $constraints;
Expand Down Expand Up @@ -521,6 +506,70 @@ private function observeSend(TemplateArgumentConstraints $constraints, Type $dec
return $constraints;
}

/**
* A template argument of an object sent to the declared argument at the same
* position. A union of several sites' markers (Box<T|U> of zip(of(1), of(2)))
* is split like Hack splits (#1 | #2) <: int: every marker in it gets the
* declared argument as an upper bound. Something flowing into the union does
* not split, so a contravariant send reaches only a lone marker.
*/
private function observeArgumentSend(TemplateArgumentConstraints $constraints, Type $declaredArgument, Type $argument, TemplateTypeVariance $variance, bool $isCallArgument): TemplateArgumentConstraints
{
if ($argument instanceof UnresolvedTemplateArgumentType && !$argument->isClosureSignature()) {
return $this->observeMarkerSend($constraints, $declaredArgument, $argument, $variance, $isCallArgument);
}
if (!$argument instanceof UnionType || $argument instanceof TemplateType) {
return $this->observeSend($constraints, $declaredArgument, $argument, $isCallArgument);
}

$upperBound = $variance->invariant() || $variance->covariant() ? TemplateTypeVariance::createCovariant() : null;
foreach ($argument->getTypes() as $member) {
if (!$member instanceof UnresolvedTemplateArgumentType || $member->isClosureSignature()) {
$constraints = $this->observeSend($constraints, $declaredArgument, $member, $isCallArgument);
continue;
}
if ($upperBound === null) {
continue;
}
$constraints = $this->observeMarkerSend($constraints, $declaredArgument, $member, $upperBound, $isCallArgument);
}

return $constraints;
}

private function observeMarkerSend(TemplateArgumentConstraints $constraints, Type $declaredArgument, UnresolvedTemplateArgumentType $marker, TemplateTypeVariance $variance, bool $isCallArgument): TemplateArgumentConstraints
{
$initial = $marker->getInitialType();
if (
$isCallArgument
&& ($initial === null || $initial instanceof NeverType)
&& self::hasOnlyInferableTemplates($declaredArgument)
) {
$declaredArgument = TemplateTypeHelper::resolveToDefaults($declaredArgument);
}
if (self::isUninformativeSendTarget($declaredArgument)) {
// An unresolved call parameter, like mixed, uses the object without
// constraining it. Return/property templates are fixed by their
// declaration and must keep an empty argument compatible with them.
if (($isCallArgument && self::hasOnlyInferableTemplates($declaredArgument)) || ($declaredArgument instanceof MixedType && !$declaredArgument instanceof TemplateType)) {
$constraints = $constraints->withUnconstrainingSend($marker);
}

return $constraints;
}

$constraints = $constraints->withSend($marker, $declaredArgument, $variance);
if ($initial === null) {
return $constraints;
}

// a site whose inferred argument itself carries markers: nested in objects
// (wrap(new Foo(1))), or bare as the sites it was inferred from
// (collect(of(1), of(2)) returning Coll<U>) - an upper bound of the site
// bounds those from above too
return $this->observeArgumentSend($constraints, $declaredArgument, $initial, $variance, $isCallArgument);
}

/**
* observeSend() observes nothing sent to plain mixed outside a call
* argument: a union recurses into its members, a bare marker never
Expand Down
26 changes: 19 additions & 7 deletions src/Analyser/Generics/TemplateArgumentSolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,20 +222,29 @@ private function resolveObservation(array $observation): Type
// type is never clamped
if (!$templateVariance->covariant() || $acceptsAnything) {
$covariantFallback = null;
$markerFallback = null;
foreach ($observation['sends'] as [$sent, $variance]) {
if ($variance->covariant()) {
// an upper bound; with nothing inferred it is the best information there is
if (!$acceptsAnything) {
continue;
}
// another site's variable is an edge (passing the object to
// join(Box<U>, Box<U>) makes it a lower bound of the call's U),
// not information - it only counts when nothing else bounds this one
if ($sent instanceof UnresolvedTemplateArgumentType) {
$markerFallback ??= $sent;
continue;
}
$covariantFallback ??= $this->substituteResolutions($sent);
continue;
}
$sent = $this->substituteResolutions($sent);
if ($variance->contravariant()) {
// Foo<contravariant int> accepts Foo<X> for every X wider than int
$lowerBounds[] = $sent;
continue;
}
if ($variance->covariant()) {
// an upper bound; with nothing inferred it is the best information there is
if ($acceptsAnything) {
$covariantFallback ??= $sent;
}
continue;
}
if (!$variance->invariant()) {
continue;
}
Expand All @@ -251,6 +260,9 @@ private function resolveObservation(array $observation): Type
return $sent;
}

if ($covariantFallback === null && $markerFallback !== null) {
$covariantFallback = $this->substituteResolutions($markerFallback);
}
if ($covariantFallback !== null) {
if (TemplateArgumentStats::$enabled) {
TemplateArgumentStats::increment('resolvedBySend');
Expand Down
9 changes: 8 additions & 1 deletion src/Analyser/Traverser/GenericTypeTemplateTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\NarrowedSubjectType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverserCallable;

Expand All @@ -25,7 +26,10 @@
* UnresolvedTemplateArgumentType keyed by the site so the body's sends and
* method calls can decide it; the second pass substitutes the frame's
* resolution. An inferred argument that already carries another site's marker
* passes through - the outer result then resolves the inner site.
* passes through - the outer result then resolves the inner site. So does any
* inferred argument of a covariant template: nothing the body does with the
* object can narrow it (see
* ResolvedFunctionVariantWithOriginal::unresolvedOrResolvedTemplateArgument()).
*/
#[ReferencedByTurboExtension(key: 'genericTypeTemplateTraverser')]
final class GenericTypeTemplateTraverser implements TypeTraverserCallable
Expand Down Expand Up @@ -63,6 +67,9 @@ public function traverse(Type $type, callable $traverse): Type
if ($initialType instanceof UnresolvedTemplateArgumentType) {
return $initialType;
}
if (!$synthetic && $initialType !== null && !$initialType instanceof NeverType && $type->getVariance()->covariant()) {
return $initialType;
}

return new UnresolvedTemplateArgumentType($this->site, $type, $initialType);
}
Expand Down
24 changes: 21 additions & 3 deletions src/Reflection/ResolvedFunctionVariantWithOriginal.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\NarrowedSubjectType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NonAcceptingNeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
Expand Down Expand Up @@ -280,8 +281,15 @@ public function resolveConditionalTypes(Type $type): Type
private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance, ?Expr $site = null, ?TemplateArgumentFrame $frame = null, bool $allowUnresolved = true): Type
{
$references = $type->getReferencedTemplateTypes($positionVariance);
$onlyCovariant = [];
if ($site !== null) {
foreach ($references as $reference) {
$name = $reference->getType()->getName();
$onlyCovariant[$name] = ($onlyCovariant[$name] ?? true) && $reference->getPositionVariance()->covariant();
}
}

$objectCb = function (Type $type, callable $traverse) use ($references, $site, $frame, $allowUnresolved): Type {
$objectCb = function (Type $type, callable $traverse) use ($references, $onlyCovariant, $site, $frame, $allowUnresolved): Type {
if (
$type instanceof TemplateType
&& !$type instanceof NarrowedSubjectType
Expand All @@ -294,7 +302,7 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
}

if ($site !== null && $frame !== null) {
$newType = $this->unresolvedOrResolvedTemplateArgument($type, $newType, $site, $frame, $allowUnresolved);
$newType = $this->unresolvedOrResolvedTemplateArgument($type, $newType, $site, $frame, $allowUnresolved, $onlyCovariant[$type->getName()] ?? false);
} else {
$newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType);
}
Expand Down Expand Up @@ -380,13 +388,23 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
* argument that already carries another site's marker passes through - the
* outer result then resolves the inner site), under a resolved frame its
* resolution, else the exact inferred type.
*
* A template occurring only covariantly in the return type gets no marker
* when something was inferred for it: nothing sent to the result can narrow
* a covariant argument, so it is the union of what it was inferred from right
* away (Hack solves such a type variable the same way). The markers of other
* sites in that union stay, each its own variable - a send of the result
* reaches every one of them (zip(of(1), of(2)) sent to Box<int>).
*/
private function unresolvedOrResolvedTemplateArgument(TemplateType $template, Type $inferred, Expr $site, TemplateArgumentFrame $frame, bool $allowUnresolved): Type
private function unresolvedOrResolvedTemplateArgument(TemplateType $template, Type $inferred, Expr $site, TemplateArgumentFrame $frame, bool $allowUnresolved, bool $onlyCovariant): Type
{
if ($allowUnresolved && $frame->isObserving()) {
if ($inferred instanceof UnresolvedTemplateArgumentType) {
return $inferred;
}
if ($onlyCovariant && !$inferred instanceof NeverType) {
return $inferred;
}
return new UnresolvedTemplateArgumentType($site, $template, $inferred);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '76fefc6';
public const EXPECTED_EXTENSION_VERSION = '33de4ac';

private static bool $active = false;

Expand Down
12 changes: 11 additions & 1 deletion src/Type/Generic/UnresolvedTemplateArgumentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public function getDelegate(): Type
return $this->initialType ?? $this->templateType->getDefault() ?? $this->templateType->getBound();
}

/**
* A parameter or return type of a closure being inferred (see
* ClosureSignatureInference), as opposed to a template argument of a call
* or `new` site.
*/
public function isClosureSignature(): bool
{
return $this->site instanceof Expr\Closure || $this->site instanceof Expr\ArrowFunction;
}

public function withInitialType(?Type $initialType): self
{
return new self($this->site, $this->templateType, $initialType);
Expand Down Expand Up @@ -160,7 +170,7 @@ public function describe(VerbosityLevel $level): string
return sprintf('unresolved#%d(%s)', spl_object_id($this->site), $this->getDelegate()->describe($level));
}

if ($this->site instanceof Expr\Closure || $this->site instanceof Expr\ArrowFunction) {
if ($this->isClosureSignature()) {
// a closure signature marker outlives the observation pass when its
// site settled on the declared types it stands for
return $this->getDelegate()->describe($level);
Expand Down
Loading
Loading