diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 8cfc08ca5b..e6868053eb 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -17,3 +17,4 @@ parameters: readOnlyByPhpDoc: true phpDocParserRequireWhitespaceBeforeDescription: true runtimeReflectionRules: true + notAnalysedTrait: true diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 5464be9e01..e5445990a6 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -22,6 +22,12 @@ rules: conditionalTags: PHPStan\Rules\Comparison\ConstantLooseComparisonRule: phpstan.rules.rule: %featureToggles.looseComparison% + PHPStan\Rules\Traits\TraitDeclarationCollector: + phpstan.collector: %featureToggles.notAnalysedTrait% + PHPStan\Rules\Traits\TraitUseCollector: + phpstan.collector: %featureToggles.notAnalysedTrait% + PHPStan\Rules\Traits\NotAnalysedTraitRule: + phpstan.rules.rule: %featureToggles.notAnalysedTrait% parameters: checkAdvancedIsset: true @@ -175,3 +181,12 @@ services: class: PHPStan\Rules\Properties\NullsafePropertyFetchRule tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Traits\TraitDeclarationCollector + + - + class: PHPStan\Rules\Traits\TraitUseCollector + + - + class: PHPStan\Rules\Traits\NotAnalysedTraitRule diff --git a/conf/config.neon b/conf/config.neon index 1956ad22b2..38f8410ca3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -47,6 +47,7 @@ parameters: readOnlyByPhpDoc: false phpDocParserRequireWhitespaceBeforeDescription: false runtimeReflectionRules: false + notAnalysedTrait: false fileExtensions: - php checkAdvancedIsset: false @@ -251,6 +252,7 @@ parametersSchema: readOnlyByPhpDoc: bool() phpDocParserRequireWhitespaceBeforeDescription: bool() runtimeReflectionRules: bool() + notAnalysedTrait: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Traits/NotAnalysedTraitRule.php b/src/Rules/Traits/NotAnalysedTraitRule.php new file mode 100644 index 0000000000..4a0bb03ecc --- /dev/null +++ b/src/Rules/Traits/NotAnalysedTraitRule.php @@ -0,0 +1,55 @@ + + */ +class NotAnalysedTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitDeclarationData = $node->get(TraitDeclarationCollector::class); + $traitUseData = $node->get(TraitUseCollector::class); + + $declaredTraits = []; + foreach ($traitDeclarationData as $file => $declaration) { + foreach ($declaration as [$name, $line]) { + $declaredTraits[strtolower($name)] = [$file, $name, $line]; + } + } + + foreach ($traitUseData as $usedNamesData) { + foreach ($usedNamesData as $usedNames) { + foreach ($usedNames as $usedName) { + unset($declaredTraits[strtolower($usedName)]); + } + } + } + + $errors = []; + foreach ($declaredTraits as [$file, $name, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Trait %s is used zero times and is not analysed.', + $name, + ))->file($file)->line($line)->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Traits/TraitDeclarationCollector.php b/src/Rules/Traits/TraitDeclarationCollector.php new file mode 100644 index 0000000000..53277f4eda --- /dev/null +++ b/src/Rules/Traits/TraitDeclarationCollector.php @@ -0,0 +1,29 @@ + + */ +class TraitDeclarationCollector implements Collector +{ + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope) + { + if ($node->namespacedName === null) { + return null; + } + + return [$node->namespacedName->toString(), $node->getLine()]; + } + +} diff --git a/src/Rules/Traits/TraitUseCollector.php b/src/Rules/Traits/TraitUseCollector.php new file mode 100644 index 0000000000..31dc36e183 --- /dev/null +++ b/src/Rules/Traits/TraitUseCollector.php @@ -0,0 +1,26 @@ +> + */ +class TraitUseCollector implements Collector +{ + + public function getNodeType(): string + { + return Node\Stmt\TraitUse::class; + } + + public function processNode(Node $node, Scope $scope) + { + return array_map(static fn (Node\Name $traitName) => $traitName->toString(), $node->traits); + } + +} diff --git a/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php b/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php new file mode 100644 index 0000000000..8400de7b83 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/NotAnalysedTraitRuleTest.php @@ -0,0 +1,37 @@ + + */ +class NotAnalysedTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NotAnalysedTraitRule(); + } + + protected function getCollectors(): array + { + return [ + new TraitDeclarationCollector(), + new TraitUseCollector(), + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/not-analysed-trait.php'], [ + [ + 'Trait NotAnalysedTrait\Bar is used zero times and is not analysed.', + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php b/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php new file mode 100644 index 0000000000..f84bc5f44f --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/not-analysed-trait.php @@ -0,0 +1,20 @@ +