Skip to content

Commit

Permalink
Add support for data provider attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
villfa authored and ondrejmirtes committed Dec 21, 2022
1 parent 7f7b59b commit 54a24bd
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 57 deletions.
33 changes: 5 additions & 28 deletions src/Rules/PHPUnit/DataProviderDeclarationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPUnit\Framework\TestCase;
use function array_merge;

Expand All @@ -22,13 +21,6 @@ class DataProviderDeclarationRule implements Rule
*/
private $dataProviderHelper;

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

/**
* When set to true, it reports data provider method with incorrect name case.
*
Expand All @@ -45,13 +37,11 @@ class DataProviderDeclarationRule implements Rule

public function __construct(
DataProviderHelper $dataProviderHelper,
FileTypeMapper $fileTypeMapper,
bool $checkFunctionNameCase,
bool $deprecationRulesInstalled
)
{
$this->dataProviderHelper = $dataProviderHelper;
$this->fileTypeMapper = $fileTypeMapper;
$this->checkFunctionNameCase = $checkFunctionNameCase;
$this->deprecationRulesInstalled = $deprecationRulesInstalled;
}
Expand All @@ -69,29 +59,16 @@ public function processNode(Node $node, Scope $scope): array
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) {
foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $node, $classReflection) as $dataProviderValue => [$dataProviderClassReflection, $dataProviderMethodName, $lineNumber]) {
$errors = array_merge(
$errors,
$this->dataProviderHelper->processDataProvider(
$scope,
$annotation,
$dataProviderValue,
$dataProviderClassReflection,
$dataProviderMethodName,
$lineNumber,
$this->checkFunctionNameCase,
$this->deprecationRulesInstalled
)
Expand Down
164 changes: 144 additions & 20 deletions src/Rules/PHPUnit/DataProviderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node\Attribute;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
Expand All @@ -10,6 +15,7 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
use function array_merge;
use function count;
use function explode;
Expand All @@ -26,19 +32,84 @@ class DataProviderHelper
*/
private $reflectionProvider;

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

/** @var bool */
private $phpunit10OrNewer;

public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer)
public function __construct(
ReflectionProvider $reflectionProvider,
FileTypeMapper $fileTypeMapper,
bool $phpunit10OrNewer
)
{
$this->reflectionProvider = $reflectionProvider;
$this->fileTypeMapper = $fileTypeMapper;
$this->phpunit10OrNewer = $phpunit10OrNewer;
}

/**
* @return iterable<array{ClassReflection|null, string, int}>
*/
public function getDataProviderMethods(
Scope $scope,
ClassMethod $node,
ClassReflection $classReflection
): iterable
{
$docComment = $node->getDocComment();
if ($docComment !== null) {
$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$classReflection->getName(),
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$node->name->toString(),
$docComment->getText()
);
foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) {
$dataProviderValue = $this->getDataProviderAnnotationValue($annotation);
if ($dataProviderValue === null) {
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
continue;
}

$dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue);
$dataProviderMethod[] = $node->getLine();

yield $dataProviderValue => $dataProviderMethod;
}
}

if (!$this->phpunit10OrNewer) {
return;
}

foreach ($node->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
$dataProviderMethod = null;
if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') {
$dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection);
} elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') {
$dataProviderMethod = $this->parseDataProviderExternalAttribute($attr);
}
if ($dataProviderMethod === null) {
continue;
}

yield from $dataProviderMethod;
}
}
}

/**
* @return array<PhpDocTagNode>
*/
public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
{
if ($phpDoc === null) {
return [];
Expand All @@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
* @return RuleError[] errors
*/
public function processDataProvider(
Scope $scope,
PhpDocTagNode $phpDocTag,
string $dataProviderValue,
?ClassReflection $classReflection,
string $methodName,
int $lineNumber,
bool $checkFunctionNameCase,
bool $deprecationRulesInstalled
): array
{
$dataProviderValue = $this->getDataProviderValue($phpDocTag);
if ($dataProviderValue === null) {
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
return [];
}

[$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue);
if ($classReflection === null) {
$error = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related class not found.',
$dataProviderValue
))->build();
))->line($lineNumber)->build();

return [$error];
}

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

return [$error];
}

$errors = [];

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

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

if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method must be static in PHPUnit 10 and newer.',
$dataProviderValue
))->build();
))->line($lineNumber)->build();
}

return $errors;
}

private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
private function getDataProviderAnnotationValue(PhpDocTagNode $phpDocTag): ?string
{
if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
return null;
Expand All @@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
/**
* @return array{ClassReflection|null, string}
*/
private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array
private function parseDataProviderAnnotationValue(Scope $scope, string $dataProviderValue): array
{
$parts = explode('::', $dataProviderValue, 2);
if (count($parts) <= 1) {
Expand All @@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue)
return [null, $dataProviderValue];
}

/**
* @return array<string, array{(ClassReflection|null), string, int}>|null
*/
private function parseDataProviderExternalAttribute(Attribute $attribute): ?array
{
if (count($attribute->args) !== 2) {
return null;
}
$methodNameArg = $attribute->args[1]->value;
if (!$methodNameArg instanceof String_) {
return null;
}
$classNameArg = $attribute->args[0]->value;
if ($classNameArg instanceof ClassConstFetch && $classNameArg->class instanceof Name) {
$className = $classNameArg->class->toString();
} elseif ($classNameArg instanceof String_) {
$className = $classNameArg->value;
} else {
return null;
}

$dataProviderClassReflection = null;
if ($this->reflectionProvider->hasClass($className)) {
$dataProviderClassReflection = $this->reflectionProvider->getClass($className);
$className = $dataProviderClassReflection->getName();
}

return [
sprintf('%s::%s', $className, $methodNameArg->value) => [
$dataProviderClassReflection,
$methodNameArg->value,
$attribute->getLine(),
],
];
}

/**
* @return array<string, array{(ClassReflection|null), string, int}>|null
*/
private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array
{
if (count($attribute->args) !== 1) {
return null;
}
$methodNameArg = $attribute->args[0]->value;
if (!$methodNameArg instanceof String_) {
return null;
}

return [
$methodNameArg->value => [
$classReflection,
$methodNameArg->value,
$attribute->getLine(),
],
];
}

}
9 changes: 7 additions & 2 deletions src/Rules/PHPUnit/DataProviderHelperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\PHPUnit;

use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\FileTypeMapper;
use PHPUnit\Framework\TestCase;
use function dirname;
use function explode;
Expand All @@ -16,9 +17,13 @@ class DataProviderHelperFactory
/** @var ReflectionProvider */
private $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
/** @var FileTypeMapper */
private $fileTypeMapper;

public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper)
{
$this->reflectionProvider = $reflectionProvider;
$this->fileTypeMapper = $fileTypeMapper;
}

public function create(): DataProviderHelper
Expand Down Expand Up @@ -46,7 +51,7 @@ public function create(): DataProviderHelper
}
}

return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer);
return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $phpUnit10OrNewer);
}

}
Loading

0 comments on commit 54a24bd

Please sign in to comment.