Skip to content

Commit adc4f08

Browse files
authored
Add NoArrayMapWithArrayCallableRule (#217)
1 parent a48d567 commit adc4f08

File tree

9 files changed

+189
-2
lines changed

9 files changed

+189
-2
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,35 @@ return __DIR__ . '/../fixtures/some_file.yml';
198198
:+1:
199199

200200

201+
<br>
202+
203+
### NoArrayMapWithArrayCallableRule
204+
205+
Array map with array callable is not allowed. Use anonymous/arrow function instead, to get better static analysis
206+
207+
```yaml
208+
rules:
209+
- Symplify\PHPStanRules\Rules\NoArrayMapWithArrayCallableRule
210+
```
211+
212+
```php
213+
$items = ['apple', 'banana', 'orange'];
214+
$items = array_map(['SomeClass', 'method'], $items);
215+
```
216+
217+
:x:
218+
219+
<br>
220+
221+
```php
222+
$items = ['apple', 'banana', 'orange'];
223+
$items = array_map(function ($item) {
224+
return $this->method($item);
225+
}, $items);
226+
```
227+
228+
:+1:
229+
201230
<br>
202231

203232
### NoConstructorOverrideRule

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
"nikic/php-parser": "^5.4",
1515
"phpunit/phpunit": "^11.5",
1616
"symfony/framework-bundle": "6.1.*",
17-
"phpecs/phpecs": "^2.0",
17+
"phpecs/phpecs": "^2.1",
1818
"tomasvotruba/class-leak": "^2.0",
19-
"rector/rector": "^2.0.10",
19+
"rector/rector": "^2.0.11",
2020
"phpstan/extension-installer": "^1.4",
2121
"symplify/phpstan-extensions": "^12.0",
2222
"tomasvotruba/unused-public": "^2.0",

config/code-complexity-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rules:
22
- Symplify\PHPStanRules\Rules\NoDynamicNameRule
33
- Symplify\PHPStanRules\Rules\Complexity\NoJustPropertyAssignRule
4+
- Symplify\PHPStanRules\Rules\Complexity\NoArrayMapWithArrayCallableRule

src/Enum/RuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,6 @@ final class RuleIdentifier
7373
public const NO_PROTECTED_CLASS_STMT = 'symplify.noProtectedClassStmt';
7474

7575
public const CONVENTION_PARAM_NAME_TO_TYPE = 'symplify.conventionParamNameToType';
76+
77+
public const NO_ARRAY_MAP_WITH_ARRAY_CALLABLE = 'symplify.noArrayMapWithArrayCallable';
7678
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Complexity;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Array_;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Name;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleError;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
16+
17+
/**
18+
* @implements Rule<FuncCall>
19+
*
20+
* @see \Symplify\PHPStanRules\Tests\Rules\NoArrayMapWithArrayCallableRule\NoArrayMapWithArrayCallableRuleTest
21+
*/
22+
final class NoArrayMapWithArrayCallableRule implements Rule
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const ERROR_MESSAGE = 'Avoid using array callables in array_map(), as it cripples static analysis on used method';
28+
29+
public function getNodeType(): string
30+
{
31+
return FuncCall::class;
32+
}
33+
34+
/**
35+
* @param FuncCall $node
36+
* @return RuleError[]
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if (! $node->name instanceof Name) {
41+
return [];
42+
}
43+
44+
$functionName = $node->name->toString();
45+
if ($functionName !== 'array_map') {
46+
return [];
47+
}
48+
49+
if ($node->isFirstClassCallable()) {
50+
return [];
51+
}
52+
53+
$args = $node->getArgs();
54+
$firstArgValue = $args[0]->value;
55+
if (! $firstArgValue instanceof Array_) {
56+
return [];
57+
}
58+
59+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
60+
->identifier(RuleIdentifier::NO_ARRAY_MAP_WITH_ARRAY_CALLABLE)
61+
->build();
62+
63+
return [$identifierRuleError];
64+
}
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\NoArrayMapWithArrayCallableRule\Fixture;
6+
7+
final class SkipValidArrayMapCall
8+
{
9+
public function run(array $items)
10+
{
11+
$changedItems = array_map(fn($item) => $this->change($item), $items);
12+
}
13+
14+
public function change($item)
15+
{
16+
// ...
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\NoArrayMapWithArrayCallableRule\Fixture;
6+
7+
final class SomeArrayMapCalls
8+
{
9+
public function run(array $items)
10+
{
11+
$changedItems = array_map([$this, 'change'], $items);
12+
}
13+
14+
public function change()
15+
{
16+
// ...
17+
}
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\NoArrayMapWithArrayCallableRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Complexity\NoArrayMapWithArrayCallableRule;
12+
13+
final class NoArrayMapWithArrayCallableRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorMessagesWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
yield [
27+
__DIR__ . '/Fixture/SomeArrayMapCalls.php',
28+
[[NoArrayMapWithArrayCallableRule::ERROR_MESSAGE, 11]],
29+
];
30+
31+
yield [
32+
__DIR__ . '/Fixture/SkipValidArrayMapCall.php',
33+
[],
34+
];
35+
}
36+
37+
/**
38+
* @return string[]
39+
*/
40+
public static function getAdditionalConfigFiles(): array
41+
{
42+
return [__DIR__ . '/config/configured_rule.neon'];
43+
}
44+
45+
protected function getRule(): Rule
46+
{
47+
return self::getContainer()->getByType(NoArrayMapWithArrayCallableRule::class);
48+
}
49+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
includes:
2+
- ../../../config/included_services.neon
3+
4+
rules:
5+
- Symplify\PHPStanRules\Rules\Complexity\NoArrayMapWithArrayCallableRule

0 commit comments

Comments
 (0)