Skip to content
Open
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
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ parameters:
-
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 4
count: 5
path: src/Type/Generic/GenericObjectType.php

-
Expand Down
8 changes: 8 additions & 0 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\InstanceofDeprecated;
use PHPStan\Type\IntegerRangeType;
Expand Down Expand Up @@ -172,6 +173,13 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
// an uncertainty originating in possible ObjectType's class subtypes.
$objectType = $this->getObjectType();

// A class name carries no type arguments, so it is compared in the
// parameterization the generic type implies for its class.
$objectType = GenericObjectType::specializeSubclass(
$genericType instanceof TemplateType ? $genericType->getBound() : $genericType,
$objectType,
);

// Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
// uncertainty into account.
if ($genericType instanceof TemplateType) {
Expand Down
7 changes: 7 additions & 0 deletions src/Type/Generic/GenericClassStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
// an uncertainty originating in possible ObjectType's class subtypes.
$objectType = new ObjectType($type->getValue());

// A class name carries no type arguments, so it is compared in the
// parameterization the generic type implies for its class.
$objectType = GenericObjectType::specializeSubclass(
$genericType instanceof TemplateType ? $genericType->getBound() : $genericType,
$objectType,
);

// Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
// uncertainty into account.
if ($genericType instanceof TemplateType) {
Expand Down
72 changes: 71 additions & 1 deletion src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_keys;
use function array_map;
use function count;
use function get_class;
use function implode;
use function sprintf;

Expand Down Expand Up @@ -443,13 +445,81 @@ public function changeSubtractedType(?Type $subtractedType): Type

// Parent handles sealed type exhaustiveness (returning NeverType when all
// allowed subtypes are subtracted, or a single remaining subtype).
if (!$result instanceof ObjectType || $result->getClassName() !== $this->getClassName()) {
if (!$result instanceof ObjectType) {
return $result;
}

// The remaining subtype comes back as the sealed tag names it, without
// type arguments, and takes the ones this type implies for it.
if ($result->getClassName() !== $this->getClassName()) {
return self::specializeSubclass($this, $result);
}

return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances);
}

/**
* Gives $subclass, a class written without type arguments, the ones
* $supertype implies for it through the class's `@extends` and
* `@implements` tags: Some with Option<int> is Some<int>, Err with
* Result<int, string> is Err<string>. The class of $supertype itself
* takes its arguments and call-site variance as written: X with X<*> is
* X<*>.
*
* Returns $subclass unchanged unless $supertype is a generic object type
* and $subclass is its class or a generic subtype of it. A subtype also
* stays unchanged when $supertype has call-site variance or does not
* determine every type argument of the subtype - an explicit argument
* would claim more than is known.
*/
public static function specializeSubclass(Type $supertype, Type $subclass): Type
{
if (!$supertype instanceof self || get_class($subclass) !== ObjectType::class) {
return $subclass;
}

if ($subclass->getClassName() === $supertype->getClassName()) {
return new self(
$supertype->getClassName(),
$supertype->types,
$subclass->getSubtractedType(),
null,
$supertype->variances,
);
}

foreach ($supertype->variances as $variance) {
if (!$variance->invariant()) {
return $subclass;
}
}

$classReflection = $subclass->getClassReflection();
if ($classReflection === null || !$classReflection->isGeneric()) {
return $subclass;
}

$templateTypeMap = $classReflection->getTemplateTypeMap();
$ancestor = (new self($classReflection->getName(), $classReflection->typeMapToList($templateTypeMap)))
->getAncestorWithClassName($supertype->getClassName());
if ($ancestor === null) {
return $subclass;
}

$inferredTypeMap = $ancestor->inferTemplateTypes($supertype);
foreach (array_keys($templateTypeMap->getTypes()) as $templateName) {
if (!$inferredTypeMap->hasType($templateName)) {
return $subclass;
}
}

return new self(
$classReflection->getName(),
$classReflection->typeMapToList($inferredTypeMap),
$subclass->getSubtractedType(),
);
}

public function toPhpDocNode(): TypeNode
{
/** @var IdentifierTypeNode $parent */
Expand Down
29 changes: 27 additions & 2 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateArrayType;
use PHPStan\Type\Generic\TemplateBenevolentUnionType;
use PHPStan\Type\Generic\TemplateMixedType;
Expand Down Expand Up @@ -902,6 +903,20 @@ private static function intersectWithSubtractedType(
} elseif ($isBAlreadySubtracted->yes()) {
$subtractedType = self::remove($a->getSubtractedType(), $b);

// The subtracted type counts only within $a, where a class written
// without type arguments has the ones $a implies: in Option<int>,
// a subtracted Some is used up by Some<int>.
$subtractedWithinA = GenericObjectType::specializeSubclass(
$a->getTypeWithoutSubtractedType(),
$a->getSubtractedType(),
);
if (
$subtractedWithinA !== $a->getSubtractedType()
&& self::remove($subtractedWithinA, $b) instanceof NeverType
) {
$subtractedType = new NeverType();
}

if (
$subtractedType instanceof NeverType
|| !$subtractedType->isSuperTypeOf($b)->no()
Expand Down Expand Up @@ -1870,6 +1885,7 @@ public static function doIntersect(Type ...$types): Type

// transform IntegerType & ConstantIntegerType to ConstantIntegerType
// transform Child & Parent to Child
// transform Child & Parent<int> to Child<int>
// transform Object & ~null to Object
// transform A & A to A
// transform int[] & string to never
Expand All @@ -1887,7 +1903,13 @@ public static function doIntersect(Type ...$types): Type
$isSuperTypeSubtractableA = $typeWithoutSubtractedTypeA->isSuperTypeOf($types[$i]);
}
if ($isSuperTypeSubtractableA->yes()) {
$types[$i] = self::unionWithSubtractedType($types[$i], $types[$j]->getSubtractedType());
// A generic class written without type arguments is a subtype of
// every parameterization of its generic ancestors, so it is kept -
// with the arguments the dropped ancestor implies for it.
$types[$i] = self::unionWithSubtractedType(
GenericObjectType::specializeSubclass($typeWithoutSubtractedTypeA, $types[$i]),
$types[$j]->getSubtractedType(),
);
array_splice($types, $j--, 1);
$typesCount--;
continue 1;
Expand All @@ -1903,7 +1925,10 @@ public static function doIntersect(Type ...$types): Type
$isSuperTypeSubtractableB = $typeWithoutSubtractedTypeB->isSuperTypeOf($types[$j]);
}
if ($isSuperTypeSubtractableB->yes()) {
$types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType());
$types[$j] = self::unionWithSubtractedType(
GenericObjectType::specializeSubclass($typeWithoutSubtractedTypeB, $types[$j]),
$types[$i]->getSubtractedType(),
);
array_splice($types, $i--, 1);
$typesCount--;
continue 2;
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15266.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

namespace Bug15266;

use function PHPStan\Testing\assertType;

/** @template T = mixed */
final class X {}

/** @template T = mixed */
final class Y {}

/**
* @param class-string<X<int>> | class-string<Y<int>> $class
* @return class-string<X<int>> | class-string<Y<int>>
*/
function parametrized(string $class): string
{
if ($class !== X::class) {
assertType('class-string<Bug15266\Y<int>>', $class);
}

return $class;
}

/**
* @param class-string<X<*>> | class-string<Y<*>> $class
* @return class-string<X<*>> | class-string<Y<*>>
*/
function star(string $class): string
{
if ($class !== X::class) {
assertType('class-string<Bug15266\Y<*>>', $class);
}

return $class;
}

/**
* @param class-string<X> | class-string<Y> $class
* @return class-string<X> | class-string<Y>
*/
function raw(string $class): string
{
if ($class !== X::class) {
assertType('class-string<Bug15266\Y>', $class);
}

return $class;
}
Loading
Loading