Skip to content

Commit

Permalink
ForbidEnumInFunctionArgumentsRule (#16)
Browse files Browse the repository at this point in the history
* ForbidEnumInFunctionArgumentsRule

* Error message polish

* Fix conflicting names, revert dummy changes

* improve error message
  • Loading branch information
janedbal committed Jul 14, 2022
1 parent 869f2a9 commit f97f513
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ class User {
}
```

### 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
- [See test](https://github.com/shipmonk-rnd/phpstan-rules/blob/master/tests/Rule/data/ForbidEnumInFunctionArgumentsRule/code.php) for all functions and their problems
```neon
rules:
- ShipMonk\PHPStan\Rule\ForbidEnumInFunctionArgumentsRule
```
```php
enum MyEnum: string {
case MyCase = 'case1';
}

implode('', [MyEnum::MyCase]); // denied, would fail on implicit toString conversion
```


### ForbidFetchOnMixedRule
- Denies property fetch on unknown type.
- Any property fetch assumes the caller is an object with such property and therefore, the typehint/phpdoc should be fixed.
Expand Down
101 changes: 101 additions & 0 deletions src/Rule/ForbidEnumInFunctionArgumentsRule.php
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;
}

}
29 changes: 29 additions & 0 deletions tests/Rule/ForbidEnumInFunctionArgumentsRuleTest.php
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 tests/Rule/data/ForbidEnumInFunctionArgumentsRule/code.php
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);

0 comments on commit f97f513

Please sign in to comment.