Skip to content

Commit

Permalink
Add rule to check @dataProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
villfa authored Dec 7, 2022
1 parent 8313d41 commit 4c06b7e
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ services:
class: PHPStan\Rules\PHPUnit\CoversHelper
-
class: PHPStan\Rules\PHPUnit\AnnotationHelper
-
class: PHPStan\Rules\PHPUnit\DataProviderHelper

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,6 +8,10 @@ rules:
services:
- class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule
- class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule
-
class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule
arguments:
checkFunctionNameCase: %checkFunctionNameCase%
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule

Expand All @@ -16,6 +20,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\DataProviderDeclarationRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule:
Expand Down
90 changes: 90 additions & 0 deletions src/Rules/PHPUnit/DataProviderDeclarationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPUnit\Framework\TestCase;
use function array_merge;

/**
* @implements Rule<Node\Stmt\ClassMethod>
*/
class DataProviderDeclarationRule implements Rule
{

/**
* Data provider helper.
*
* @var DataProviderHelper
*/
private $dataProviderHelper;

/**
* The file type mapper.
*
* @var FileTypeMapper
*/
private $fileTypeMapper;

/**
* When set to true, it reports data provider method with incorrect name case.
*
* @var bool
*/
private $checkFunctionNameCase;

public function __construct(
DataProviderHelper $dataProviderHelper,
FileTypeMapper $fileTypeMapper,
bool $checkFunctionNameCase
)
{
$this->dataProviderHelper = $dataProviderHelper;
$this->fileTypeMapper = $fileTypeMapper;
$this->checkFunctionNameCase = $checkFunctionNameCase;
}

public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();

if ($classReflection === null || !$classReflection->isSubclassOf(TestCase::class)) {
return [];
}

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

$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$classReflection->getName(),
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$node->name->toString(),
$docComment->getText()
);

$annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc);

$errors = [];

foreach ($annotations as $annotation) {
$errors = array_merge(
$errors,
$this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase)
);
}

return $errors;
}

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

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function array_merge;
use function preg_match;
use function sprintf;

class DataProviderHelper
{

/**
* @return array<PhpDocTagNode>
*/
public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
{
if ($phpDoc === null) {
return [];
}

$phpDocNodes = $phpDoc->getPhpDocNodes();

$annotations = [];

foreach ($phpDocNodes as $docNode) {
$annotations = array_merge(
$annotations,
$docNode->getTagsByName('@dataProvider')
);
}

return $annotations;
}

/**
* @return RuleError[] errors
*/
public function processDataProvider(
Scope $scope,
PhpDocTagNode $phpDocTag,
bool $checkFunctionNameCase
): array
{
$dataProviderName = $this->getDataProviderName($phpDocTag);
if ($dataProviderName === null) {
// Missing name is already handled in NoMissingSpaceInMethodAnnotationRule
return [];
}

$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
// Should not happen
return [];
}

try {
$dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName);
} catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) {
$error = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method not found.',
$dataProviderName
))->build();

return [$error];
}

$errors = [];

if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method is used with incorrect case: %s.',
$dataProviderName,
$dataProviderMethodReflection->getName()
))->build();
}

if (!$dataProviderMethodReflection->isPublic()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method must be public.',
$dataProviderName
))->build();
}

return $errors;
}

private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string
{
if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
return null;
}

return $matches[0];
}

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

namespace PHPStan\Rules\PHPUnit;

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

/**
* @extends RuleTestCase<DataProviderDeclarationRule>
*/
class DataProviderDeclarationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new DataProviderDeclarationRule(
new DataProviderHelper(),
self::getContainer()->getByType(FileTypeMapper::class),
true
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [
[
'@dataProvider providebaz related method is used with incorrect case: provideBaz.',
13,
],
[
'@dataProvider provideQuux related method must be public.',
13,
],
[
'@dataProvider provideNonExisting related method not found.',
66,
],
]);
}

/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../extension.neon',
];
}
}
70 changes: 70 additions & 0 deletions tests/Rules/PHPUnit/data/data-provider-declaration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types=1);

namespace ExampleTestCase;

class FooTestCase extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideBar Comment.
* @dataProvider providebaz
* @dataProvider provideQux
* @dataProvider provideQuux
*/
public function testIsNotFoo(string $subject): void
{
self::assertNotSame('foo', $subject);
}

public static function provideBar(): iterable
{
return [
['bar'],
];
}

public static function provideBaz(): iterable
{
return [
['baz'],
];
}

public function provideQux(): iterable
{
return [
['qux'],
];
}

protected static function provideQuux(): iterable
{

return [
['quux'],
];
}
}

trait BarProvider
{
public static function provideCorge(): iterable
{
return [
['corge'],
];
}
}

class BarTestCase extends \PHPUnit\Framework\TestCase
{
use BarProvider;

/**
* @dataProvider provideNonExisting
* @dataProvider provideCorge
*/
public function testIsNotBar(string $subject): void
{
self::assertNotSame('bar', $subject);
}
}

0 comments on commit 4c06b7e

Please sign in to comment.