Skip to content

Commit

Permalink
Add ForbiddenNewArgumentRule, RequireQueryBuilderOnRepositoryRule and…
Browse files Browse the repository at this point in the history
… NoGetInControllerRule (#158)
  • Loading branch information
TomasVotruba authored Dec 22, 2024
1 parent a45bd5a commit 83cf086
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/Enum/RuleIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,10 @@ final class RuleIdentifier
public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';

public const PHPUNIT_PUBLIC_STATIC_DATA_PROVIDER = 'phpunit.publicStaticDataProvider';

public const FORBIDDEN_NEW_INSTANCE = 'symplify.forbiddenNewInstance';

public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';

public const NO_GET_IN_CONTROLLER = 'symfony.noGetInController';
}
60 changes: 60 additions & 0 deletions src/Rules/Complexity/ForbiddenNewArgumentRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules\Complexity;

use PhpParser\Node;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\RuleIdentifier;

/**
* @implements Rule<New_>
*/
final readonly class ForbiddenNewArgumentRule implements Rule
{
/**
* @param string[] $forbiddenTypes
*/
public function __construct(
private array $forbiddenTypes
) {
}

public function getNodeType(): string
{
return New_::class;
}

/**
* @param New_ $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $node->class instanceof Name) {
return [];
}

$className = $node->class->toString();
if (! in_array($className, $this->forbiddenTypes)) {
return [];
}

$errorMessage = sprintf(
'Type "%s" is forbidden to be created manually. Use service and constructor injection instead',
$className
);

$identifierRuleError = RuleErrorBuilder::message($errorMessage)
->identifier(RuleIdentifier::FORBIDDEN_NEW_INSTANCE)
->build();

return [$identifierRuleError];
}
}
61 changes: 61 additions & 0 deletions src/Rules/Doctrine/RequireQueryBuilderOnRepositoryRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules\Doctrine;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;

/**
* @implements Rule<MethodCall>
*/
final class RequireQueryBuilderOnRepositoryRule implements Rule
{
/**
* @var string
*/
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';

public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param MethodCall $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $node->name instanceof Identifier) {
return [];
}

if ($node->name->toString() !== 'createQueryBuilder') {
return [];
}

$callerType = $scope->getType($node->var);
if (! $callerType instanceof ObjectType) {
return [];
}

// we safe as both select() + from() calls are made on the repository
if ($callerType->isInstanceOf(ClassName::ENTITY_REPOSITORY_CLASS)->yes()) {
return [];
}

$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::REQUIRE_QUERY_BUILDER_ON_REPOSITORY)
->build();

return [$identifierRuleError];
}
}
99 changes: 99 additions & 0 deletions src/Rules/Symfony/NoGetInControllerRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules\Symfony;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;

/**
* @implements Rule<MethodCall>
*/
final class NoGetInControllerRule implements Rule
{
/**
* @var string[]
*/
private const CONTROLLER_TYPES = [
ClassName::SYMFONY_CONTROLLER,
ClassName::SYMFONY_ABSTRACT_CONTROLLER,
];

/**
* @var string
*/
private const ERROR_MESSAGE = 'Do not use $this->get(Type::class) method in controller to get services. Use __construct(Type $type) instead';

public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param MethodCall $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->isThisGetMethodCall($node)) {
return [];
}

if (! $this->isInControllerClass($scope)) {
return [];
}

$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->file($scope->getFile())
->line($node->getStartLine())
->identifier(RuleIdentifier::NO_GET_IN_CONTROLLER)
->build();

return [$ruleError];
}

private function isInControllerClass(Scope $scope): bool
{
if (! $scope->isInClass()) {
return false;
}

$classReflection = $scope->getClassReflection();
foreach (self::CONTROLLER_TYPES as $controllerType) {
if ($classReflection->isSubclassOf($controllerType)) {
return true;
}
}

return false;
}

private function isThisGetMethodCall(MethodCall $methodCall): bool
{
if (! $methodCall->name instanceof Identifier) {
return false;
}

if ($methodCall->name->toString() !== 'get') {
return false;
}

// is "$this"?
if (! $methodCall->var instanceof Variable) {
return false;
}

if (! is_string($methodCall->var->name)) {
return false;
}

return $methodCall->var->name === 'this';
}
}

0 comments on commit 83cf086

Please sign in to comment.