Skip to content
Closed
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 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 = '3ff623f';
public const EXPECTED_EXTENSION_VERSION = '740aaeb';

private static bool $active = false;

Expand Down
19 changes: 1 addition & 18 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\InstanceofDeprecated;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
Expand All @@ -42,7 +41,6 @@
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\StringType;
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -166,24 +164,9 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
if ($genericType instanceof MixedType) {
return IsSuperTypeOfResult::createMaybe();
}
if ($genericType instanceof StaticType) {
$genericType = $genericType->getStaticObjectType();
}

// We are transforming constant class-string to ObjectType. But we need to filter out
// an uncertainty originating in possible ObjectType's class subtypes.
$objectType = $this->getObjectType();

// Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
// uncertainty into account.
if ($genericType instanceof TemplateType) {
$isSuperType = $genericType->getBound()->isSuperTypeOf($objectType);
} else {
$isSuperType = $genericType->isSuperTypeOf($objectType);
}

// Explicitly handle the uncertainty for Yes & Maybe.
if ($isSuperType->yes()) {
if (GenericClassStringType::isValueOfGenericType($genericType, $this->value)->yes()) {
return IsSuperTypeOfResult::createMaybe();
}
return IsSuperTypeOfResult::createNo();
Expand Down
50 changes: 35 additions & 15 deletions src/Type/Generic/GenericClassStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function count;
Expand Down Expand Up @@ -111,6 +112,39 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult
return $this->type->accepts($objectType, $strictTypes);
}

/**
* Whether a class named $className can be the value behind `class-string<$genericType>`.
*
* @internal
*/
public static function isValueOfGenericType(Type $genericType, string $className): IsSuperTypeOfResult
{
if ($genericType instanceof StaticType) {
$genericType = $genericType->getStaticObjectType();
}

// Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
// uncertainty into account.
if ($genericType instanceof TemplateType) {
$genericType = $genericType->getBound();
}

// A class-string carries a class name and never its type arguments, so the type
// arguments must not take part in the comparison: `X::class` is a value of
// `class-string<X<int>>` and of `class-string<X<*>>` just like it is of `class-string<X>`.
$genericType = TypeTraverser::map($genericType, static function (Type $type, callable $traverse): Type {
if ($type instanceof GenericObjectType) {
return new ObjectType($type->getClassName(), $type->getSubtractedType());
}

return $traverse($type);
});

// We are transforming constant class-string to ObjectType. But we need to filter out
// an uncertainty originating in possible ObjectType's class subtypes.
return $genericType->isSuperTypeOf(new ObjectType($className));
}

public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
{
if ($type instanceof CompoundType) {
Expand All @@ -123,21 +157,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
return IsSuperTypeOfResult::createYes();
}

if ($genericType instanceof StaticType) {
$genericType = $genericType->getStaticObjectType();
}

// We are transforming constant class-string to ObjectType. But we need to filter out
// an uncertainty originating in possible ObjectType's class subtypes.
$objectType = new ObjectType($type->getValue());

// Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
// uncertainty into account.
if ($genericType instanceof TemplateType) {
$isSuperType = $genericType->getBound()->isSuperTypeOf($objectType);
} else {
$isSuperType = $genericType->isSuperTypeOf($objectType);
}
$isSuperType = self::isValueOfGenericType($genericType, $type->getValue());

if (!$type->isClassString()->yes()) {
$isSuperType = $isSuperType->and(IsSuperTypeOfResult::createMaybe());
Expand Down
127 changes: 127 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15266.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?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;
}

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

echo $class;
}

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

/**
* @param class-string<X<int>>|class-string<Y<int>> $class
*/
function switchOnClassString(string $class): void
{
switch ($class) {
case X::class:
assertType('\'Bug15266\\\\X\'', $class);
break;
default:
assertType('class-string<Bug15266\Y<int>>', $class);
}
}

/**
* @param class-string<X<int>>|class-string<Y<int>> $class
*/
function inArrayOnClassString(string $class): void
{
if (in_array($class, [X::class], true)) {
assertType('\'Bug15266\\\\X\'', $class);
}
}

/**
* @param class-string<X<int>>|class-string<Y<int>> $class
*/
function offsetOnClassString(string $class): void
{
$map = [X::class => 1, Y::class => 2];
assertType('1|2', $map[$class]);
}

/**
* @param class-string<X<int>> $class
*/
function unionWithConstantClassString(string $class, bool $bool): void
{
assertType('class-string<Bug15266\X<int>>', $bool ? $class : X::class);
}

