From 4ba1489783ccb392bf35725a2bb4ee3dbf95220c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 7 Jan 2025 13:48:31 +0000 Subject: [PATCH] Skip parent type with required ctor --- .../Symfony/RequiredOnlyInAbstractRule.php | 54 ++++++++++++++++--- .../MongoDB/Repository/DocumentRepository.php | 11 ++++ .../Fixture/SkipParentDocumentRepository.php | 13 +++++ .../RequiredOnlyInAbstractRuleTest.php | 1 + 4 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 stubs/Doctrine/ODM/MongoDB/Repository/DocumentRepository.php create mode 100644 tests/Rules/Symfony/RequiredOnlyInAbstractRule/Fixture/SkipParentDocumentRepository.php diff --git a/src/Rules/Symfony/RequiredOnlyInAbstractRule.php b/src/Rules/Symfony/RequiredOnlyInAbstractRule.php index a68858073..5938068e2 100644 --- a/src/Rules/Symfony/RequiredOnlyInAbstractRule.php +++ b/src/Rules/Symfony/RequiredOnlyInAbstractRule.php @@ -7,6 +7,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier; @@ -15,31 +17,51 @@ /** * @see \Symplify\PHPStanRules\Tests\Rules\Symfony\RequiredOnlyInAbstractRule\RequiredOnlyInAbstractRuleTest * - * @implements Rule + * @implements Rule */ final class RequiredOnlyInAbstractRule implements Rule { /** * @var string */ - public const ERROR_MESSAGE = '#@required is reserved exclusively for abstract classes. For the rest of classes, use clean constructor injection'; + public const ERROR_MESSAGE = '#Symfony @required or #[Required] is reserved exclusively for abstract classes. For the rest of classes, use clean constructor injection'; + + /** + * Magic parent types that require constructor internally, + * so @required on final class is allowed + * + * @var string[] + */ + private const SKIPPED_PARENT_TYPES = [ + 'Doctrine\ODM\MongoDB\Repository\DocumentRepository', + ]; public function getNodeType(): string { - return Class_::class; + return InClassNode::class; } /** - * @param Class_ $node + * @param InClassNode $node */ public function processNode(Node $node, Scope $scope): array { - foreach ($node->getMethods() as $classMethod) { + $originalNode = $node->getOriginalNode(); + if (! $originalNode instanceof Class_) { + return []; + } + + if ($this->shouldSkipClass($scope)) { + return []; + } + + $class = $originalNode; + foreach ($class->getMethods() as $classMethod) { if (! SymfonyRequiredMethodAnalyzer::detect($classMethod)) { continue; } - if ($node->isAbstract()) { + if ($class->isAbstract()) { continue; } @@ -53,4 +75,24 @@ public function processNode(Node $node, Scope $scope): array return []; } + + private function shouldSkipClass(Scope $scope): bool + { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return false; + } + + if ($classReflection->isAbstract()) { + return true; + } + + foreach (self::SKIPPED_PARENT_TYPES as $skippedParentType) { + if ($classReflection->isSubclassOf($skippedParentType)) { + return true; + } + } + + return false; + } } diff --git a/stubs/Doctrine/ODM/MongoDB/Repository/DocumentRepository.php b/stubs/Doctrine/ODM/MongoDB/Repository/DocumentRepository.php new file mode 100644 index 000000000..f516fbe69 --- /dev/null +++ b/stubs/Doctrine/ODM/MongoDB/Repository/DocumentRepository.php @@ -0,0 +1,11 @@ +