-
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.
ForbidEnumInFunctionArgumentsRule (#16)
* ForbidEnumInFunctionArgumentsRule * Error message polish * Fix conflicting names, revert dummy changes * improve error message
- Loading branch information
Showing
4 changed files
with
175 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr\FuncCall; | ||
use PhpParser\Node\Name; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use ShipMonk\PHPStan\Helper\EnumTypeHelper; | ||
use function array_key_exists; | ||
use function count; | ||
use function implode; | ||
|
||
/** | ||
* @template-implements Rule<FuncCall> | ||
*/ | ||
class ForbidEnumInFunctionArgumentsRule implements Rule | ||
{ | ||
|
||
private const ANY_ARGUMENT = -1; | ||
|
||
private const REASON_IMPLICIT_TO_STRING = 'as the function causes implicit __toString conversion which is not supported for enums'; | ||
private const REASON_UNPREDICTABLE_RESULT = 'as the function causes unexpected results'; // https://3v4l.org/YtGVa | ||
private const REASON_SKIPS_ENUMS = 'as the function will skip any enums and produce warning'; | ||
|
||
private const FUNCTION_MAP = [ | ||
'array_intersect' => [self::ANY_ARGUMENT, self::REASON_IMPLICIT_TO_STRING], | ||
'array_intersect_assoc' => [self::ANY_ARGUMENT, self::REASON_IMPLICIT_TO_STRING], | ||
'array_diff' => [self::ANY_ARGUMENT, self::REASON_IMPLICIT_TO_STRING], | ||
'array_diff_assoc' => [self::ANY_ARGUMENT, self::REASON_IMPLICIT_TO_STRING], | ||
'array_unique' => [0, self::REASON_IMPLICIT_TO_STRING], | ||
'array_combine' => [0, self::REASON_IMPLICIT_TO_STRING], | ||
'sort' => [0, self::REASON_UNPREDICTABLE_RESULT], | ||
'asort' => [0, self::REASON_UNPREDICTABLE_RESULT], | ||
'arsort' => [0, self::REASON_UNPREDICTABLE_RESULT], | ||
'natsort' => [0, self::REASON_IMPLICIT_TO_STRING], | ||
'array_count_values' => [0, self::REASON_SKIPS_ENUMS], | ||
'array_fill_keys' => [0, self::REASON_IMPLICIT_TO_STRING], | ||
'array_flip' => [0, self::REASON_SKIPS_ENUMS], | ||
'array_product' => [0, self::REASON_UNPREDICTABLE_RESULT], | ||
'array_sum' => [0, self::REASON_UNPREDICTABLE_RESULT], | ||
'implode' => [1, self::REASON_IMPLICIT_TO_STRING], | ||
]; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return FuncCall::class; | ||
} | ||
|
||
/** | ||
* @param FuncCall $node | ||
* @return string[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$node->name instanceof Name) { | ||
return []; | ||
} | ||
|
||
$functionName = $node->name->toLowerString(); | ||
|
||
if (!array_key_exists($functionName, self::FUNCTION_MAP)) { | ||
return []; | ||
} | ||
|
||
[$forbiddenArgumentPosition, $reason] = self::FUNCTION_MAP[$functionName]; | ||
|
||
$wrongArguments = []; | ||
|
||
foreach ($node->getArgs() as $position => $argument) { | ||
if (!$this->matchesPosition((int) $position, $forbiddenArgumentPosition)) { | ||
continue; | ||
} | ||
|
||
$argumentType = $scope->getType($argument->value); | ||
|
||
if (EnumTypeHelper::containsEnum($argumentType)) { | ||
$wrongArguments[] = $position + 1; | ||
} | ||
} | ||
|
||
if ($wrongArguments !== []) { | ||
$plural = count($wrongArguments) > 1 ? 's' : ''; | ||
$wrongArgumentsString = implode(', ', $wrongArguments); | ||
return ["Argument{$plural} {$wrongArgumentsString} in {$node->name->toString()}() cannot contain enum {$reason}"]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
private function matchesPosition(int $position, int $forbiddenArgumentPosition): bool | ||
{ | ||
if ($forbiddenArgumentPosition === self::ANY_ARGUMENT) { | ||
return true; | ||
} | ||
|
||
return $position === $forbiddenArgumentPosition; | ||
} | ||
|
||
} |
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,29 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace ShipMonk\PHPStan\Rule; | ||
|
||
use PHPStan\Rules\Rule; | ||
use ShipMonk\PHPStan\RuleTestCase; | ||
use const PHP_VERSION_ID; | ||
|
||
/** | ||
* @extends RuleTestCase<ForbidEnumInFunctionArgumentsRule> | ||
*/ | ||
class ForbidEnumInFunctionArgumentsRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new ForbidEnumInFunctionArgumentsRule(); | ||
} | ||
|
||
public function test(): void | ||
{ | ||
if (PHP_VERSION_ID < 80_100) { | ||
self::markTestSkipped('Requires PHP 8.1'); | ||
} | ||
|
||
$this->analyseFile(__DIR__ . '/data/ForbidEnumInFunctionArgumentsRule/code.php'); | ||
} | ||
|
||
} |
28 changes: 28 additions & 0 deletions
28
tests/Rule/data/ForbidEnumInFunctionArgumentsRule/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,28 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
enum SomeEnum: string { | ||
case Bar = 'bar'; | ||
case Baz = 'baz'; | ||
} | ||
|
||
$enums1 = [SomeEnum::Bar, SomeEnum::Baz]; | ||
$enums2 = [SomeEnum::Bar]; | ||
$enums3 = [SomeEnum::Baz]; | ||
|
||
array_intersect($enums1, $enums2, $enums3); // error: Arguments 1, 2, 3 in array_intersect() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_intersect_assoc($enums1, $enums2, $enums3); // error: Arguments 1, 2, 3 in array_intersect_assoc() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_diff($enums1, $enums2, $enums3); // error: Arguments 1, 2, 3 in array_diff() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_diff_assoc($enums1, $enums2, $enums3); // error: Arguments 1, 2, 3 in array_diff_assoc() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_unique($enums1); // error: Argument 1 in array_unique() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_combine($enums2, $enums3); // error: Argument 1 in array_combine() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
sort($enums1); // error: Argument 1 in sort() cannot contain enum as the function causes unexpected results | ||
asort($enums1); // error: Argument 1 in asort() cannot contain enum as the function causes unexpected results | ||
arsort($enums1); // error: Argument 1 in arsort() cannot contain enum as the function causes unexpected results | ||
natsort($enums1); // error: Argument 1 in natsort() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_count_values($enums1); // error: Argument 1 in array_count_values() cannot contain enum as the function will skip any enums and produce warning | ||
array_fill_keys($enums1, 1); // error: Argument 1 in array_fill_keys() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
array_flip($enums1); // error: Argument 1 in array_flip() cannot contain enum as the function will skip any enums and produce warning | ||
array_product($enums1); // error: Argument 1 in array_product() cannot contain enum as the function causes unexpected results | ||
array_sum($enums1); // error: Argument 1 in array_sum() cannot contain enum as the function causes unexpected results | ||
implode('', $enums1); // error: Argument 2 in implode() cannot contain enum as the function causes implicit __toString conversion which is not supported for enums | ||
in_array(SomeEnum::Bar, $enums1); |