Skip to content

Commit

Permalink
BackedEnumGenericsRule (#20)
Browse files Browse the repository at this point in the history
* BackedEnumGenericsRule

* readme

* fix php 7.4

* Proper fix by conditional ignore
  • Loading branch information
janedbal committed Aug 4, 2022
1 parent e6e64fc commit fc3baef
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 0 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ class User {
}
```

### BackedEnumGenericsRule
- Ensures that every BackedEnum child defines generic type
- This makes sense only when BackedEnum was hacked to be generic as described in [this article](https://rnd.shipmonk.com/hacking-generics-into-backedenum-in-php-81/)
```neon
rules:
- ShipMonk\PHPStan\Rule\BackedEnumGenericsRule
parameters:
stubFiles:
- BackedEnum.php.stub # see article or BackedEnumGenericsRuleTest
ignoreErrors:
- '#^Enum .*? has @implements tag, but does not implement any interface.$#'
```
```php
enum MyEnum: string { // missing @implements tag
case MyCase = 'case1';
}
```

### ForbidEnumInFunctionArgumentsRule
- Guards passing native enums to native functions where it fails / produces warning or does unexpected behaviour
- Most of the array manipulation functions does not work with enums as they do implicit __toString conversion inside, but that is not possible to do with enums
Expand Down
10 changes: 10 additions & 0 deletions phpstan.ignores.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

$config = [];

// https://github.com/phpstan/phpstan/issues/6290
if (PHP_VERSION_ID < 80_000) {
$config['parameters']['ignoreErrors'][] = '~Class BackedEnum not found.~';
}

return $config;
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ includes:
- ./vendor/phpstan/phpstan-strict-rules/rules.neon
- ./vendor/phpstan/phpstan-phpunit/extension.neon
- ./vendor/phpstan/phpstan-phpunit/rules.neon
- ./phpstan.ignores.php

parameters:
paths:
Expand Down
85 changes: 85 additions & 0 deletions src/Rule/BackedEnumGenericsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Rule;

use BackedEnum;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Type\VerbosityLevel;

/**
* @implements Rule<InClassNode>
*/
class BackedEnumGenericsRule implements Rule
{

public function getNodeType(): string
{
return InClassNode::class;
}

/**
* @param InClassNode $node
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$backedEnumType = $classReflection->getBackedEnumType();

if ($backedEnumType === null) {
return [];
}

if (!$this->isGenericBackedEnum($classReflection)) {
return [];
}

$expectedType = $backedEnumType->describe(VerbosityLevel::typeOnly());
$expectedTag = BackedEnum::class . "<$expectedType>";

foreach ($classReflection->getAncestors() as $interface) {
if ($this->hasGenericsTag($interface, $expectedTag)) {
return [];
}
}

return ["Class {$classReflection->getName()} extends generic BackedEnum, but does not specify its type. Use @implements $expectedTag"];
}

private function hasGenericsTag(ClassReflection $classReflection, string $expectedTag): bool
{
if ($classReflection->isBackedEnum()) {
$tags = $classReflection->getImplementsTags();
} elseif ($classReflection->isInterface()) {
$tags = $classReflection->getExtendsTags();
} else {
$tags = [];
}

foreach ($tags as $tag) {
$implementsTagType = $tag->getType();

if ($implementsTagType->describe(VerbosityLevel::typeOnly()) === $expectedTag) {
return true;
}
}

return false;
}

private function isGenericBackedEnum(ClassReflection $classReflection): bool
{
foreach ($classReflection->getAncestors() as $ancestor) {
if ($ancestor->getName() === BackedEnum::class && $ancestor->isGeneric()) {
return true;
}
}

return false;
}

}
41 changes: 41 additions & 0 deletions tests/Rule/BackedEnumGenericsRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Rule;

use PHPStan\Rules\Rule;
use ShipMonk\PHPStan\RuleTestCase;
use function array_merge;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<BackedEnumGenericsRule>
*/
class BackedEnumGenericsRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new BackedEnumGenericsRule();
}

/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return array_merge(
parent::getAdditionalConfigFiles(),
[__DIR__ . '/data/BackedEnumGenericsRule/stub.neon'],
);
}

public function testClass(): void
{
if (PHP_VERSION_ID < 80_100) {
self::markTestSkipped('Requires PHP 8.1');
}

$this->analyseFile(__DIR__ . '/data/BackedEnumGenericsRule/code.php');
}

}
14 changes: 14 additions & 0 deletions tests/Rule/data/BackedEnumGenericsRule/BackedEnum.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @template T of int|string
*/
interface BackedEnum
{

/**
* @var T
*/
public $value;

}
26 changes: 26 additions & 0 deletions tests/Rule/data/BackedEnumGenericsRule/code.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* @implements BackedEnum<string>
*/
enum MyStringEnum: string {
}

/**
* @implements BackedEnum<int>
*/
enum MyIntEnum: int {
}

/**
* @extends BackedEnum<int>
*/
interface MyBackedEnum extends BackedEnum {

}

enum MyIntEnumWithoutImplements: int { // error: Class MyIntEnumWithoutImplements extends generic BackedEnum, but does not specify its type. Use @implements BackedEnum<int>
}

enum MyIntEnumWithImplementsInParent: int implements MyBackedEnum {
}
3 changes: 3 additions & 0 deletions tests/Rule/data/BackedEnumGenericsRule/stub.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
parameters:
stubFiles:
- BackedEnum.php.stub

0 comments on commit fc3baef

Please sign in to comment.