From 66227338f7d8f629460e6e80c38de4d5f18aca34 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 15:22:59 +0100 Subject: [PATCH 1/7] 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 0e31dec7eb..2061292532 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; @@ -51,6 +52,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 e959753c4b..42e8c154f5 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3674,6 +3674,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 208666e4026f9585f83922788e6dc54aee8174fa Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 15:40:53 +0100 Subject: [PATCH 2/7] 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 2061292532..68c6f99ca3 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -52,11 +52,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 324b276a086cb52592ec7adf83d6f814448e5a6b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 16:55:58 +0100 Subject: [PATCH 3/7] 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 68c6f99ca3..e2d3f0516c 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -52,15 +52,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 af9cdf0d41074855f43a290dacfa7382518fea4c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 11 Nov 2025 16:58:13 +0100 Subject: [PATCH 4/7] 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)); From 23597bab7d2f6e41f997d71f735ebd31748181fa Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 12:32:40 +0100 Subject: [PATCH 5/7] Add test --- .../Rules/Methods/CallMethodsRuleTest.php | 7 +++++- .../PHPStan/Rules/Methods/data/bug-12219.php | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 42e8c154f5..24deaee554 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3681,7 +3681,12 @@ public function testBug12219(): void $this->checkNullables = true; $this->checkUnionTypes = true; $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/bug-12219.php'], []); + $this->analyse([__DIR__ . '/data/bug-12219.php'], [ + [ + 'Parameter #2 $value of method Bug12219\Bug3::foo() contains unresolvable type.', + 75, + ], + ]); } public function testBug13511(): void diff --git a/tests/PHPStan/Rules/Methods/data/bug-12219.php b/tests/PHPStan/Rules/Methods/data/bug-12219.php index 85c52922c4..5607975491 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-12219.php +++ b/tests/PHPStan/Rules/Methods/data/bug-12219.php @@ -52,3 +52,26 @@ public function bar(string $class, mixed $value): void $this->foo($class, $value); } } + +class Bug3 +{ + /** + * @template T of \UnitEnum + * @param class-string $class + * @param value-of $value + */ + public function foo(string $class, mixed $value): void + { + } + + /** + * @template Q of \UnitEnum + * @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 4c6c6c858536a9a78ae17e5144cb426a1d015b7b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 15:15:14 +0100 Subject: [PATCH 6/7] Try a mutant --- src/Type/ValueOfType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index e2d3f0516c..d9eb5eb828 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -56,7 +56,7 @@ protected function getResult(): Type if ( $enumCases === [] && $this->type instanceof TemplateType - && (new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->yes() + && !(new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->no() ) { return new UnionType([new IntegerType(), new StringType()]); } From 59710ef4a70c68042b036b6ad61dd859d089b0b9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 15:31:12 +0100 Subject: [PATCH 7/7] Revert "Try a mutant" This reverts commit 6cb52a2049cf9499edc8e25942d5250e1ea6823b. --- src/Type/ValueOfType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index d9eb5eb828..e2d3f0516c 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -56,7 +56,7 @@ protected function getResult(): Type if ( $enumCases === [] && $this->type instanceof TemplateType - && !(new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->no() + && (new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->yes() ) { return new UnionType([new IntegerType(), new StringType()]); }