diff --git a/config/static-rules.neon b/config/static-rules.neon index 3c33dfc0..d7690943 100644 --- a/config/static-rules.neon +++ b/config/static-rules.neon @@ -15,3 +15,6 @@ rules: - Symplify\PHPStanRules\Rules\NoReferenceRule - Symplify\PHPStanRules\Rules\ForbiddenStaticClassConstFetchRule - Symplify\PHPStanRules\Rules\Complexity\ForbiddenArrayMethodCallRule + + # docblock + - Symplify\PHPStanRules\Rules\NoMissnamedDocTagRule diff --git a/src/Enum/RuleIdentifier.php b/src/Enum/RuleIdentifier.php index 0cb7d91c..bfcf582e 100644 --- a/src/Enum/RuleIdentifier.php +++ b/src/Enum/RuleIdentifier.php @@ -79,4 +79,6 @@ final class RuleIdentifier public const RULE_IDENTIFIER = 'symplify.foreachCeption'; public const NO_MISSING_VARIABLE_DIM_FETCH = 'symplify.noMissingVariableDimFetch'; + + public const NO_MISSNAMED_DOC_TAG = 'symplify.noMissnamedDocTag'; } diff --git a/src/Rules/NoMissnamedDocTagRule.php b/src/Rules/NoMissnamedDocTagRule.php new file mode 100644 index 00000000..83e75082 --- /dev/null +++ b/src/Rules/NoMissnamedDocTagRule.php @@ -0,0 +1,103 @@ + + * + * @see \Symplify\PHPStanRules\Tests\Rules\NoMissnamedDocTagRule\NoMissnamedDocTagRuleTest + */ +final class NoMissnamedDocTagRule implements Rule +{ + /** + * @api used in tests + */ + public const CONSTANT_ERROR_MESSAGE = 'Constant doc comment tag must be @var, "%s" given'; + + /** + * @api used in tests + */ + public const PROPERTY_ERROR_MESSAGE = 'Property doc comment tag must be @var, "%s" given'; + + /** + * @api used in tests + */ + public const METHOD_ERROR_MESSAGE = 'Method doc comment tag must be @param or @return, "%s" given'; + + public function getNodeType(): string + { + return Class_::class; + } + + /** + * @param Class_ $node + * @return array + */ + public function processNode(Node $node, Scope $scope): array + { + $ruleErrors = []; + + foreach ($node->getMethods() as $classMethod) { + // match "@return" and "@param" tags + if ($classMethod->getDocComment() === null) { + continue; + } + + $matches = Strings::match($classMethod->getDocComment()->getText(), '#(@var)\b#mi'); + if ($matches === null) { + continue; + } + + $ruleErrors[] = RuleErrorBuilder::message(sprintf(self::METHOD_ERROR_MESSAGE, $matches[1])) + ->identifier(RuleIdentifier::NO_MISSNAMED_DOC_TAG) + ->line($classMethod->getStartLine()) + ->build(); + } + + foreach ($node->getProperties() as $property) { + // match "@return" and "@param" tags + if ($property->getDocComment() === null) { + continue; + } + + $matches = Strings::match($property->getDocComment()->getText(), '#(@param|@return)\b#mi'); + if ($matches === null) { + continue; + } + + $ruleErrors[] = RuleErrorBuilder::message(sprintf(self::PROPERTY_ERROR_MESSAGE, $matches[1])) + ->identifier(RuleIdentifier::NO_MISSNAMED_DOC_TAG) + ->line($property->getStartLine()) + ->build(); + } + + foreach ($node->getConstants() as $classConst) { + if ($classConst->getDocComment() === null) { + continue; + } + + $matches = Strings::match($classConst->getDocComment()->getText(), '#(@param|@return)\b#mi'); + if ($matches === null) { + continue; + } + + $ruleErrors[] = RuleErrorBuilder::message(sprintf(self::CONSTANT_ERROR_MESSAGE, $matches[1])) + ->identifier(RuleIdentifier::NO_MISSNAMED_DOC_TAG) + ->line($classConst->getStartLine()) + ->build(); + } + + return $ruleErrors; + } +} diff --git a/tests/Rules/NoMissnamedDocTagRule/Fixture/ClassMethodNonReturn.php b/tests/Rules/NoMissnamedDocTagRule/Fixture/ClassMethodNonReturn.php new file mode 100644 index 00000000..1e0f4d21 --- /dev/null +++ b/tests/Rules/NoMissnamedDocTagRule/Fixture/ClassMethodNonReturn.php @@ -0,0 +1,15 @@ +> $expectedErrorMessagesWithLines + */ + #[DataProvider('provideData')] + public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void + { + $this->analyse([$filePath], $expectedErrorMessagesWithLines); + } + + /** + * @return Iterator + */ + public static function provideData(): Iterator + { + yield [__DIR__ . '/Fixture/ClassMethodNonReturn.php', [ + [sprintf(NoMissnamedDocTagRule::METHOD_ERROR_MESSAGE, '@var'), 12], + ]]; + + yield [__DIR__ . '/Fixture/SomeClass.php', [ + [sprintf(NoMissnamedDocTagRule::PROPERTY_ERROR_MESSAGE, '@return'), 12], + ]]; + + yield [__DIR__ . '/Fixture/SomeConstant.php', [ + [sprintf(NoMissnamedDocTagRule::CONSTANT_ERROR_MESSAGE, '@return'), 12], + ]]; + + yield [__DIR__ . '/Fixture/SkipValidPropertyTag.php', []]; + } + + /** + * @return array + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(NoMissnamedDocTagRule::class); + } +} diff --git a/tests/Rules/NoMissnamedDocTagRule/config/configured_rule.neon b/tests/Rules/NoMissnamedDocTagRule/config/configured_rule.neon new file mode 100644 index 00000000..75104425 --- /dev/null +++ b/tests/Rules/NoMissnamedDocTagRule/config/configured_rule.neon @@ -0,0 +1,5 @@ +includes: + - ../../../config/included_services.neon + +rules: + - Symplify\PHPStanRules\Rules\NoMissnamedDocTagRule