Skip to content

Commit

Permalink
Add rule that checks for invalid and unrecognized annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
PrinsFrank authored Oct 26, 2022
1 parent 68017cc commit 9b88cef
Show file tree
Hide file tree
Showing 9 changed files with 467 additions and 0 deletions.
2 changes: 2 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ services:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\Rules\PHPUnit\CoversHelper
-
class: PHPStan\Rules\PHPUnit\AnnotationHelper

conditionalTags:
PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension:
Expand Down
6 changes: 6 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ rules:
services:
- class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule
- class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule

conditionalTags:
PHPStan\Rules\PHPUnit\ClassCoversExistsRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
66 changes: 66 additions & 0 deletions src/Rules/PHPUnit/AnnotationHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Comment\Doc;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function array_key_exists;
use function in_array;
use function preg_match;
use function preg_split;

class AnnotationHelper
{

private const ANNOTATIONS_WITH_PARAMS = [
'backupGlobals',
'backupStaticAttributes',
'covers',
'coversDefaultClass',
'dataProvider',
'depends',
'group',
'preserveGlobalState',
'requires',
'testDox',
'testWith',
'ticket',
'uses',
];

/**
* @return RuleError[] errors
*/
public function processDocComment(Doc $docComment): array
{
$errors = [];
$docCommentLines = preg_split("/((\r?\n)|(\r\n?))/", $docComment->getText());
if ($docCommentLines === false) {
return [];
}

foreach ($docCommentLines as $docCommentLine) {
// These annotations can't be retrieved using the getResolvedPhpDoc method on the FileTypeMapper as they are not present when they are invalid
$annotation = preg_match('/(?<annotation>@(?<property>[a-zA-Z]+)(?<whitespace>\s*)(?<value>.*))/', $docCommentLine, $matches);
if ($annotation === false) {
continue; // Line without annotation
}

if (array_key_exists('property', $matches) === false || array_key_exists('whitespace', $matches) === false || array_key_exists('annotation', $matches) === false) {
continue;
}

if (!in_array($matches['property'], self::ANNOTATIONS_WITH_PARAMS, true) || $matches['whitespace'] !== '') {
continue;
}

$errors[] = RuleErrorBuilder::message(
'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.'
)->build();
}

return $errors;
}

}
49 changes: 49 additions & 0 deletions src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPUnit\Framework\TestCase;

/**
* @implements Rule<InClassNode>
*/
class NoMissingSpaceInClassAnnotationRule implements Rule
{

/**
* Covers helper.
*
* @var AnnotationHelper
*/
private $annotationHelper;

public function __construct(AnnotationHelper $annotationHelper)
{
$this->annotationHelper = $annotationHelper;
}

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) {
return [];
}

$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

return $this->annotationHelper->processDocComment($docComment);
}

}
49 changes: 49 additions & 0 deletions src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Rules\Rule;
use PHPUnit\Framework\TestCase;

/**
* @implements Rule<InClassMethodNode>
*/
class NoMissingSpaceInMethodAnnotationRule implements Rule
{

/**
* Covers helper.
*
* @var AnnotationHelper
*/
private $annotationHelper;

public function __construct(AnnotationHelper $annotationHelper)
{
$this->annotationHelper = $annotationHelper;
}

public function getNodeType(): string
{
return InClassMethodNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) {
return [];
}

$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

return $this->annotationHelper->processDocComment($docComment);
}

}
87 changes: 87 additions & 0 deletions tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<NoMissingSpaceInClassAnnotationRule>
*/
class NoMissingSpaceInClassAnnotationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new NoMissingSpaceInClassAnnotationRule(new AnnotationHelper());
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/InvalidClassCoversAnnotation.php'], [
[
'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.',
36,
],
[
'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.',
36,
],
[
'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.',
36,
],
[
'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.',
36,
],
[
'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.',
36,
],
[
'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.',
36,
],
[
'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.',
36,
],
[
'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.',
36,
],
[
'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.',
36,
],
[
'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.',
36,
],
[
'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.',
36,
],
[
'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.',
36,
],
[
'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.',
36,
],
]);
}

/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../extension.neon',
];
}

}
87 changes: 87 additions & 0 deletions tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<NoMissingSpaceInMethodAnnotationRule>
*/
class NoMissingSpaceInMethodAnnotationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new NoMissingSpaceInMethodAnnotationRule(new AnnotationHelper());
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/InvalidMethodCoversAnnotation.php'], [
[
'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.',
12,
],
[
'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.',
19,
],
[
'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.',
27,
],
[
'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.',
27,
],
[
'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.',
33,
],
[
'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.',
39,
],
[
'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.',
45,
],
[
'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.',
52,
],
[
'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.',
58,
],
[
'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.',
64,
],
[
'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.',
70,
],
[
'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.',
76,
],
[
'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.',
82,
],
]);
}

/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../extension.neon',
];
}

}
Loading

0 comments on commit 9b88cef

Please sign in to comment.