Skip to content

Commit

Permalink
Add SingleRequiredMethodRule to spot multiple @required methods in Sy…
Browse files Browse the repository at this point in the history
…mfony projects (#163)

* split identifiers by group to easily search through them

* Add  SingleRequiredMethodRule
  • Loading branch information
TomasVotruba authored Jan 7, 2025
1 parent 39b9c4a commit 2b10e91
Show file tree
Hide file tree
Showing 44 changed files with 318 additions and 138 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,17 @@ class SomeClass extends SomeParentClass

<br>

### SingleRequiredMethodRule

There must be maximum 1 @required method in the class. Merge it to one to avoid possible injection collision or duplicated injects.

```yaml
rules:
- Symplify\PHPStanRules\Rules\Symfony\SingleRequiredMethodRule
```

<br>

### RequireAttributeNameRule

Attribute must have all names explicitly defined
Expand Down
1 change: 0 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,4 @@ parameters:
- '#Public constant "Symplify\\PHPStanRules\\(.*?)Rule\:\:ERROR_MESSAGE" is never used#'

- '#Although PHPStan\\Node\\InClassNode is covered by backward compatibility promise, this instanceof assumption might break because (.*?) not guaranteed to always stay the same#'

- '#PHPStan\\DependencyInjection\\NeonAdapter#'
5 changes: 5 additions & 0 deletions src/Enum/ClassName.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ final class ClassName
* @var string
*/
public const MOCK_OBJECT_CLASS = 'PHPUnit\Framework\MockObject\MockObject';

/**
* @var string
*/
public const REQUIRED_ATTRIBUTE = 'Symfony\Contracts\Service\Attribute\Required';
}
18 changes: 18 additions & 0 deletions src/Enum/DoctrineRuleIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Enum;

final class DoctrineRuleIdentifier
{
public const DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';

public const DOCTRINE_NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';

public const DOCTRINE_NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';

public const NO_ENTITY_MOCKING = 'doctrine.noEntityMocking';

public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
}
14 changes: 14 additions & 0 deletions src/Enum/PHPUnitRuleIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Enum;

final class PHPUnitRuleIdentifier
{
public const NO_DOCUMENT_MOCKING = 'phpunit.noDocumentMocking';

public const NO_MOCK_ONLY = 'phpunit.noMockOnly';

public const PUBLIC_STATIC_DATA_PROVIDER = 'phpunit.publicStaticDataProvider';
}
16 changes: 16 additions & 0 deletions src/Enum/RectorRuleIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Enum;

final class RectorRuleIdentifier
{
public const NO_INSTANCE_OF_STATIC_REFLECTION = 'rector.noInstanceOfStaticReflection';

public const UPGRADE_DOWNGRADE_REGISTERED_IN_SET = 'rector.upgradeDowngradeRegisteredInSet';

public const PHP_RULE_IMPLEMENTS_MIN_VERSION = 'rector.phpRuleImplementsMinVersion';

public const NO_CLASS_REFLECTION_STATIC_REFLECTION = 'rector.noClassReflectionStaticReflection';
}
40 changes: 0 additions & 40 deletions src/Enum/RuleIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,8 @@ final class RuleIdentifier

public const REQUIRE_ATTRIBUTE_NAME = 'symplify.requireAttributeName';

public const RECTOR_PHP_RULE_IMPLEMENTS_MIN_VERSION = 'rector.phpRuleImplementsMinVersion';

public const RECTOR_UPGRADE_DOWNGRADE_REGISTERED_IN_SET = 'rector.upgradeDowngradeRegisteredInSet';

public const PHP_PARSER_NO_LEADING_BACKSLASH_IN_NAME = 'phpParser.noLeadingBackslashInName';

public const RECTOR_NO_INSTANCE_OF_STATIC_REFLECTION = 'rector.noInstanceOfStaticReflection';

public const RECTOR_NO_CLASS_REFLECTION_STATIC_REFLECTION = 'rector.noClassReflectionStaticReflection';