/**
* @param class-string<X<int>>&literal-string $class
*/
function classStringWithAccessoryType(string $class): void
{
if ($class !== X::class) {
assertType('*NEVER*', $class);
}

echo $class;
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ public function testInTrait(): void
]);
}

public function testBug15266(): void
{
$this->analyse([__DIR__ . '/data/bug-15266.php'], []);
}

public function testMatchArmComparisonNotSuppressedByImpossibleCheck(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,11 @@ public function testBug14966(): void
$this->analyse([__DIR__ . '/data/bug-14966.php'], []);
}

public function testBug10498(): void
{
$this->analyse([__DIR__ . '/data/bug-10498.php'], []);
}

public function testBug14847(): void
{
$this->analyse([__DIR__ . '/data/bug-14847.php'], [
Expand Down
34 changes: 34 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-10498.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Bug10498;

use const PHP_EOL;
use function printf;

/**
* @template T of object
*/
abstract class A
{

public static function foo(): void
{
if (static::class === B::class) {
printf("I'm a %s" . PHP_EOL, B::class);
}
}

}

/**
* @extends A<C>
*/
class B extends A
{

}

class C
{

}
42 changes: 42 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-15266.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug15266Match;

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

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

/**
* @param class-string<X<int>>|class-string<Y<int>> $class
*/
function parametrized(string $class): string
{
return match ($class) {
X::class => 'x',
Y::class => 'y',
};
}

/**
* @param class-string<X<*>>|class-string<Y<*>> $class
*/
function star(string $class): string
{
return match ($class) {
X::class => 'x',
Y::class => 'y',
};
}

/**
* @param class-string<X>|class-string<Y> $class
*/
function raw(string $class): string
{
return match ($class) {
X::class => 'x',
Y::class => 'y',
};
}
32 changes: 5 additions & 27 deletions turbo-ext/src/ConstantStringType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,8 @@ class ConstantStringType
return exported;
}

/* maybe/no for a GenericClassStringType by whether its generic type (a
* StaticType's object type, a TemplateType's bound; mixed is maybe) is
* a supertype of this value's ObjectType; maybe/no for a
/* maybe/no for a GenericClassStringType by whether this value can be a
* value of its generic type (mixed is maybe); maybe/no for a
* ClassStringType by whether this is a class-string; yes/no against
* another ConstantStringType's value; maybe for any other StringType;
* the CompoundType callback; no otherwise; UNDEF = pending exception */
Expand All @@ -269,31 +268,10 @@ class ConstantStringType
zv::Val genericType = pt_type_call(Z_OBJ_P(type), PT_LC("getgenerictype"), 0, NULL);
if (UNEXPECTED(genericType.isUndef())) return zv::Val();
if (zv::Ref(genericType.raw()).instanceOf(pt_ce_mixed_type)) return pt_type_is_super_type_of_result(PT_TRI_MAYBE);
/* $genericType instanceof StaticType — the shadowing class */
bool isStatic = zv::Ref(genericType.raw()).instanceOf(pt_ce_static_type);
if (isStatic) {
genericType = pt_type_call(Z_OBJ_P(genericType.raw()), PT_LC("getstaticobjecttype"), 0, NULL);
if (UNEXPECTED(genericType.isUndef())) return zv::Val();
}

/* We are transforming constant class-string to ObjectType. But
* we need to filter out an uncertainty originating in possible
* ObjectType's class subtypes. */
zval *objectType = getObjectType();
if (UNEXPECTED(objectType == NULL)) return zv::Val();

/* Do not use TemplateType's isSuperTypeOf handling directly
* because it takes ObjectType uncertainty into account. */
bool isTemplate;
if (UNEXPECTED(!pt_type_instanceof(genericType.raw(), PT_CLASS_TEMPLATE_TYPE, isTemplate))) return zv::Val();
zv::Val isSuperType;
if (isTemplate) {
zv::Val bound = pt_type_call(Z_OBJ_P(genericType.raw()), PT_LC("getbound"), 0, NULL);
if (UNEXPECTED(bound.isUndef())) return zv::Val();
isSuperType = pt_type_op(Z_OBJ_P(bound.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, objectType);
} else {
isSuperType = pt_type_op(Z_OBJ_P(genericType.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, objectType);
}
zend_string *v = value();
if (UNEXPECTED(v == NULL)) return zv::Val();
zv::Val isSuperType = pt_generic_class_string_is_value_of_generic_type(genericType.raw(), v);
if (UNEXPECTED(isSuperType.isUndef())) return zv::Val();
zend_long verdict = pt_type_result_trinary(isSuperType.raw());
if (UNEXPECTED(verdict < 0)) return zv::Val();
Expand Down
Loading
Loading