diff --git a/extension.neon b/extension.neon index 28900524..70831227 100644 --- a/extension.neon +++ b/extension.neon @@ -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() diff --git a/phpstan.neon b/phpstan.neon index 57379453..7f64d1c0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,8 @@ includes: - phpstan-baseline.neon parameters: + reportUnmatchedIgnoredErrors: false + excludePaths: - tests/*/data/* ignoreErrors: diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 5f65656f..64137c76 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PhpParser\Comment\Doc; use PhpParser\Modifiers; use PhpParser\Node\Attribute; use PhpParser\Node\Expr\ClassConstFetch; @@ -19,25 +20,19 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; +use ReflectionMethod; use function array_merge; use function count; use function explode; +use function method_exists; use function preg_match; use function sprintf; class DataProviderHelper { - /** - * Reflection provider. - * - */ private ReflectionProvider $reflectionProvider; - /** - * The file type mapper. - * - */ private FileTypeMapper $fileTypeMapper; private Parser $parser; @@ -58,56 +53,23 @@ public function __construct( } /** + * @param ReflectionMethod|ClassMethod $node + * * @return iterable */ 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); } /** @@ -306,4 +268,91 @@ private function parseDataProviderAttribute(Attribute $attribute, ClassReflectio ]; } + /** + * @param ReflectionMethod|ClassMethod $node + * + * @return iterable + */ + private function yieldDataProviderAttributes($node, ClassReflection $classReflection): iterable + { + if ( + $node instanceof ReflectionMethod + ) { + /** @phpstan-ignore function.alreadyNarrowedType */ + if (!method_exists($node, 'getAttributes')) { + return; + } + + foreach ($node->getAttributes('PHPUnit\Framework\Attributes\DataProvider') as $attr) { + $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 + */ + 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; + } + } + } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index a0768c8a..23a5f34a 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -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 { @@ -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()); } } diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php new file mode 100644 index 00000000..841e69b9 --- /dev/null +++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php @@ -0,0 +1,57 @@ +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; + } + +}