Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ services:
class: PHPStan\Rules\PHPUnit\CoversHelper
-
class: PHPStan\Rules\PHPUnit\AnnotationHelper
-
class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector

-
class: PHPStan\Rules\PHPUnit\DataProviderHelper
factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create()
Expand Down
133 changes: 87 additions & 46 deletions src/Rules/PHPUnit/DataProviderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Comment\Doc;
use PhpParser\Modifiers;
use PhpParser\Node\Attribute;
use PhpParser\Node\Expr\ClassConstFetch;
Expand All @@ -19,6 +20,7 @@
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\FileTypeMapper;
use ReflectionMethod;
use function array_merge;
use function count;
use function explode;
Expand All @@ -28,16 +30,8 @@
class DataProviderHelper
{

/**
* Reflection provider.
*
*/
private ReflectionProvider $reflectionProvider;

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

private Parser $parser;
Expand All @@ -58,56 +52,23 @@
}

/**
* @param ReflectionMethod|ClassMethod $node
*
* @return iterable<array{ClassReflection|null, string, int}>
*/
public function getDataProviderMethods(
Scope $scope,
ClassMethod $node,
$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->getStartLine();

yield $dataProviderValue => $dataProviderMethod;
}
}
yield from $this->yieldDataProviderAnnotations($node, $scope, $classReflection);

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;
}
}
yield from $this->yieldDataProviderAttributes($node, $classReflection);
}

/**
Expand Down Expand Up @@ -306,4 +267,84 @@
];
}

/**
* @param ReflectionMethod|ClassMethod $node
*
* @return iterable<array{ClassReflection|null, string, int}>
*/
private function yieldDataProviderAttributes($node, ClassReflection $classReflection): iterable
{
if ($node instanceof ReflectionMethod) {
foreach ($node->getAttributes('PHPUnit\Framework\Attributes\DataProvider') as $attr) {

Check failure on line 278 in src/Rules/PHPUnit/DataProviderHelper.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, highest, ^9.5)

Call to an undefined method ReflectionMethod::getAttributes().

Check failure on line 278 in src/Rules/PHPUnit/DataProviderHelper.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, lowest, ^9.5)

Call to an undefined method ReflectionMethod::getAttributes().
$args = $attr->getArguments();
if (count($args) !== 1) {
continue;
}

$startLine = $node->getStartLine();
if ($startLine === false) {
$startLine = -1;
}

yield [$classReflection, $args[0], $startLine];
}

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;
}
}
}

/**
* @param ReflectionMethod|ClassMethod $node
*
* @return iterable<array{ClassReflection|null, string, int}>
*/
private function yieldDataProviderAnnotations($node, Scope $scope, ClassReflection $classReflection): iterable
{
$docComment = $node->getDocComment();
if ($docComment === null || $docComment === false) {
return;
}

$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$classReflection->getName(),
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$node instanceof ClassMethod ? $node->name->toString() : $node->getName(),
$docComment instanceof Doc ? $docComment->getText() : $docComment,
);
foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) {
$dataProviderValue = $this->getDataProviderAnnotationValue($annotation);
if ($dataProviderValue === null) {
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
continue;
}

$startLine = $node->getStartLine();
if ($startLine === false) {
$startLine = -1;
}

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

yield $dataProviderValue => $dataProviderMethod;
}
}

}
37 changes: 6 additions & 31 deletions src/Rules/PHPUnit/DataProviderHelperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@
use PHPStan\Parser\Parser;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\FileTypeMapper;
use PHPUnit\Framework\TestCase;
use function dirname;
use function explode;
use function file_get_contents;
use function is_file;
use function json_decode;

class DataProviderHelperFactory
{
Expand All @@ -21,43 +15,24 @@ class DataProviderHelperFactory

private Parser $parser;

private PHPUnitVersionDetector $PHPUnitVersionDetector;

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

public function create(): DataProviderHelper
{
$phpUnit10OrNewer = false;
if ($this->reflectionProvider->hasClass(TestCase::class)) {
$testCase = $this->reflectionProvider->getClass(TestCase::class);
$file = $testCase->getFileName();
if ($file !== null) {
$phpUnitRoot = dirname($file, 3);
$phpUnitComposer = $phpUnitRoot . '/composer.json';
if (is_file($phpUnitComposer)) {
$composerJson = @file_get_contents($phpUnitComposer);
if ($composerJson !== false) {
$json = json_decode($composerJson, true);
$version = $json['extra']['branch-alias']['dev-main'] ?? null;
if ($version !== null) {
$majorVersion = (int) explode('.', $version)[0];
if ($majorVersion >= 10) {
$phpUnit10OrNewer = true;
}
}
}
}
}
}

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

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

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Reflection\ReflectionProvider;
use PHPUnit\Framework\TestCase;
use function dirname;
use function explode;
use function file_get_contents;
use function is_file;
use function json_decode;

class PHPUnitVersionDetector
{

private ?bool $is10OrNewer = null;

private ReflectionProvider $reflectionProvider;

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

public function isPHPUnit10OrNewer(): bool
{
if ($this->is10OrNewer !== null) {
return $this->is10OrNewer;
}

$this->is10OrNewer = false;
if ($this->reflectionProvider->hasClass(TestCase::class)) {
$testCase = $this->reflectionProvider->getClass(TestCase::class);
$file = $testCase->getFileName();
if ($file !== null) {
$phpUnitRoot = dirname($file, 3);
$phpUnitComposer = $phpUnitRoot . '/composer.json';
if (is_file($phpUnitComposer)) {
$composerJson = @file_get_contents($phpUnitComposer);
if ($composerJson !== false) {
$json = json_decode($composerJson, true);
$version = $json['extra']['branch-alias']['dev-main'] ?? null;
if ($version !== null) {
$majorVersion = (int) explode('.', $version)[0];
if ($majorVersion >= 10) {
$this->is10OrNewer = true;
}
}
}
}
}
}

return $this->is10OrNewer;
}

}
Loading