From a07d0f540d769dc568004d4e71987098f0587493 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 15:22:59 +0100 Subject: [PATCH 1/4] Resolve value of BackEnum --- src/Type/ValueOfType.php | 8 ++++++ .../Rules/Methods/CallMethodsRuleTest.php | 10 +++++++ .../PHPStan/Rules/Methods/data/bug-12219.php | 26 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12219.php diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index d02b7d11f7..1731582213 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,6 +51,13 @@ public function isResolvable(): bool protected function getResult(): Type { if ($this->type->isEnum()->yes()) { + if ($this->type instanceof TemplateType) { + $bound = $this->type->getBound(); + if ($bound->equals(new ObjectType('BackedEnum'))) { + return new UnionType([new IntegerType(), new StringType()]); + } + } + $valueTypes = []; foreach ($this->type->getEnumCases() as $enumCase) { $valueType = $enumCase->getBackingValueType(); 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..5c477046fa --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12219.php @@ -0,0 +1,26 @@ += 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); + } +} From 5a008c69eb6e07ee9f250061fdc2318689aafc9f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 15:40:53 +0100 Subject: [PATCH 2/4] Add tests --- src/Type/ValueOfType.php | 10 ++-- tests/PHPStan/Analyser/nsrt/bug-13282.php | 46 +++++++++++++++++++ ...rictComparisonOfDifferentTypesRuleTest.php | 6 +++ .../Rules/Functions/ReturnTypeRuleTest.php | 8 ++++ .../Rules/Functions/data/bug-13638.php | 15 ++++++ 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13282.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13638.php diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index 1731582213..8816bf075b 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -51,11 +51,11 @@ public function isResolvable(): bool protected function getResult(): Type { if ($this->type->isEnum()->yes()) { - if ($this->type instanceof TemplateType) { - $bound = $this->type->getBound(); - if ($bound->equals(new ObjectType('BackedEnum'))) { - return new UnionType([new IntegerType(), new StringType()]); - } + if ( + $this->type instanceof TemplateType + && $this->type->getBound()->equals(new ObjectType('BackedEnum')) + ) { + return new UnionType([new IntegerType(), new StringType()]); } $valueTypes = []; 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/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]; +} From 8832cc51a275078675edc220d4cdc71554f8198f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 16:55:58 +0100 Subject: [PATCH 3/4] Rework --- src/Type/ValueOfType.php | 8 ++++-- .../PHPStan/Rules/Methods/data/bug-12219.php | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index 8816bf075b..b8348dd84c 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -51,15 +51,17 @@ public function isResolvable(): bool protected function getResult(): Type { if ($this->type->isEnum()->yes()) { + $enumCases = $this->type->getEnumCases(); if ( - $this->type instanceof TemplateType - && $this->type->getBound()->equals(new ObjectType('BackedEnum')) + $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/Rules/Methods/data/bug-12219.php b/tests/PHPStan/Rules/Methods/data/bug-12219.php index 5c477046fa..85c52922c4 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-12219.php +++ b/tests/PHPStan/Rules/Methods/data/bug-12219.php @@ -24,3 +24,31 @@ public function bar(string $class, mixed $value): void $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); + } +} From 14a8b06a7ed7031ac8e57d6fc8d1122066b4b280 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 16:58:13 +0100 Subject: [PATCH 4/4] Add test --- tests/PHPStan/Analyser/nsrt/bug-13782.php | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13782.php 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));