diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index d02b7d11f7..b8348dd84c 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -5,6 +5,7 @@ use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -50,8 +51,17 @@ public function isResolvable(): bool protected function getResult(): Type { if ($this->type->isEnum()->yes()) { + $enumCases = $this->type->getEnumCases(); + if ( + $enumCases === [] + && $this->type instanceof TemplateType + && (new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->yes() + ) { + return new UnionType([new IntegerType(), new StringType()]); + } + $valueTypes = []; - foreach ($this->type->getEnumCases() as $enumCase) { + foreach ($enumCases as $enumCase) { $valueType = $enumCase->getBackingValueType(); if ($valueType === null) { continue; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13282.php b/tests/PHPStan/Analyser/nsrt/bug-13282.php new file mode 100644 index 0000000000..c8f06ce6df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13282.php @@ -0,0 +1,46 @@ += 8.1 + +namespace Bug13283; + +use function PHPStan\Testing\assertType; + +enum Test: string +{ + case NAME = 'name'; + case VALUE = 'value'; +} + +/** + * @template T of \BackedEnum + * @param class-string $enum + * @phpstan-assert null|value-of $value + */ +function assertValue(mixed $value, string $enum): void +{ + if (null === $value) { + return; + } + + if (! is_int($value) && ! is_string($value)) { + throw new \Exception(); + } + + if (null === $enum::tryFrom($value)) { + throw new \Exception(); + } +} + +function getFromRequest(): mixed +{ + return 'name'; +} + +$v = getFromRequest(); + +assertType('mixed', $v); + +assertValue($v, Test::class); + +assertType("'name'|'value'|null", $v); + +$a = null !== $v ? Test::from($v) : null; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13782.php b/tests/PHPStan/Analyser/nsrt/bug-13782.php new file mode 100644 index 0000000000..974ee51f11 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13782.php @@ -0,0 +1,38 @@ += 8.1 + +namespace Bug13782; + +use BackedEnum; +use function PHPStan\Testing\assertType; + +enum IntEnum : int +{ + case A = 1; + case B = 2; +} + +class EnumMethods +{ + /** + * @template TEnum of BackedEnum + * @param TEnum $enum + * @return value-of + */ + public static function getValue(BackedEnum $enum): int|string + { + return $enum->value; + } + + /** + * @template TEnum of BackedEnum + * @param TEnum|null $enum + * @return ($enum is TEnum ? value-of : null) + */ + public static function getNullableValue(?BackedEnum $enum): int|string|null + { + return $enum === null ? null : $enum->value; + } +} + +assertType("2", EnumMethods::getValue(IntEnum::B)); +assertType("2", EnumMethods::getNullableValue(IntEnum::B)); diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 9971f74981..965ed1e348 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1034,6 +1034,12 @@ public function testBug13208(): void $this->analyse([__DIR__ . '/data/bug-13208.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug13282(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13282.php'], []); + } + public function testBug11609(): void { $this->analyse([__DIR__ . '/data/bug-11609.php'], [ diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index c1fac8e311..169aff175f 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -357,6 +357,14 @@ public function testBug12274(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13638(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13638.php'], []); + } + public function testBug13484(): void { $this->checkExplicitMixed = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-13638.php b/tests/PHPStan/Rules/Functions/data/bug-13638.php new file mode 100644 index 0000000000..1951a78cc8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13638.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug13638; + +use BackedEnum; + +/** + * @template T of BackedEnum + * @param ?value-of $a + * @return ($a is null ? list> : list>) + */ +function test1(int | string | null $a): array +{ + return [$a]; +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 0dbebf0da3..7eeeabb6fd 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3666,6 +3666,16 @@ public function testBug3396(): void $this->analyse([__DIR__ . '/data/bug-3396.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug12219(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12219.php'], []); + } + public function testBug13511(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-12219.php b/tests/PHPStan/Rules/Methods/data/bug-12219.php new file mode 100644 index 0000000000..85c52922c4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12219.php @@ -0,0 +1,54 @@ += 8.1 + +namespace Bug12219; + +class Bug +{ + /** + * @template T of \BackedEnum + * @param class-string $class + * @param value-of $value + */ + public function foo(string $class, mixed $value): void + { + } + + /** + * @template Q of \BackedEnum + * @param class-string $class + * @param value-of $value + */ + public function bar(string $class, mixed $value): void + { + // Parameter #2 $value of static method Bug::foo() contains unresolvable type. + $this->foo($class, $value); + } +} + +interface SuperBackedEnum extends \BackedEnum +{ + public function customMethod(): void; +} + +class Bug2 +{ + /** + * @template T of SuperBackedEnum + * @param class-string $class + * @param value-of $value + */ + public function foo(string $class, mixed $value): void + { + } + + /** + * @template Q of SuperBackedEnum + * @param class-string $class + * @param value-of $value + */ + public function bar(string $class, mixed $value): void + { + // Parameter #2 $value of static method Bug::foo() contains unresolvable type. + $this->foo($class, $value); + } +}