Skip to content

Commit 83cf086

Browse files
authored
Add ForbiddenNewArgumentRule, RequireQueryBuilderOnRepositoryRule and NoGetInControllerRule (#158)
1 parent a45bd5a commit 83cf086

File tree

4 files changed

+226
-0
lines changed

4 files changed

+226
-0
lines changed

src/Enum/RuleIdentifier.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,10 @@ final class RuleIdentifier
194194
public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';
195195

196196
public const PHPUNIT_PUBLIC_STATIC_DATA_PROVIDER = 'phpunit.publicStaticDataProvider';
197+
198+
public const FORBIDDEN_NEW_INSTANCE = 'symplify.forbiddenNewInstance';
199+
200+
public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
201+
202+
public const NO_GET_IN_CONTROLLER = 'symfony.noGetInController';
197203
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\New_;
9+
use PhpParser\Node\Name;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
15+
16+
/**
17+
* @implements Rule<New_>
18+
*/
19+
final readonly class ForbiddenNewArgumentRule implements Rule
20+
{
21+
/**
22+
* @param string[] $forbiddenTypes
23+
*/
24+
public function __construct(
25+
private array $forbiddenTypes
26+
) {
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return New_::class;
32+
}
33+
34+
/**
35+
* @param New_ $node
36+
* @return RuleError[]
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if (! $node->class instanceof Name) {
41+
return [];
42+
}
43+
44+
$className = $node->class->toString();
45+
if (! in_array($className, $this->forbiddenTypes)) {
46+
return [];
47+
}
48+
49+
$errorMessage = sprintf(
50+
'Type "%s" is forbidden to be created manually. Use service and constructor injection instead',
51+
$className
52+
);
53+
54+
$identifierRuleError = RuleErrorBuilder::message($errorMessage)
55+
->identifier(RuleIdentifier::FORBIDDEN_NEW_INSTANCE)
56+
->build();
57+
58+
return [$identifierRuleError];
59+
}
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Doctrine;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Identifier;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\ObjectType;
14+
use Symplify\PHPStanRules\Enum\ClassName;
15+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
16+
17+
/**
18+
* @implements Rule<MethodCall>
19+
*/
20+
final class RequireQueryBuilderOnRepositoryRule implements Rule
21+
{
22+
/**
23+
* @var string
24+
*/
25+
private const ERROR_MESSAGE = 'Avoid calling ->createQueryBuilder() directly on EntityManager as it requires select() + from() calls with specific values. Use $repository->createQueryBuilder() to be safe instead';
26+
27+
public function getNodeType(): string
28+
{
29+
return MethodCall::class;
30+
}
31+
32+
/**
33+
* @param MethodCall $node
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (! $node->name instanceof Identifier) {
38+
return [];
39+
}
40+
41+
if ($node->name->toString() !== 'createQueryBuilder') {
42+
return [];
43+
}
44+
45+
$callerType = $scope->getType($node->var);
46+
if (! $callerType instanceof ObjectType) {
47+
return [];
48+
}
49+
50+
// we safe as both select() + from() calls are made on the repository
51+
if ($callerType->isInstanceOf(ClassName::ENTITY_REPOSITORY_CLASS)->yes()) {
52+
return [];
53+
}
54+
55+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
56+
->identifier(RuleIdentifier::REQUIRE_QUERY_BUILDER_ON_REPOSITORY)
57+
->build();
58+
59+
return [$identifierRuleError];
60+
}
61+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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\MethodCall;
9+
use PhpParser\Node\Expr\Variable;
10+
use PhpParser\Node\Identifier;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symplify\PHPStanRules\Enum\ClassName;
15+
use Symplify\PHPStanRules\Enum\RuleIdentifier;
16+
17+
/**
18+
* @implements Rule<MethodCall>
19+
*/
20+
final class NoGetInControllerRule implements Rule
21+
{
22+
/**
23+
* @var string[]
24+
*/
25+
private const CONTROLLER_TYPES = [
26+
ClassName::SYMFONY_CONTROLLER,
27+
ClassName::SYMFONY_ABSTRACT_CONTROLLER,
28+
];
29+
30+
/**
31+
* @var string
32+
*/
33+
private const ERROR_MESSAGE = 'Do not use $this->get(Type::class) method in controller to get services. Use __construct(Type $type) instead';
34+
35+
public function getNodeType(): string
36+
{
37+
return MethodCall::class;
38+
}
39+
40+
/**
41+
* @param MethodCall $node
42+
*/
43+
public function processNode(Node $node, Scope $scope): array
44+
{
45+
if (! $this->isThisGetMethodCall($node)) {
46+
return [];
47+
}
48+
49+
if (! $this->isInControllerClass($scope)) {
50+
return [];
51+
}
52+
53+
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
54+
->file($scope->getFile())
55+
->line($node->getStartLine())
56+
->identifier(RuleIdentifier::NO_GET_IN_CONTROLLER)
57+
->build();
58+
59+
return [$ruleError];
60+
}
61+
62+
private function isInControllerClass(Scope $scope): bool
63+
{
64+
if (! $scope->isInClass()) {
65+
return false;
66+
}
67+
68+
$classReflection = $scope->getClassReflection();
69+
foreach (self::CONTROLLER_TYPES as $controllerType) {
70+
if ($classReflection->isSubclassOf($controllerType)) {
71+
return true;
72+
}
73+
}
74+
75+
return false;
76+
}
77+
78+
private function isThisGetMethodCall(MethodCall $methodCall): bool
79+
{
80+
if (! $methodCall->name instanceof Identifier) {
81+
return false;
82+
}
83+
84+
if ($methodCall->name->toString() !== 'get') {
85+
return false;
86+
}
87+
88+
// is "$this"?
89+
if (! $methodCall->var instanceof Variable) {
90+
return false;
91+
}
92+
93+
if (! is_string($methodCall->var->name)) {
94+
return false;
95+
}
96+
97+
return $methodCall->var->name === 'this';
98+
}
99+
}

0 commit comments

Comments
 (0)