public const PARENT_METHOD_VISIBILITY_OVERRIDE = 'symplify.parentMethodVisibilityOverride';

public const NO_RETURN_SETTER_METHOD = 'symplify.noReturnSetterMethod';
Expand Down Expand Up @@ -62,47 +54,15 @@ final class RuleIdentifier

public const REQUIRED_INTERFACE_CONTRACT_NAMESPACE = 'symplify.requiredInterfaceContractNamespace';

public const SYMFONY_REQUIRE_INVOKABLE_CONTROLLER = 'symfony.requireInvokableController';

public const NO_VALUE_OBJECT_IN_SERVICE_CONSTRUCTOR = 'symplify.noValueObjectInServiceConstructor';

public const DOCTRINE_NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';

public const PHPUNIT_NO_DOCUMENT_MOCKING = 'phpunit.noDocumentMocking';

public const NO_DYNAMIC_NAME = 'symplify.noDynamicName';

public const NO_REFERENCE = 'symplify.noReference';

public const PHPUNIT_NO_MOCK_ONLY = 'phpunit.noMockOnly';

public const SINGLE_ARG_EVENT_DISPATCH = 'symfony.singleArgEventDispatch';

public const NO_ENTITY_MOCKING = 'doctrine.noEntityMocking';

public const NO_STRING_IN_GET_SUBSCRIBED_EVENTS = 'symfony.noStringInGetSubscribedEvents';

public const NO_LISTENER_WITHOUT_CONTRACT = 'symfony.noListenerWithoutContract';

public const DOCTRINE_NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';

public const DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';

public const SYMFONY_NO_REQUIRED_OUTSIDE_CLASS = 'symfony.noRequiredOutsideClass';

public const NO_CONSTRUCTOR_OVERRIDE = 'symplify.noConstructorOverride';

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';

public const NO_GET_DOCTRINE_IN_CONTROLLER = 'symfony.noGetDoctrineInController';

public const MAXIMUM_IGNORED_ERROR_COUNT = 'symplify.maximumIgnoredErrorCount';
}
26 changes: 26 additions & 0 deletions src/Enum/SymfonyRuleIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Enum;

final class SymfonyRuleIdentifier
{
public const NO_GET_IN_CONTROLLER = 'symfony.noGetInController';

public const NO_GET_DOCTRINE_IN_CONTROLLER = 'symfony.noGetDoctrineInController';

public const SINGLE_ARG_EVENT_DISPATCH = 'symfony.singleArgEventDispatch';

public const NO_LISTENER_WITHOUT_CONTRACT = 'symfony.noListenerWithoutContract';

public const SYMFONY_REQUIRE_INVOKABLE_CONTROLLER = 'symfony.requireInvokableController';

public const SYMFONY_NO_REQUIRED_OUTSIDE_CLASS = 'symfony.noRequiredOutsideClass';

public const NO_STRING_IN_GET_SUBSCRIBED_EVENTS = 'symfony.noStringInGetSubscribedEvents';

public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';

public const SINGLE_REQUIRED_METHOD = 'symfony.singleRequiredMethod';
}
39 changes: 39 additions & 0 deletions src/NodeAnalyzer/SymfonyRequiredMethodAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\NodeAnalyzer;

use PhpParser\Comment\Doc;
use PhpParser\Node\Stmt\ClassMethod;
use Symplify\PHPStanRules\Enum\ClassName;

final class SymfonyRequiredMethodAnalyzer
{
public static function detect(ClassMethod $classMethod): bool
{
// speed up
if (! $classMethod->isPublic()) {
return false;
}

if ($classMethod->isMagic()) {
return false;
}

foreach ($classMethod->getAttrGroups() as $attributeGroup) {
foreach ($attributeGroup->attrs as $attr) {
if ($attr->name->toString() === ClassName::REQUIRED_ATTRIBUTE) {
return true;
}
}
}

$docComment = $classMethod->getDocComment();
if (! $docComment instanceof Doc) {
return false;
}

return str_contains($docComment->getText(), '@required');
}
}
4 changes: 2 additions & 2 deletions src/Rules/Doctrine/NoDocumentMockingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\PHPUnitRuleIdentifier;

