Skip to content

Commit 2b10e91

Browse files
authored
Add SingleRequiredMethodRule to spot multiple @required methods in Symfony projects (#163)
* split identifiers by group to easily search through them * Add SingleRequiredMethodRule
1 parent 39b9c4a commit 2b10e91

File tree

44 files changed

+318
-138
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+318
-138
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,17 @@ class SomeClass extends SomeParentClass
794794

795795
<br>
796796

797+
### SingleRequiredMethodRule
798+
799+
There must be maximum 1 @required method in the class. Merge it to one to avoid possible injection collision or duplicated injects.
800+
801+
```yaml
802+
rules:
803+
- Symplify\PHPStanRules\Rules\Symfony\SingleRequiredMethodRule
804+
```
805+
806+
<br>
807+
797808
### RequireAttributeNameRule
798809

799810
Attribute must have all names explicitly defined

phpstan.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,4 @@ parameters:
4848
- '#Public constant "Symplify\\PHPStanRules\\(.*?)Rule\:\:ERROR_MESSAGE" is never used#'
4949

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

src/Enum/ClassName.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,9 @@ final class ClassName
8080
* @var string
8181
*/
8282
public const MOCK_OBJECT_CLASS = 'PHPUnit\Framework\MockObject\MockObject';
83+
84+
/**
85+
* @var string
86+
*/
87+
public const REQUIRED_ATTRIBUTE = 'Symfony\Contracts\Service\Attribute\Required';
8388
}
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\Enum;
6+
7+
final class DoctrineRuleIdentifier
8+
{
9+
public const DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';
10+
11+
public const DOCTRINE_NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';
12+
13+
public const DOCTRINE_NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';
14+
15+
public const NO_ENTITY_MOCKING = 'doctrine.noEntityMocking';
16+
17+
public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
18+
}

src/Enum/PHPUnitRuleIdentifier.php

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\Enum;
6+
7+
final class PHPUnitRuleIdentifier
8+
{
9+
public const NO_DOCUMENT_MOCKING = 'phpunit.noDocumentMocking';
10+
11+
public const NO_MOCK_ONLY = 'phpunit.noMockOnly';
12+
13+
public const PUBLIC_STATIC_DATA_PROVIDER = 'phpunit.publicStaticDataProvider';
14+
}

src/Enum/RectorRuleIdentifier.php

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\Enum;
6+
7+
final class RectorRuleIdentifier
8+
{
9+
public const NO_INSTANCE_OF_STATIC_REFLECTION = 'rector.noInstanceOfStaticReflection';
10+
11+
public const UPGRADE_DOWNGRADE_REGISTERED_IN_SET = 'rector.upgradeDowngradeRegisteredInSet';
12+
13+
public const PHP_RULE_IMPLEMENTS_MIN_VERSION = 'rector.phpRuleImplementsMinVersion';
14+
15+
public const NO_CLASS_REFLECTION_STATIC_REFLECTION = 'rector.noClassReflectionStaticReflection';
16+
}

src/Enum/RuleIdentifier.php

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,8 @@ final class RuleIdentifier
1212

1313
public const REQUIRE_ATTRIBUTE_NAME = 'symplify.requireAttributeName';
1414

15-
public const RECTOR_PHP_RULE_IMPLEMENTS_MIN_VERSION = 'rector.phpRuleImplementsMinVersion';
16-
17-
public const RECTOR_UPGRADE_DOWNGRADE_REGISTERED_IN_SET = 'rector.upgradeDowngradeRegisteredInSet';
18-
1915
public const PHP_PARSER_NO_LEADING_BACKSLASH_IN_NAME = 'phpParser.noLeadingBackslashInName';
2016

21-
public const RECTOR_NO_INSTANCE_OF_STATIC_REFLECTION = 'rector.noInstanceOfStaticReflection';
22-
23-
public const RECTOR_NO_CLASS_REFLECTION_STATIC_REFLECTION = 'rector.noClassReflectionStaticReflection';
24-
2517
public const PARENT_METHOD_VISIBILITY_OVERRIDE = 'symplify.parentMethodVisibilityOverride';
2618

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

6355
public const REQUIRED_INTERFACE_CONTRACT_NAMESPACE = 'symplify.requiredInterfaceContractNamespace';
6456

65-
public const SYMFONY_REQUIRE_INVOKABLE_CONTROLLER = 'symfony.requireInvokableController';
66-
6757
public const NO_VALUE_OBJECT_IN_SERVICE_CONSTRUCTOR = 'symplify.noValueObjectInServiceConstructor';
6858

69-
public const DOCTRINE_NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';
70-
71-
public const PHPUNIT_NO_DOCUMENT_MOCKING = 'phpunit.noDocumentMocking';
72-
7359
public const NO_DYNAMIC_NAME = 'symplify.noDynamicName';
7460

