-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
280 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\BinaryOp; | ||
use PhpParser\Node\Expr\BinaryOp\Identical; | ||
use PhpParser\Node\Expr\BinaryOp\NotIdentical; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Type\Enum\EnumCaseObjectType; | ||
use function array_map; | ||
use function array_merge; | ||
use function array_unique; | ||
use function count; | ||
|
||
/** | ||
* @implements Rule<BinaryOp> | ||
*/ | ||
class EnforceEnumMatchRule implements Rule | ||
{ | ||
|
||
public function getNodeType(): string | ||
{ | ||
return BinaryOp::class; | ||
} | ||
|
||
/** | ||
* @param BinaryOp $node | ||
* @return list<string> | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$node instanceof Identical && !$node instanceof NotIdentical) { | ||
return []; | ||
} | ||
|
||
$conditionType = $scope->getType($node); | ||
|
||
if (!$conditionType->isTrue()->yes() && !$conditionType->isFalse()->yes()) { | ||
return []; | ||
} | ||
|
||
$leftType = $scope->getType($node->left); | ||
$rightType = $scope->getType($node->right); | ||
|
||
if ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) { | ||
$enumCases = array_unique( | ||
array_merge( | ||
array_map(static fn (EnumCaseObjectType $type) => "{$type->getClassName()}::{$type->getEnumCaseName()}", $leftType->getEnumCases()), | ||
array_map(static fn (EnumCaseObjectType $type) => "{$type->getClassName()}::{$type->getEnumCaseName()}", $rightType->getEnumCases()), | ||
), | ||
); | ||
|
||
if (count($enumCases) !== 1) { | ||
return []; // do not report nonsense comparison | ||
} | ||
|
||
$trueFalse = $conditionType->isTrue()->yes() ? 'true' : 'false'; | ||
return ["This condition contains always-$trueFalse enum comparison of $enumCases[0]. Use match expression instead, PHPStan will report unhandled enum cases"]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PHPStan\Rules\Rule; | ||
use ShipMonk\PHPStan\RuleTestCase; | ||
use const PHP_VERSION_ID; | ||
|
||
/** | ||
* @extends RuleTestCase<EnforceEnumMatchRule> | ||
*/ | ||
class EnforceEnumMatchRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new EnforceEnumMatchRule(); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
if (PHP_VERSION_ID < 80_100) { | ||
self::markTestSkipped('Requires PHP 8.1'); | ||
} | ||
|
||
$this->analyseFile( | ||
__DIR__ . '/data/EnforceEnumMatchRule/code.php', | ||
); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace EnforceEnumMatchRule; | ||
|
||
enum SomeEnum: string | ||
{ | ||
|
||
case One = 'one'; | ||
case Two = 'two'; | ||
case Three = 'three'; | ||
|
||
public function nonEnumConditionIncluded(bool $condition): int | ||
{ | ||
if ($this === self::Three || $this === self::One) { | ||
return -1; | ||
} elseif ($this === self::Two || $condition) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::Two. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 0; | ||
} | ||
|
||
return 1; | ||
} | ||
|
||
public function exhaustiveWithOrCondition(): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} elseif ($this === self::Two || $this === self::One) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::One. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 0; | ||
} | ||
} | ||
|
||
public function basicExhaustive(): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} elseif ($this === self::Two) { | ||
return 0; | ||
} elseif ($this === self::One) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::One. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 1; | ||
} | ||
} | ||
|
||
public function notExhaustiveWithNegatedConditionInLastElseif(): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} elseif ($this === self::Two) { | ||
return 0; | ||
} elseif ($this !== self::One) { // error: This condition contains always-false enum comparison of EnforceEnumMatchRule\SomeEnum::One. Use match expression instead, PHPStan will report unhandled enum cases | ||
throw new \LogicException('Not expected case'); | ||
} | ||
|
||
return 1; | ||
} | ||
|
||
public function nonSenseAlwaysFalseCodeNotReported(self $enum): int | ||
{ | ||
if ($enum === self::Three) { | ||
return -1; | ||
} | ||
|
||
if ($enum === self::Three) { // cannot use $this, see https://github.com/phpstan/phpstan/issues/9142 | ||
return 0; | ||
} | ||
|
||
return 1; | ||
} | ||
|
||
public function notExhaustive(): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} elseif ($this === self::Two) { | ||
return 0; | ||
} | ||
|
||
return 1; | ||
} | ||
|
||
public function exhaustiveButNoElseIf(): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} | ||
|
||
if ($this === self::Two) { | ||
return 0; | ||
} | ||
|
||
if ($this === self::One) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::One. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 1; | ||
} | ||
} | ||
|
||
/** | ||
* @param self::Two|self::Three $param | ||
*/ | ||
public function exhaustiveOfSubset(self $param): int | ||
{ | ||
if ($param === self::Three) { | ||
return -1; | ||
} elseif ($param === self::Two) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::Two. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 0; | ||
} | ||
} | ||
|
||
public function exhaustiveButNotAllInThatElseIfChain(self $param): int | ||
{ | ||
if ($param === self::One) { | ||
throw new \LogicException(); | ||
} | ||
|
||
if ($param === self::Three) { | ||
return -1; | ||
} elseif ($param === self::Two) { // error: This condition contains always-true enum comparison of EnforceEnumMatchRule\SomeEnum::Two. Use match expression instead, PHPStan will report unhandled enum cases | ||
return 0; | ||
} | ||
} | ||
|
||
/** | ||
* @param true $true | ||
*/ | ||
public function alwaysTrueButNoEnumThere(bool $true): int | ||
{ | ||
if ($this === self::Three) { | ||
return -1; | ||
} elseif ($true === true) { | ||
return 0; | ||
} | ||
} | ||
|
||
} |