Skip to content

Commit

Permalink
Type projections, part 1: call-site variance in GenericObjectType
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil committed Jun 23, 2023
1 parent b9d2cd8 commit be55d53
Show file tree
Hide file tree
Showing 12 changed files with 577 additions and 19 deletions.
40 changes: 36 additions & 4 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
use PHPStan\Type\FloatType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Helper\GetTemplateTypeType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
Expand Down Expand Up @@ -602,6 +603,21 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
{
$mainTypeName = strtolower($typeNode->type->name);
$genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope);
$variances = array_map(
static function (string $variance): TemplateTypeVariance {
switch ($variance) {
case GenericTypeNode::VARIANCE_INVARIANT:
return TemplateTypeVariance::createInvariant();
case GenericTypeNode::VARIANCE_COVARIANT:
return TemplateTypeVariance::createCovariant();
case GenericTypeNode::VARIANCE_CONTRAVARIANT:
return TemplateTypeVariance::createContravariant();
case GenericTypeNode::VARIANCE_BIVARIANT:
return TemplateTypeVariance::createBivariant();
}
},
$typeNode->variances,
);

if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') {
if (count($genericTypes) === 1) { // array<ValueType>
Expand Down Expand Up @@ -748,7 +764,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na

if ($mainTypeClassName !== null) {
if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) {
return new GenericObjectType($mainTypeClassName, $genericTypes);
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
}

$classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName);
Expand All @@ -762,13 +778,19 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
return new GenericObjectType($mainTypeClassName, [
new MixedType(true),
$genericTypes[0],
], null, null, [
TemplateTypeVariance::createInvariant(),
$variances[0],
]);
}

if (count($genericTypes) === 2) {
return new GenericObjectType($mainTypeClassName, [
$genericTypes[0],
$genericTypes[1],
], null, null, [
$variances[0],
$variances[1],
]);
}
}
Expand All @@ -780,6 +802,11 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
$genericTypes[0],
$mixed,
$mixed,
], null, null, [
TemplateTypeVariance::createInvariant(),
$variances[0],
TemplateTypeVariance::createInvariant(),
TemplateTypeVariance::createInvariant(),
]);
}

Expand All @@ -790,19 +817,24 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
$genericTypes[1],
$mixed,
$mixed,
], null, null, [
$variances[0],
$variances[1],
TemplateTypeVariance::createInvariant(),
TemplateTypeVariance::createInvariant(),
]);
}
}

if (!$mainType->isIterable()->yes()) {
return new GenericObjectType($mainTypeClassName, $genericTypes);
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
}

if (
count($genericTypes) !== 1
|| $classReflection->getTemplateTypeMap()->count() === 1
) {
return new GenericObjectType($mainTypeClassName, $genericTypes);
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
}
}
}
Expand All @@ -824,7 +856,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
}

if ($mainTypeClassName !== null) {
return new GenericObjectType($mainTypeClassName, $genericTypes);
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
}

return new ErrorType();
Expand Down
109 changes: 107 additions & 2 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Generic\TypeProjectionHelper;
use PHPStan\Type\Type;
use PHPStan\Type\TypeAlias;
use PHPStan\Type\TypehintHelper;
Expand Down Expand Up @@ -94,6 +97,10 @@ class ClassReflection

private ?TemplateTypeMap $activeTemplateTypeMap = null;

private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null;

private ?TemplateTypeVarianceMap $callSiteVarianceMap = null;

/** @var array<string,ClassReflection>|null */
private ?array $ancestors = null;

Expand Down Expand Up @@ -140,6 +147,7 @@ public function __construct(
private ?TemplateTypeMap $resolvedTemplateTypeMap,
private ?ResolvedPhpDocBlock $stubPhpDocBlock,
private ?string $extraCacheKey = null,
private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null,
)
{
}
Expand Down Expand Up @@ -241,7 +249,11 @@ public function getDisplayName(bool $withTemplateTypes = true): string
return $name;
}

