Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
60 changes: 39 additions & 21 deletions src/Rules/Classes/EnumSanityRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand All @@ -31,6 +36,12 @@ final class EnumSanityRule implements Rule
'__invoke' => true,
];

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
)
{
}

public function getNodeType(): string
{
return InClassNode::class;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down
40 changes: 39 additions & 1 deletion tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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')]
Expand Down Expand Up @@ -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,
],
]);
}

}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-13768.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php // lint >= 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;
}
Loading