Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
type projections: enhance template type resolution
  • Loading branch information
jiripudil committed Sep 18, 2023
commit 01131e8f2208b00cf72ec0f66f18ea75aacf97a6
31 changes: 26 additions & 5 deletions src/Type/Generic/TemplateTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,53 @@
namespace PHPStan\Type\Generic;

use PHPStan\Type\ErrorType;
use PHPStan\Type\NonAcceptingNeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VarianceAwareTypeTraverser;

class TemplateTypeHelper
{

/**
* Replaces template types with standin types
*/
public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins, bool $keepErrorTypes = false): Type
public static function resolveTemplateTypes(
Type $type,
TemplateTypeMap $standins,
TemplateTypeVarianceMap $callSiteVariances,
TemplateTypeVariance $positionVariance,
bool $keepErrorTypes = false,
): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $keepErrorTypes): Type {
return VarianceAwareTypeTraverser::map($type, $positionVariance, static function (Type $type, TemplateTypeVariance $variance, callable $traverse) use ($standins, $callSiteVariances, $keepErrorTypes): Type {
if ($type instanceof TemplateType && !$type->isArgument()) {
$newType = $standins->getType($type->getName());
if ($newType === null) {
return $traverse($type);
return $traverse($type, $variance);
}

if ($newType instanceof ErrorType && !$keepErrorTypes) {
return $traverse($type->getBound());
return $traverse($type->getBound(), $variance);
}

$callSiteVariance = $callSiteVariances->getVariance($type->getName());
if ($callSiteVariance === null || $callSiteVariance->invariant()) {
return $newType;
}

if (!$callSiteVariance->covariant() && $variance->covariant()) {
return $traverse($type->getBound(), $variance);
}

if (!$callSiteVariance->contravariant() && $variance->contravariant()) {
return new NonAcceptingNeverType();
}

return $newType;
}

return $traverse($type);
return $traverse($type, $variance);
});
}

Expand Down
7 changes: 7 additions & 0 deletions src/Type/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ public function traverse(callable $cb): Type;
*/
public function traverseSimultaneously(Type $right, callable $cb): Type;

/**
* Traverses inner types while composing their positional variance.
*
* @param callable(Type $type, TemplateTypeVariance $variance): Type $cb
*/
public function traverseWithVariance(TemplateTypeVariance $variance, callable $cb): Type;

public function toPhpDocNode(): TypeNode;

/**
Expand Down
46 changes: 46 additions & 0 deletions src/Type/VarianceAwareTypeTraverser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Type\Generic\TemplateTypeVariance;

/**
* @phpstan-type Callback callable(Type $type, TemplateTypeVariance $variance, callable(Type, TemplateTypeVariance): Type $traverse): Type
*/
class VarianceAwareTypeTraverser
{

/** @var Callback */
private $cb;

/**
* @param Callback $cb
*/
public static function map(Type $type, TemplateTypeVariance $variance, callable $cb): Type
{
$self = new self($cb);

return $self->mapInternal($type, $variance);
}

/**
* @param Callback $cb
*/
private function __construct(callable $cb)
{
$this->cb = $cb;
}

/** @internal */
public function mapInternal(Type $type, TemplateTypeVariance $variance): Type
{
return ($this->cb)($type, $variance, [$this, 'traverseInternal']);
}

/** @internal */
public function traverseInternal(Type $type, TemplateTypeVariance $variance): Type
{
return $type->traverseWithVariance($variance, [$this, 'mapInternal']);
}

}