Skip to content

Commit be55d53

Browse files
authored
Type projections, part 1: call-site variance in GenericObjectType
1 parent b9d2cd8 commit be55d53

File tree

12 files changed

+577
-19
lines changed

12 files changed

+577
-19
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
use PHPStan\Type\FloatType;
6666
use PHPStan\Type\Generic\GenericClassStringType;
6767
use PHPStan\Type\Generic\GenericObjectType;
68+
use PHPStan\Type\Generic\TemplateTypeVariance;
6869
use PHPStan\Type\Helper\GetTemplateTypeType;
6970
use PHPStan\Type\IntegerRangeType;
7071
use PHPStan\Type\IntegerType;
@@ -602,6 +603,21 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
602603
{
603604
$mainTypeName = strtolower($typeNode->type->name);
604605
$genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope);
606+
$variances = array_map(
607+
static function (string $variance): TemplateTypeVariance {
608+
switch ($variance) {
609+
case GenericTypeNode::VARIANCE_INVARIANT:
610+
return TemplateTypeVariance::createInvariant();
611+
case GenericTypeNode::VARIANCE_COVARIANT:
612+
return TemplateTypeVariance::createCovariant();
613+
case GenericTypeNode::VARIANCE_CONTRAVARIANT:
614+
return TemplateTypeVariance::createContravariant();
615+
case GenericTypeNode::VARIANCE_BIVARIANT:
616+
return TemplateTypeVariance::createBivariant();
617+
}
618+
},
619+
$typeNode->variances,
620+
);
605621

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

749765
if ($mainTypeClassName !== null) {
750766
if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) {
751-
return new GenericObjectType($mainTypeClassName, $genericTypes);
767+
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
752768
}
753769

754770
$classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName);
@@ -762,13 +778,19 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
762778
return new GenericObjectType($mainTypeClassName, [
763779
new MixedType(true),
764780
$genericTypes[0],
781+
], null, null, [
782+
TemplateTypeVariance::createInvariant(),
783+
$variances[0],
765784
]);
766785
}
767786

768787
if (count($genericTypes) === 2) {
769788
return new GenericObjectType($mainTypeClassName, [
770789
$genericTypes[0],
771790
$genericTypes[1],
791+
], null, null, [
792+
$variances[0],
793+
$variances[1],
772794
]);
773795
}
774796
}
@@ -780,6 +802,11 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
780802
$genericTypes[0],
781803
$mixed,
782804
$mixed,
805+
], null, null, [
806+
TemplateTypeVariance::createInvariant(),
807+
$variances[0],
808+
TemplateTypeVariance::createInvariant(),
809+
TemplateTypeVariance::createInvariant(),
783810
]);
784811
}
785812

@@ -790,19 +817,24 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
790817
$genericTypes[1],
791818
$mixed,
792819
$mixed,
820+
], null, null, [
821+
$variances[0],
822+
$variances[1],
823+
TemplateTypeVariance::createInvariant(),
824+
TemplateTypeVariance::createInvariant(),
793825
]);
794826
}
795827
}
796828

797829
if (!$mainType->isIterable()->yes()) {
798-
return new GenericObjectType($mainTypeClassName, $genericTypes);
830+
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
799831
}
800832

801833
if (
802834
count($genericTypes) !== 1
803835
|| $classReflection->getTemplateTypeMap()->count() === 1
804836
) {
805-
return new GenericObjectType($mainTypeClassName, $genericTypes);
837+
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
806838
}
807839
}
808840
}
@@ -824,7 +856,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
824856
}
825857

826858
if ($mainTypeClassName !== null) {
827-
return new GenericObjectType($mainTypeClassName, $genericTypes);
859+
return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances);
828860
}
829861

830862
return new ErrorType();

src/Reflection/ClassReflection.php

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
use PHPStan\Type\Generic\TemplateTypeHelper;
3737
use PHPStan\Type\Generic\TemplateTypeMap;
3838
use PHPStan\Type\Generic\TemplateTypeScope;
39+
use PHPStan\Type\Generic\TemplateTypeVariance;
40+
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
41+
use PHPStan\Type\Generic\TypeProjectionHelper;
3942
use PHPStan\Type\Type;
4043
use PHPStan\Type\TypeAlias;
4144
use PHPStan\Type\TypehintHelper;
@@ -94,6 +97,10 @@ class ClassReflection
9497

9598
private ?TemplateTypeMap $activeTemplateTypeMap = null;
9699

100+
private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null;
101+
102+
private ?TemplateTypeVarianceMap $callSiteVarianceMap = null;
103+
97104
/** @var array<string,ClassReflection>|null */
98105
private ?array $ancestors = null;
99106

@@ -140,6 +147,7 @@ public function __construct(
140147
private ?TemplateTypeMap $resolvedTemplateTypeMap,
141148
private ?ResolvedPhpDocBlock $stubPhpDocBlock,
142149
private ?string $extraCacheKey = null,
150+
private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null,
143151
)
144152
{
145153
}
@@ -241,7 +249,11 @@ public function getDisplayName(bool $withTemplateTypes = true): string
241249
return $name;
242250
}
243251

