diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 5994a945569..6ed6cd7535c 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -32,7 +32,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; @@ -41,7 +40,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; @@ -164,24 +162,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(); diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index 7fd22cc8ecb..b0024808092 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -22,6 +22,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; @@ -109,6 +110,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>` and of `class-string>` just like it is of `class-string`. + $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) { @@ -121,21 +155,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()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-15266.php b/tests/PHPStan/Analyser/nsrt/bug-15266.php new file mode 100644 index 00000000000..d61bcea0317 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15266.php @@ -0,0 +1,127 @@ +> | class-string> $class + * @return class-string> | class-string> + */ +function parametrized(string $class): string +{ + if ($class !== X::class) { + assertType('class-string>', $class); + } + + return $class; +} + +/** + * @param class-string> | class-string> $class + * @return class-string> | class-string> + */ +function star(string $class): string +{ + if ($class !== X::class) { + assertType('class-string>', $class); + } + + return $class; +} + +/** + * @param class-string | class-string $class + * @return class-string | class-string + */ +function raw(string $class): string +{ + if ($class !== X::class) { + assertType('class-string', $class); + } + + return $class; +} + +/** + * @param class-string|Y> $class + */ +function genericUnionInsideClassString(string $class): void +{ + if ($class !== X::class) { + assertType('class-string>', $class); + } + + echo $class; +} + +/** + * @param class-string>|class-string> $class + */ +function identical(string $class): void +{ + if ($class === X::class) { + assertType('\'Bug15266\\\\X\'', $class); + } else { + assertType('class-string>', $class); + } +} + +/** + * @param class-string>|class-string> $class + */ +function switchOnClassString(string $class): void +{ + switch ($class) { + case X::class: + assertType('\'Bug15266\\\\X\'', $class); + break; + default: + assertType('class-string>', $class); + } +} + +/** + * @param class-string>|class-string> $class + */ +function inArrayOnClassString(string $class): void +{ + if (in_array($class, [X::class], true)) { + assertType('\'Bug15266\\\\X\'', $class); + } +} + +/** + * @param class-string>|class-string> $class + */ +function offsetOnClassString(string $class): void +{ + $map = [X::class => 1, Y::class => 2]; + assertType('1|2', $map[$class]); +} + +/** + * @param class-string> $class + */ +function unionWithConstantClassString(string $class, bool $bool): void +{ + assertType('class-string>', $bool ? $class : X::class); +} + +/** + * @param class-string>&literal-string $class + */ +function classStringWithAccessoryType(string $class): void +{ + if ($class !== X::class) { + assertType('*NEVER*', $class); + } + + echo $class; +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 3171da7f961..72fbc8c253e 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -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; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-15266.php b/tests/PHPStan/Rules/Comparison/data/bug-15266.php new file mode 100644 index 00000000000..771ab86d617 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-15266.php @@ -0,0 +1,42 @@ +>|class-string> $class + */ +function parametrized(string $class): string +{ + return match ($class) { + X::class => 'x', + Y::class => 'y', + }; +} + +/** + * @param class-string>|class-string> $class + */ +function star(string $class): string +{ + return match ($class) { + X::class => 'x', + Y::class => 'y', + }; +} + +/** + * @param class-string|class-string $class + */ +function raw(string $class): string +{ + return match ($class) { + X::class => 'x', + Y::class => 'y', + }; +}