Skip to content

Commit ce6cf97

Browse files
authored
[complexity] add foreach ception rule + no autowire duplicate symfony rule (#236)
1 parent b16a6d0 commit ce6cf97

File tree

11 files changed

+263
-1
lines changed

11 files changed

+263
-1
lines changed

config/code-complexity-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ rules:
33
- Symplify\PHPStanRules\Rules\Complexity\NoJustPropertyAssignRule
44
- Symplify\PHPStanRules\Rules\Complexity\NoArrayMapWithArrayCallableRule
55
- Symplify\PHPStanRules\Rules\Complexity\NoConstructorOverrideRule
6+
- Symplify\PHPStanRules\Rules\Complexity\ForeachCeptionRule

config/symfony-config-rules.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ rules:
44
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\AlreadyRegisteredAutodiscoveryServiceRule
55
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\TaggedIteratorOverRepeatedServiceCallRule
66

7+
# no autowire duplicate
8+
- Symplify\PHPStanRules\Rules\Symfony\NoServiceAutowireDuplicateRule
9+
710
# args() and arg() call
811
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoDuplicateArgsAutowireByTypeRule
912
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule

src/Enum/RuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@ final class RuleIdentifier
7575
public const CONVENTION_PARAM_NAME_TO_TYPE = 'symplify.conventionParamNameToType';
7676

7777
public const NO_ARRAY_MAP_WITH_ARRAY_CALLABLE = 'symplify.noArrayMapWithArrayCallable';
78+
79+
public const RULE_IDENTIFIER = 'symplify.foreachCeption';
7880
}

src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,6 @@ final class SymfonyRuleIdentifier
5757
public const REQUIRED_IS_GRANTED_ENUM = 'symfony.requiredIsGrantedEnum';
5858

