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..fc454bc696 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -6,8 +6,11 @@ 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; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\VerbosityLevel; @@ -16,6 +19,8 @@ use function count; use function implode; use function in_array; +use function is_int; +use function is_string; use function sprintf; /** @@ -31,6 +36,12 @@ final class EnumSanityRule implements Rule '__invoke' => true, ]; + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function getNodeType(): string { return InClassNode::class; @@ -145,27 +156,20 @@ 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 %s.', + $classReflection->getDisplayName(), + $caseName, + $this->initializerExprTypeResolver->getType( + $stmt->expr, + InitializerExprContext::fromScope($scope), + )->describe(VerbosityLevel::value()), + )) + ->identifier('enum.caseWithValue') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); } if ($enumNode->scalarType === null) { @@ -189,6 +193,20 @@ public function processNode(Node $node, Scope $scope): array $exprType = $scope->getType($stmt->expr); $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; } diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 22bb10a00e..4ab2c6b842 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')] @@ -141,4 +144,39 @@ public function testBug11592(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13768(): void + { + $this->analyse([__DIR__ . '/data/bug-13768.php'], [ + [ + '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 2.5.', + 8, + ], + [ + 'Enum Bug13768\Order is not backed, but case C has value 3.', + 9, + ], + [ + 'Enum Bug13768\Order is not backed, but case D has value \'3\'.', + 10, + ], + [ + 'Enum Bug13768\Order is not backed, but case E has value false.', + 11, + ], + [ + 'Enum Bug13768\Order is not backed, but case F has value 1.', + 12, + ], + [ + 'Enum Bug13768\Backed has duplicate value 1 for cases One, Two.', + 20, + ], + ]); + } + } 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..6a7ffda48b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-13768.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug13768; + +enum Order { + case U; + case A = 1.5; + case B = 2.5; + case C = 3; + case D = '3'; + case E = false; + case F = Foo::A; +} + +class Foo +{ + public const A = 1; +} + +enum Backed: int { + case One = Foo::A; + case Two = Foo::A; +}