Skip to content

Commit

Permalink
EnumSanityRule - use scope inside enum
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jun 7, 2023
1 parent c8032a0 commit 04af510
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 50 deletions.
88 changes: 39 additions & 49 deletions src/Rules/Classes/EnumSanityRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
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;
Expand All @@ -18,7 +17,7 @@
use function sprintf;

/**
* @implements Rule<Node\Stmt\Enum_>
* @implements Rule<InClassNode>
*/
class EnumSanityRule implements Rule
{
Expand All @@ -29,33 +28,28 @@ class EnumSanityRule implements Rule
'__invoke' => true,
];

public function __construct(
private ReflectionProvider $reflectionProvider,
)
{
}

public function getNodeType(): string
{
return Node\Stmt\Enum_::class;
return InClassNode::class;
}

/**
* @param Node\Stmt\Enum_ $node
*/
public function processNode(Node $node, Scope $scope): array
{
$errors = [];

if ($node->namespacedName === null) {
throw new ShouldNotHappenException();
$classReflection = $node->getClassReflection();
if (!$classReflection->isEnum()) {
return [];
}

foreach ($node->getMethods() as $methodNode) {
/** @var Node\Stmt\Enum_ $enumNode */
$enumNode = $node->getOriginalNode();

$errors = [];

foreach ($enumNode->getMethods() as $methodNode) {
if ($methodNode->isAbstract()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s contains abstract method %s().',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$methodNode->name->name,
))->line($methodNode->getLine())->nonIgnorable()->build();
}
Expand All @@ -66,17 +60,17 @@ public function processNode(Node $node, Scope $scope): array
if ($lowercasedMethodName === '__construct') {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s contains constructor.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
))->line($methodNode->getLine())->nonIgnorable()->build();
} elseif ($lowercasedMethodName === '__destruct') {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s contains destructor.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
))->line($methodNode->getLine())->nonIgnorable()->build();
} elseif (!array_key_exists($lowercasedMethodName, self::ALLOWED_MAGIC_METHODS)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s contains magic method %s().',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$methodNode->name->name,
))->line($methodNode->getLine())->nonIgnorable()->build();
}
Expand All @@ -85,12 +79,12 @@ public function processNode(Node $node, Scope $scope): array
if ($lowercasedMethodName === 'cases') {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s cannot redeclare native method %s().',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$methodNode->name->name,
))->line($methodNode->getLine())->nonIgnorable()->build();
}

if ($node->scalarType === null) {
if ($enumNode->scalarType === null) {
continue;
}

Expand All @@ -100,45 +94,41 @@ public function processNode(Node $node, Scope $scope): array

$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s cannot redeclare native method %s().',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$methodNode->name->name,
))->line($methodNode->getLine())->nonIgnorable()->build();
}

if (
$node->scalarType !== null
&& $node->scalarType->name !== 'int'
&& $node->scalarType->name !== 'string'
$enumNode->scalarType !== null
&& $enumNode->scalarType->name !== 'int'
&& $enumNode->scalarType->name !== 'string'
) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Backed enum %s can have only "int" or "string" type.',
$node->namespacedName->toString(),
))->line($node->scalarType->getLine())->nonIgnorable()->build();
$classReflection->getDisplayName(),
))->line($enumNode->scalarType->getLine())->nonIgnorable()->build();
}

if ($this->reflectionProvider->hasClass($node->namespacedName->toString())) {
$classReflection = $this->reflectionProvider->getClass($node->namespacedName->toString());

if ($classReflection->implementsInterface(Serializable::class)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s cannot implement the Serializable interface.',
$node->namespacedName->toString(),
))->line($node->getLine())->nonIgnorable()->build();
}
if ($classReflection->implementsInterface(Serializable::class)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s cannot implement the Serializable interface.',
$classReflection->getDisplayName(),
))->line($enumNode->getLine())->nonIgnorable()->build();
}

$enumCases = [];
foreach ($node->stmts as $stmt) {
foreach ($enumNode->stmts as $stmt) {
if (!$stmt instanceof Node\Stmt\EnumCase) {
continue;
}
$caseName = $stmt->name->name;

if (($stmt->expr instanceof Node\Scalar\LNumber || $stmt->expr instanceof Node\Scalar\String_)) {
if ($node->scalarType === null) {
if ($enumNode->scalarType === null) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s is not backed, but case %s has value %s.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$caseName,
$stmt->expr->value,
))
Expand All @@ -157,16 +147,16 @@ public function processNode(Node $node, Scope $scope): array
}
}

if ($node->scalarType === null) {
if ($enumNode->scalarType === null) {
continue;
}

if ($stmt->expr === null) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Enum case %s::%s does not have a value but the enum is backed with the "%s" type.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$caseName,
$node->scalarType->name,
$enumNode->scalarType->name,
))
->identifier('enum.missingCase')
->line($stmt->getLine())
Expand All @@ -176,14 +166,14 @@ public function processNode(Node $node, Scope $scope): array
}

$exprType = $scope->getType($stmt->expr);
$scalarType = $node->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType();
$scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType();
if ($scalarType->isSuperTypeOf($exprType)->yes()) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'Enum case %s::%s value %s does not match the "%s" type.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$caseName,
$exprType->describe(VerbosityLevel::value()),
$scalarType->describe(VerbosityLevel::typeOnly()),
Expand All @@ -201,12 +191,12 @@ public function processNode(Node $node, Scope $scope): array

$errors[] = RuleErrorBuilder::message(sprintf(
'Enum %s has duplicate value %s for cases %s.',
$node->namespacedName->toString(),
$classReflection->getDisplayName(),
$caseValue,
implode(', ', $caseNames),
))
->identifier('enum.duplicateValue')
->line($node->getLine())
->line($enumNode->getLine())
->nonIgnorable()
->build();
}
Expand Down
16 changes: 15 additions & 1 deletion tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EnumSanityRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new EnumSanityRule($this->createReflectionProvider());
return new EnumSanityRule();
}

public function testRule(): void
Expand Down Expand Up @@ -112,4 +112,18 @@ public function testRule(): void
$this->analyse([__DIR__ . '/data/enum-sanity.php'], $expected);
}

public function testBug9402(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0');
}

$this->analyse([__DIR__ . '/data/bug-9402.php'], [
[
'Enum case Bug9402\Foo::Two value \'foo\' does not match the "int" type.',
13,
],
]);
}

}
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-9402.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php // lint >= 8.1

namespace Bug9402;

enum Foo: int
{

private const MY_CONST = 1;
private const MY_CONST_STRING = 'foo';

case Zero = 0;
case One = self::MY_CONST;
case Two = self::MY_CONST_STRING;

}

0 comments on commit 04af510

Please sign in to comment.