return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>';
return $name . '<' . implode(',', array_map(
static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::typeOnly()),
$this->getActiveTemplateTypeMap()->getTypes(),
$this->getCallSiteVarianceMap()->getVariances(),
)) . '>';
}

public function getCacheKey(): string
Expand All @@ -254,7 +266,11 @@ public function getCacheKey(): string
$cacheKey = $this->displayName;

if ($this->resolvedTemplateTypeMap !== null) {
$cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>';
$cacheKey .= '<' . implode(',', array_map(
static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::cache()),
$this->getActiveTemplateTypeMap()->getTypes(),
$this->getCallSiteVarianceMap()->getVariances(),
)) . '>';
}

if ($this->extraCacheKey !== null) {
Expand Down Expand Up @@ -1239,6 +1255,32 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap
return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap();
}

private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap
{
if ($this->defaultCallSiteVarianceMap !== null) {
return $this->defaultCallSiteVarianceMap;
}

$resolvedPhpDoc = $this->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
$this->defaultCallSiteVarianceMap = TemplateTypeVarianceMap::createEmpty();
return $this->defaultCallSiteVarianceMap;
}

$map = [];
foreach ($this->getTemplateTags() as $templateTag) {
$map[$templateTag->getName()] = TemplateTypeVariance::createInvariant();
}

$this->defaultCallSiteVarianceMap = new TemplateTypeVarianceMap($map);
return $this->defaultCallSiteVarianceMap;
}

public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
{
return $this->callSiteVarianceMap ??= $this->resolvedCallSiteVarianceMap ?? $this->getDefaultCallSiteVarianceMap();
}

public function isGeneric(): bool
{
if ($this->isGeneric === null) {
Expand Down Expand Up @@ -1272,6 +1314,26 @@ public function typeMapFromList(array $types): TemplateTypeMap
return new TemplateTypeMap($map);
}

/**
* @param array<int, TemplateTypeVariance> $variances
*/
public function varianceMapFromList(array $variances): TemplateTypeVarianceMap
{
$resolvedPhpDoc = $this->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
return new TemplateTypeVarianceMap([]);
}

$map = [];
$i = 0;
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
$map[$tag->getName()] = $variances[$i] ?? TemplateTypeVariance::createInvariant();
$i++;
}

return new TemplateTypeVarianceMap($map);
}

/** @return array<int, Type> */
public function typeMapToList(TemplateTypeMap $typeMap): array
{
Expand All @@ -1288,6 +1350,22 @@ public function typeMapToList(TemplateTypeMap $typeMap): array
return $list;
}

/** @return array<int, TemplateTypeVariance> */
public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array
{
$resolvedPhpDoc = $this->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
return [];
}

$list = [];
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
$list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant();
}

return $list;
}

/**
* @param array<int, Type> $types
*/
Expand All @@ -1308,6 +1386,33 @@ public function withTypes(array $types): self
$this->anonymousFilename,
$this->typeMapFromList($types),
$this->stubPhpDocBlock,
null,
$this->resolvedCallSiteVarianceMap,
);
}

/**
* @param array<int, TemplateTypeVariance> $variances
*/
public function withVariances(array $variances): self
{
return new self(
$this->reflectionProvider,
$this->initializerExprTypeResolver,
$this->fileTypeMapper,
$this->stubPhpDocProvider,
$this->phpDocInheritanceResolver,
$this->phpVersion,
$this->propertiesClassReflectionExtensions,
$this->methodsClassReflectionExtensions,
$this->allowedSubTypesClassReflectionExtensions,
$this->displayName,
$this->reflection,
$this->anonymousFilename,
$this->resolvedTemplateTypeMap,
$this->stubPhpDocBlock,
null,
$this->varianceMapFromList($variances),
);
}

Expand Down

0 comments on commit be55d53

Please sign in to comment.