/**
* @implements Rule<MethodCall>
Expand Down Expand Up @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array
}

$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::PHPUNIT_NO_DOCUMENT_MOCKING)
->identifier(PHPUnitRuleIdentifier::NO_DOCUMENT_MOCKING)
->build();

return [$ruleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Doctrine/NoEntityMockingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Doctrine\DoctrineEntityDocumentAnalyser;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;
use Symplify\PHPStanRules\NodeAnalyzer\MethodCallNameAnalyzer;

/**
Expand Down Expand Up @@ -62,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array
}

$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::NO_ENTITY_MOCKING)
->identifier(DoctrineRuleIdentifier::NO_ENTITY_MOCKING)
->build();

return [$ruleError];
Expand Down
6 changes: 3 additions & 3 deletions src/Rules/Doctrine/NoGetRepositoryOutsideServiceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;

/**
* @see \Symplify\PHPStanRules\Tests\Rules\Doctrine\NoGetRepositoryOutsideServiceRule\NoGetRepositoryOutsideServiceRuleTest
Expand Down Expand Up @@ -44,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array

if (! $scope->isInClass()) {
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE)
->identifier(DoctrineRuleIdentifier::DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE)
->build();

return [$ruleError];
Expand All @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
}

$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE)
->identifier(DoctrineRuleIdentifier::DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE)
->build();

return [$ruleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Doctrine/NoParentRepositoryRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;

/**
* Check if class extends repository class,
Expand Down Expand Up @@ -48,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array
}

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

return [$identifierRuleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Doctrine/NoRepositoryCallInDataFixtureRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;
use Symplify\PHPStanRules\Tests\Rules\Doctrine\NoRepositoryCallInDataFixtureRule\NoRepositoryCallInDataFixtureRuleTest;

/**
Expand Down Expand Up @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array
}

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

return [$identifierRuleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Doctrine/RequireQueryBuilderOnRepositoryRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;

/**
* @implements Rule<MethodCall>
Expand Down Expand Up @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array
}

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

return [$identifierRuleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/PHPUnit/NoMockOnlyTestRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\ClassName;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\PHPUnitRuleIdentifier;
use Symplify\PHPStanRules\Testing\PHPUnitTestAnalyser;

/**
Expand Down Expand Up @@ -69,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array
}

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

return [$identifierRuleError];
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/PHPUnit/PublicStaticDataProviderRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\PHPUnitRuleIdentifier;
use Symplify\PHPStanRules\PHPUnit\DataProviderMethodResolver;
use Symplify\PHPStanRules\Testing\PHPUnitTestAnalyser;

Expand Down Expand Up @@ -76,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array
if (! $dataProviderClassMethod->isPublic()) {
$errorMessage = sprintf(self::PUBLIC_ERROR_MESSAGE, $dataProviderMethodName);
$ruleErrors[] = RuleErrorBuilder::message($errorMessage)
->identifier(RuleIdentifier::PHPUNIT_PUBLIC_STATIC_DATA_PROVIDER)
->identifier(PHPUnitRuleIdentifier::PUBLIC_STATIC_DATA_PROVIDER)
->line($dataProviderClassMethod->getLine())
->build();
}
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Rector/NoClassReflectionStaticReflectionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use ReflectionClass;
use Symplify\PHPStanRules\Enum\RuleIdentifier;
use Symplify\PHPStanRules\Enum\RectorRuleIdentifier;
use Symplify\PHPStanRules\TypeAnalyzer\RectorAllowedAutoloadedTypeAnalyzer;

/**
Expand Down Expand Up @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array
}

return [RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier(RuleIdentifier::RECTOR_NO_CLASS_REFLECTION_STATIC_REFLECTION)
->identifier(RectorRuleIdentifier::NO_CLASS_REFLECTION_STATIC_REFLECTION)
->build()];
}
}
Loading

0 comments on commit 2b10e91

Please sign in to comment.