5959
public const PREFER_AUTOWIRE_ATTRIBUTE_OVER_CONFIG_PARAM = 'symfony.preferAutowireAttributeOverConfigParam';
60+
61+
public const RULE_IDENTIFIER = 'symfony.noServiceAutowireDuplicate';
6062
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Complexity;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Foreach_;
9+
use PhpParser\NodeFinder;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
14+
15+
/**
16+
* @implements Rule<Foreach_>
17+
*/
18+
final class ForeachCeptionRule implements Rule
19+
{
20+
/**
21+
* @var string
22+
*/
23+
public const ERROR_MESSAGE = 'There is %d nested foreach nested in each other. Refactor to more flat approach or to collection to avoid high complexity';
24+
25+
private const MAX_NESTED_FOREACHES = 3;
26+
27+
public function getNodeType(): string
28+
{
29+
return Foreach_::class;
30+
}
31+
32+
/**
33+
* @param Foreach_ $node
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
$nodeFinder = new NodeFinder();
38+
39+
$nestedForeaches = $nodeFinder->findInstanceOf($node->stmts, Foreach_::class);
40+
if (count($nestedForeaches) <= self::MAX_NESTED_FOREACHES) {
41+
return [];
42+
}
43+
44+
$identifierRuleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, count($nestedForeaches) + 1))
45+
->identifier(RuleIdentifier::RULE_IDENTIFIER)
46+
->build();
47+
48+
return [$identifierRuleError];
49+
}
50+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Closure;
9+
use PhpParser\Node\Expr\ConstFetch;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\NodeFinder;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier;
16+
use Symplify\PHPStanRules\Helper\NamingHelper;
17+
use Symplify\PHPStanRules\Symfony\NodeAnalyzer\SymfonyClosureDetector;
18+
19+
/**
20+
* @implements Rule<Closure>
21+
*/
22+
final class NoServiceAutowireDuplicateRule implements Rule
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const ERROR_MESSAGE = 'Service autowire() is called as duplicate of $services->defaults()->autowire(). Remove it on the service';
28+
29+
public function getNodeType(): string
30+
{
31+
return Closure::class;
32+
}
33+
34+
/**
35+
* @param Closure $node
36+
*/
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (! SymfonyClosureDetector::detect($node)) {
40+
return [];
41+
}
42+
43+
$ruleErrors = [];
44+
45+
$hasDefaultsAutowire = false;
46+
47+
foreach ($node->stmts as $stmt) {
48+
if ($this->hasAutowireDefaultsMethodCall($stmt)) {
49+
$hasDefaultsAutowire = true;
50+
continue;
51+
}
52+
53+
if (! $hasDefaultsAutowire) {
54+
continue;
55+
}
56+
57+
$serviceAutowireMethodCall = $this->matchServiceAutowireMethodCall($stmt);
58+
if (! $serviceAutowireMethodCall instanceof MethodCall) {
59+
continue;
60+
}
61+
62+
$ruleErrors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
63+
->line($serviceAutowireMethodCall->getLine())
64+
->identifier(SymfonyRuleIdentifier::RULE_IDENTIFIER)
65+
->build();
66+
}
67+
68+
return $ruleErrors;
69+
}
70+
71+
private function hasAutowireDefaultsMethodCall(Node $someNode): bool
72+
{
73+
$nodeFinder = new NodeFinder();
74+
75+
$autowireDefaultsMethodCall = $nodeFinder->findFirst($someNode, function (Node $node): bool {
76+
if (! $node instanceof MethodCall) {
77+
return false;
78+
}
79+
80+
if (! NamingHelper::isName($node->name, 'autowire')) {
81+
return false;
82+
}
83+
84+
if (! $node->var instanceof MethodCall) {
85+
return false;
86+
}
87+
88+
// dummy way to detect, @todo improve with possible type check
89+
return NamingHelper::isName($node->var->name, 'defaults');
90+
});
91+
92+
return $autowireDefaultsMethodCall instanceof MethodCall;
93+
}
94+
95+
private function matchServiceAutowireMethodCall(Node $someNode): ?MethodCall
96+
{
97+
$nodeFinder = new NodeFinder();
98+
99+
$foundNode = $nodeFinder->findFirst($someNode, function (Node $node): bool {
100+
if (! $node instanceof MethodCall) {
101+
return false;
102+
}
103+
104+
if (! NamingHelper::isName($node->name, 'autowire')) {
105+
return false;
106+
}
107+
108+
if ($node->getArgs() === []) {
109+
return true;
110+
}
111+
112+
$firstArg = $node->getArgs()[0];
113+
if (! $firstArg->value instanceof ConstFetch) {
114+
return false;
115+
}
116+
117+
return $firstArg->value->name->toLowerString() === 'true';
118+
});
119+
120+
/** @var MethodCall|null $foundNode */
121+
return $foundNode;
122+
}
123+
}

tests/Issues/InstantiateMaximumIgnoredErrorCountRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
namespace Symplify\PHPStanRules\Tests\Issues;
66

7-
use Symplify\PHPStanRules\Rules\MaximumIgnoredErrorCountRule;
87
use PHPUnit\Framework\TestCase;
8+
use Symplify\PHPStanRules\Rules\MaximumIgnoredErrorCountRule;
99

1010
final class InstantiateMaximumIgnoredErrorCountRuleTest extends TestCase
1111
{
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoServiceAutowireDuplicateRule\Fixture;
6+
7+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
8+
9+
return function (ContainerConfigurator $containerConfigurator): void {
10+
$services = $containerConfigurator->services();
11+
$services->defaults();
12+
13+
$services->set('some_service')
14+
->autowire();
15+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoServiceAutowireDuplicateRule\Fixture;
6+
7+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
8+
9+
return function (ContainerConfigurator $containerConfigurator): void {
10+
$services = $containerConfigurator->services();
11+
$services->defaults()->autowire();
12+
13+
// this is duplicate
14+
$services->set('some_service')
15+
->autowire();
16+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoServiceAutowireDuplicateRule\Fixture;
6+
7+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
8+
9+
return function (ContainerConfigurator $containerConfigurator): void {
10+
$services = $containerConfigurator->services();
11+
$services->defaults()->autowire();
12+
13+
$services->set('some_service');
14+
};

0 commit comments

Comments
 (0)