244-
return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>';
252+
return $name . '<' . implode(',', array_map(
253+
static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::typeOnly()),
254+
$this->getActiveTemplateTypeMap()->getTypes(),
255+
$this->getCallSiteVarianceMap()->getVariances(),
256+
)) . '>';
245257
}
246258

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

256268
if ($this->resolvedTemplateTypeMap !== null) {
257-
$cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>';
269+
$cacheKey .= '<' . implode(',', array_map(
270+
static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::cache()),
271+
$this->getActiveTemplateTypeMap()->getTypes(),
272+
$this->getCallSiteVarianceMap()->getVariances(),
273+
)) . '>';
258274
}
259275

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

1258+
private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap
1259+
{
1260+
if ($this->defaultCallSiteVarianceMap !== null) {
1261+
return $this->defaultCallSiteVarianceMap;
1262+
}
1263+
1264+
$resolvedPhpDoc = $this->getResolvedPhpDoc();
1265+
if ($resolvedPhpDoc === null) {
1266+
$this->defaultCallSiteVarianceMap = TemplateTypeVarianceMap::createEmpty();
1267+
return $this->defaultCallSiteVarianceMap;
1268+
}
1269+
1270+
$map = [];
1271+
foreach ($this->getTemplateTags() as $templateTag) {
1272+
$map[$templateTag->getName()] = TemplateTypeVariance::createInvariant();
1273+
}
1274+
1275+
$this->defaultCallSiteVarianceMap = new TemplateTypeVarianceMap($map);
1276+
return $this->defaultCallSiteVarianceMap;
1277+
}
1278+
1279+
public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
1280+
{
1281+
return $this->callSiteVarianceMap ??= $this->resolvedCallSiteVarianceMap ?? $this->getDefaultCallSiteVarianceMap();
1282+
}
1283+
12421284
public function isGeneric(): bool
12431285
{
12441286
if ($this->isGeneric === null) {
@@ -1272,6 +1314,26 @@ public function typeMapFromList(array $types): TemplateTypeMap
12721314
return new TemplateTypeMap($map);
12731315
}
12741316

1317+
/**
1318+
* @param array<int, TemplateTypeVariance> $variances
1319+
*/
1320+
public function varianceMapFromList(array $variances): TemplateTypeVarianceMap
1321+
{
1322+
$resolvedPhpDoc = $this->getResolvedPhpDoc();
1323+
if ($resolvedPhpDoc === null) {
1324+
return new TemplateTypeVarianceMap([]);
1325+
}
1326+
1327+
$map = [];
1328+
$i = 0;
1329+
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1330+
$map[$tag->getName()] = $variances[$i] ?? TemplateTypeVariance::createInvariant();
1331+
$i++;
1332+
}
1333+
1334+
return new TemplateTypeVarianceMap($map);
1335+
}
1336+
12751337
/** @return array<int, Type> */
12761338
public function typeMapToList(TemplateTypeMap $typeMap): array
12771339
{
@@ -1288,6 +1350,22 @@ public function typeMapToList(TemplateTypeMap $typeMap): array
12881350
return $list;
12891351
}
12901352

1353+
/** @return array<int, TemplateTypeVariance> */
1354+
public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array
1355+
{
1356+
$resolvedPhpDoc = $this->getResolvedPhpDoc();
1357+
if ($resolvedPhpDoc === null) {
1358+
return [];
1359+
}
1360+
1361+
$list = [];
1362+
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1363+
$list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant();
1364+
}
1365+
1366+
return $list;
1367+
}
1368+
12911369
/**
12921370
* @param array<int, Type> $types
12931371
*/
@@ -1308,6 +1386,33 @@ public function withTypes(array $types): self
13081386
$this->anonymousFilename,
13091387
$this->typeMapFromList($types),
13101388
$this->stubPhpDocBlock,
1389+
null,
1390+
$this->resolvedCallSiteVarianceMap,
1391+
);
1392+
}
1393+
1394+
/**
1395+
* @param array<int, TemplateTypeVariance> $variances
1396+
*/
1397+
public function withVariances(array $variances): self
1398+
{
1399+
return new self(
1400+
$this->reflectionProvider,
1401+
$this->initializerExprTypeResolver,
1402+
$this->fileTypeMapper,
1403+
$this->stubPhpDocProvider,
1404+
$this->phpDocInheritanceResolver,
1405+
$this->phpVersion,
1406+
$this->propertiesClassReflectionExtensions,
1407+
$this->methodsClassReflectionExtensions,
1408+
$this->allowedSubTypesClassReflectionExtensions,
1409+
$this->displayName,
1410+
$this->reflection,
1411+
$this->anonymousFilename,
1412+
$this->resolvedTemplateTypeMap,
1413+
$this->stubPhpDocBlock,
1414+
null,
1415+
$this->varianceMapFromList($variances),
13111416
);
13121417
}
13131418

0 commit comments

Comments
 (0)