7561
public const NO_REFERENCE = 'symplify.noReference';
7662

77-
public const PHPUNIT_NO_MOCK_ONLY = 'phpunit.noMockOnly';
78-
79-
public const SINGLE_ARG_EVENT_DISPATCH = 'symfony.singleArgEventDispatch';
80-
81-
public const NO_ENTITY_MOCKING = 'doctrine.noEntityMocking';
82-
83-
public const NO_STRING_IN_GET_SUBSCRIBED_EVENTS = 'symfony.noStringInGetSubscribedEvents';
84-
85-
public const NO_LISTENER_WITHOUT_CONTRACT = 'symfony.noListenerWithoutContract';
86-
87-
public const DOCTRINE_NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';
88-
89-
public const DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';
90-
91-
public const SYMFONY_NO_REQUIRED_OUTSIDE_CLASS = 'symfony.noRequiredOutsideClass';
92-
9363
public const NO_CONSTRUCTOR_OVERRIDE = 'symplify.noConstructorOverride';
9464

95-
public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';
96-
97-
public const PHPUNIT_PUBLIC_STATIC_DATA_PROVIDER = 'phpunit.publicStaticDataProvider';
98-
9965
public const FORBIDDEN_NEW_INSTANCE = 'symplify.forbiddenNewInstance';
10066

101-
public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
102-
103-
public const NO_GET_IN_CONTROLLER = 'symfony.noGetInController';
104-
105-
public const NO_GET_DOCTRINE_IN_CONTROLLER = 'symfony.noGetDoctrineInController';
106-
10767
public const MAXIMUM_IGNORED_ERROR_COUNT = 'symplify.maximumIgnoredErrorCount';
10868
}

src/Enum/SymfonyRuleIdentifier.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Enum;
6+
7+
final class SymfonyRuleIdentifier
8+
{
9+
public const NO_GET_IN_CONTROLLER = 'symfony.noGetInController';
10+
11+
public const NO_GET_DOCTRINE_IN_CONTROLLER = 'symfony.noGetDoctrineInController';
12+
13+
public const SINGLE_ARG_EVENT_DISPATCH = 'symfony.singleArgEventDispatch';
14+
15+
public const NO_LISTENER_WITHOUT_CONTRACT = 'symfony.noListenerWithoutContract';
16+
17+
public const SYMFONY_REQUIRE_INVOKABLE_CONTROLLER = 'symfony.requireInvokableController';
18+
19+
public const SYMFONY_NO_REQUIRED_OUTSIDE_CLASS = 'symfony.noRequiredOutsideClass';
20+
21+
public const NO_STRING_IN_GET_SUBSCRIBED_EVENTS = 'symfony.noStringInGetSubscribedEvents';
22+
23+
public const SYMFONY_NO_ABSTRACT_CONTROLLER_CONSTRUCTOR = 'symfony.noAbstractControllerConstructor';
24+
25+
public const SINGLE_REQUIRED_METHOD = 'symfony.singleRequiredMethod';
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\NodeAnalyzer;
6+
7+
use PhpParser\Comment\Doc;
8+
use PhpParser\Node\Stmt\ClassMethod;
9+
use Symplify\PHPStanRules\Enum\ClassName;
10+
11+
final class SymfonyRequiredMethodAnalyzer
12+
{
13+
public static function detect(ClassMethod $classMethod): bool
14+
{
15+
// speed up
16+
if (! $classMethod->isPublic()) {
17+
return false;
18+
}
19+
20+
if ($classMethod->isMagic()) {
21+
return false;
22+
}
23+
24+
foreach ($classMethod->getAttrGroups() as $attributeGroup) {
25+
foreach ($attributeGroup->attrs as $attr) {
26+
if ($attr->name->toString() === ClassName::REQUIRED_ATTRIBUTE) {
27+
return true;
28+
}
29+
}
30+
}
31+
32+
$docComment = $classMethod->getDocComment();
33+
if (! $docComment instanceof Doc) {
34+
return false;
35+
}
36+
37+
return str_contains($docComment->getText(), '@required');
38+
}
39+
}

src/Rules/Doctrine/NoDocumentMockingRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13-
use Symplify\PHPStanRules\Enum\RuleIdentifier;
13+
use Symplify\PHPStanRules\Enum\PHPUnitRuleIdentifier;
1414

1515
/**
1616
* @implements Rule<MethodCall>
@@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array
5353
}
5454

5555
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
56-
->identifier(RuleIdentifier::PHPUNIT_NO_DOCUMENT_MOCKING)
56+
->identifier(PHPUnitRuleIdentifier::NO_DOCUMENT_MOCKING)
5757
->build();
5858

5959
return [$ruleError];

0 commit comments

Comments
 (0)