From bb0f72904d8c646e35253b13ca19535c0c4861ae Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 10 Nov 2025 09:41:11 +0100 Subject: [PATCH 1/7] Improve EnumSanityRule --- Makefile | 1 + src/Rules/Classes/EnumSanityRule.php | 41 +++++++++---------- .../Rules/Classes/EnumSanityRuleTest.php | 26 +++++++----- .../PHPStan/Rules/Classes/data/bug-13768.php | 21 ++++++++++ 4 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-13768.php diff --git a/Makefile b/Makefile index f5476d6067..0d4adf8b5f 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ lint: --exclude tests/PHPStan/Rules/Names/data \ --exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \ --exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-13768.php \ --exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \ --exclude tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php \ --exclude tests/PHPStan/Rules/Classes/data/enum-sanity.php \ diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index fc5d56ba8a..309863c64e 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -145,27 +145,16 @@ public function processNode(Node $node, Scope $scope): array } $caseName = $stmt->name->name; - if ($stmt->expr instanceof Node\Scalar\Int_ || $stmt->expr instanceof Node\Scalar\String_) { - if ($enumNode->scalarType === null) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s is not backed, but case %s has value %s.', - $classReflection->getDisplayName(), - $caseName, - $stmt->expr->value, - )) - ->identifier('enum.caseWithValue') - ->line($stmt->getStartLine()) - ->nonIgnorable() - ->build(); - } else { - $caseValue = $stmt->expr->value; - - if (!isset($enumCases[$caseValue])) { - $enumCases[$caseValue] = []; - } - - $enumCases[$caseValue][] = $caseName; - } + if ($enumNode->scalarType === null && $stmt->expr !== null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s is not backed, but case %s has value.', + $classReflection->getDisplayName(), + $caseName, + )) + ->identifier('enum.caseWithValue') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); } if ($enumNode->scalarType === null) { @@ -187,6 +176,16 @@ public function processNode(Node $node, Scope $scope): array } $exprType = $scope->getType($stmt->expr); + $constantValues = $exprType->getConstantScalarValues(); + if (count($constantValues) === 1) { + $caseValue = $constantValues[0]; + if (!isset($enumCases[$caseValue])) { + $enumCases[$caseValue] = []; + } + + $enumCases[$caseValue][] = $caseName; + } + $scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType(); if ($scalarType->isSuperTypeOf($exprType)->yes()) { continue; diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 22bb10a00e..6b6f7c2f02 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -95,7 +95,7 @@ public function testRule(): void 110, ], [ - 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value 1.', + 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value.', 114, ], [ @@ -119,24 +119,28 @@ public function testBug9402(): void } #[RequiresPhp('>= 8.1')] - public function testBug11592(): void + public function testBug13768(): void { - $this->analyse([__DIR__ . '/data/bug-11592.php'], [ + $this->analyse([__DIR__ . '/data/bug-13768.php'], [ [ - 'Enum Bug11592\Test2 cannot redeclare native method cases().', - 22, + 'Enum Bug13768\Order is not backed, but case A has value.', + 7, ], [ - 'Enum Bug11592\BackedTest2 cannot redeclare native method cases().', - 37, + 'Enum Bug13768\Order is not backed, but case B has value.', + 8, ], [ - 'Enum Bug11592\BackedTest2 cannot redeclare native method from().', - 39, + 'Enum Bug13768\Order is not backed, but case C has value.', + 9, + ], + [ + 'Enum Bug13768\Order is not backed, but case D has value.', + 10, ], [ - 'Enum Bug11592\BackedTest2 cannot redeclare native method tryFrom().', - 41, + 'Enum Bug13768\Backed has duplicate value 1 for cases One, Two.', + 18, ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/bug-13768.php b/tests/PHPStan/Rules/Classes/data/bug-13768.php new file mode 100644 index 0000000000..7624cc59ca --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-13768.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug13768; + +enum Order { + case U; + case A = 1.5; + case B = 2.5; + case C = 3; + case D = '3'; +} + +class Foo +{ + public const A = 1; +} + +enum Backed: int { + case One = Foo::A; + case Two = Foo::A; +} From b2166bd2056eea1e588a9d80810cf592858e90ad Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 10 Nov 2025 09:57:09 +0100 Subject: [PATCH 2/7] Rework --- src/Rules/Classes/EnumSanityRule.php | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index 309863c64e..3d1e42a00e 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -8,6 +8,7 @@ use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\VerbosityLevel; @@ -16,6 +17,8 @@ use function count; use function implode; use function in_array; +use function is_int; +use function is_string; use function sprintf; /** @@ -176,18 +179,22 @@ public function processNode(Node $node, Scope $scope): array } $exprType = $scope->getType($stmt->expr); - $constantValues = $exprType->getConstantScalarValues(); - if (count($constantValues) === 1) { - $caseValue = $constantValues[0]; - if (!isset($enumCases[$caseValue])) { - $enumCases[$caseValue] = []; - } - - $enumCases[$caseValue][] = $caseName; - } - $scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType(); if ($scalarType->isSuperTypeOf($exprType)->yes()) { + $constantValues = $exprType->getConstantScalarValues(); + if (count($constantValues) === 1) { + $caseValue = $constantValues[0]; + if (!is_string($caseValue) && !is_int($caseValue)) { + throw new ShouldNotHappenException(); + } + + if (!isset($enumCases[$caseValue])) { + $enumCases[$caseValue] = []; + } + + $enumCases[$caseValue][] = $caseName; + } + continue; } From 24ef1dccd9649ef2391bb8440f70e8522efde4f8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 09:08:39 +0100 Subject: [PATCH 3/7] Fix --- .../Rules/Classes/EnumSanityRuleTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 6b6f7c2f02..b2d8a54eae 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -118,6 +118,29 @@ public function testBug9402(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug11592(): void + { + $this->analyse([__DIR__ . '/data/bug-11592.php'], [ + [ + 'Enum Bug11592\Test2 cannot redeclare native method cases().', + 22, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method cases().', + 37, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method from().', + 39, + ], + [ + 'Enum Bug11592\BackedTest2 cannot redeclare native method tryFrom().', + 41, + ], + ]); + } + #[RequiresPhp('>= 8.1')] public function testBug13768(): void { From 8e72e2bcfdc8ace4ef8198f362a179fee7cd6b60 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 09:44:44 +0100 Subject: [PATCH 4/7] Rework --- src/Rules/Classes/EnumSanityRule.php | 12 ++++++++++- .../Rules/Classes/EnumSanityRuleTest.php | 20 +++++++++++++------ .../PHPStan/Rules/Classes/data/bug-13768.php | 2 ++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index 3d1e42a00e..e1670e1d58 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -149,10 +149,20 @@ public function processNode(Node $node, Scope $scope): array $caseName = $stmt->name->name; if ($enumNode->scalarType === null && $stmt->expr !== null) { + $value = null; + if ( + $stmt->expr instanceof Node\Scalar\Int_ + || $stmt->expr instanceof Node\Scalar\String_ + || $stmt->expr instanceof Node\Scalar\Float_ + ) { + $value = $stmt->expr->value; + } + $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s is not backed, but case %s has value.', + 'Enum %s is not backed, but case %s has value%s.', $classReflection->getDisplayName(), $caseName, + $value !== null ? ' '.$value : '', )) ->identifier('enum.caseWithValue') ->line($stmt->getStartLine()) diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index b2d8a54eae..95e7b28f81 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -95,7 +95,7 @@ public function testRule(): void 110, ], [ - 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value.', + 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value 1.', 114, ], [ @@ -146,24 +146,32 @@ public function testBug13768(): void { $this->analyse([__DIR__ . '/data/bug-13768.php'], [ [ - 'Enum Bug13768\Order is not backed, but case A has value.', + 'Enum Bug13768\Order is not backed, but case A has value 1.5.', 7, ], [ - 'Enum Bug13768\Order is not backed, but case B has value.', + 'Enum Bug13768\Order is not backed, but case B has value 2.5.', 8, ], [ - 'Enum Bug13768\Order is not backed, but case C has value.', + 'Enum Bug13768\Order is not backed, but case C has value 3.', 9, ], [ - 'Enum Bug13768\Order is not backed, but case D has value.', + 'Enum Bug13768\Order is not backed, but case D has value 3.', 10, ], + [ + 'Enum Bug13768\Order is not backed, but case E has value.', + 11, + ], + [ + 'Enum Bug13768\Order is not backed, but case F has value.', + 12, + ], [ 'Enum Bug13768\Backed has duplicate value 1 for cases One, Two.', - 18, + 20, ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/bug-13768.php b/tests/PHPStan/Rules/Classes/data/bug-13768.php index 7624cc59ca..6a7ffda48b 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-13768.php +++ b/tests/PHPStan/Rules/Classes/data/bug-13768.php @@ -8,6 +8,8 @@ enum Order { case B = 2.5; case C = 3; case D = '3'; + case E = false; + case F = Foo::A; } class Foo From dbe3ed422af97232b0707c3001b49bfd90aa25fa Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 09:47:15 +0100 Subject: [PATCH 5/7] Fix cs --- src/Rules/Classes/EnumSanityRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index e1670e1d58..06813085dc 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -162,7 +162,7 @@ public function processNode(Node $node, Scope $scope): array 'Enum %s is not backed, but case %s has value%s.', $classReflection->getDisplayName(), $caseName, - $value !== null ? ' '.$value : '', + $value !== null ? ' ' . $value : '', )) ->identifier('enum.caseWithValue') ->line($stmt->getStartLine()) From 1713ab6a7af24360865363af3f52f1c70f796122 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 09:56:43 +0100 Subject: [PATCH 6/7] Improve --- src/Rules/Classes/EnumSanityRule.php | 24 ++++++++++--------- .../Rules/Classes/EnumSanityRuleTest.php | 11 +++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index 06813085dc..6ba66d015b 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -6,6 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -34,6 +36,12 @@ final class EnumSanityRule implements Rule '__invoke' => true, ]; + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function getNodeType(): string { return InClassNode::class; @@ -149,20 +157,14 @@ public function processNode(Node $node, Scope $scope): array $caseName = $stmt->name->name; if ($enumNode->scalarType === null && $stmt->expr !== null) { - $value = null; - if ( - $stmt->expr instanceof Node\Scalar\Int_ - || $stmt->expr instanceof Node\Scalar\String_ - || $stmt->expr instanceof Node\Scalar\Float_ - ) { - $value = $stmt->expr->value; - } - $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s is not backed, but case %s has value%s.', + 'Enum %s is not backed, but case %s has value %s.', $classReflection->getDisplayName(), $caseName, - $value !== null ? ' ' . $value : '', + $this->initializerExprTypeResolver->getType( + $stmt->expr, + InitializerExprContext::fromScope($scope) + )->describe(VerbosityLevel::value()), )) ->identifier('enum.caseWithValue') ->line($stmt->getStartLine()) diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 95e7b28f81..b13138d03e 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -14,7 +15,9 @@ class EnumSanityRuleTest extends RuleTestCase protected function getRule(): Rule { - return new EnumSanityRule(); + return new EnumSanityRule( + self::getContainer()->getByType(InitializerExprTypeResolver::class) + ); } #[RequiresPhp('>= 8.1')] @@ -158,15 +161,15 @@ public function testBug13768(): void 9, ], [ - 'Enum Bug13768\Order is not backed, but case D has value 3.', + 'Enum Bug13768\Order is not backed, but case D has value \'3\'.', 10, ], [ - 'Enum Bug13768\Order is not backed, but case E has value.', + 'Enum Bug13768\Order is not backed, but case E has value false.', 11, ], [ - 'Enum Bug13768\Order is not backed, but case F has value.', + 'Enum Bug13768\Order is not backed, but case F has value 1.', 12, ], [ From d37fc05a8470455ca0f8c127d9126ff2d963fec2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 10:51:13 +0100 Subject: [PATCH 7/7] Fix cs --- src/Rules/Classes/EnumSanityRule.php | 2 +- tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index 6ba66d015b..fc454bc696 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -163,7 +163,7 @@ public function processNode(Node $node, Scope $scope): array $caseName, $this->initializerExprTypeResolver->getType( $stmt->expr, - InitializerExprContext::fromScope($scope) + InitializerExprContext::fromScope($scope), )->describe(VerbosityLevel::value()), )) ->identifier('enum.caseWithValue') diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index b13138d03e..4ab2c6b842 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -16,7 +16,7 @@ class EnumSanityRuleTest extends RuleTestCase protected function getRule(): Rule { return new EnumSanityRule( - self::getContainer()->getByType(InitializerExprTypeResolver::class) + self::getContainer()->getByType(InitializerExprTypeResolver::class), ); }