Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ services:
-
class: PHPStan\Type\PHPUnit\DynamicCallToAssertionIgnoreExtension

-
class: PHPStan\Rules\PHPUnit\AttributeVersionRequirementHelper
arguments:
deprecationRulesInstalled: %deprecationRulesInstalled%
bleedingEdge: %featureToggles.bleedingEdge%

conditionalTags:
PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension:
phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType%
Expand Down
3 changes: 0 additions & 3 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ services:

-
class: PHPStan\Rules\PHPUnit\AttributeRequiresPhpVersionRule
arguments:
deprecationRulesInstalled: %deprecationRulesInstalled%
bleedingEdge: %featureToggles.bleedingEdge%
tags:
- phpstan.rules.rule

Expand Down
187 changes: 7 additions & 180 deletions src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,29 @@

namespace PHPStan\Rules\PHPUnit;

use PharIo\Version\UnsupportedVersionConstraintException;
use PharIo\Version\Version;
use PharIo\Version\VersionConstraintParser;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Php\PhpMinorVersionIterator;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\IntegerRangeType;
use PHPUnit\Framework\TestCase;
use function count;
use function is_numeric;
use function preg_match;
use function sprintf;
use function substr_count;
use function version_compare;

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

private const VERSION_COMPARISON = "/(?P<operator>!=|<|<=|<>|=|==|>|>=)?\s*(?P<version>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m";

private PHPUnitVersion $PHPUnitVersion;

private TestMethodsHelper $testMethodsHelper;

private PhpVersion $fallbackPhpVersion;

/**
* When phpstan-deprecation-rules is installed, rule reports deprecated usages.
*/
private bool $deprecationRulesInstalled;

/**
* Whether warnings about incomplete versions are allowed to be emitted
*/
private bool $warnAboutIncompleteVersion;

private bool $bleedingEdge;
private AttributeVersionRequirementHelper $attributeVersionRequirementHelper;

public function __construct(
PHPUnitVersion $PHPUnitVersion,
TestMethodsHelper $testMethodsHelper,
bool $deprecationRulesInstalled,
PhpVersion $phpVersion,
bool $bleedingEdge,
bool $warnAboutIncompleteVersion = true
AttributeVersionRequirementHelper $attributeVersionRequirementHelper
)
{
$this->PHPUnitVersion = $PHPUnitVersion;
$this->testMethodsHelper = $testMethodsHelper;
$this->deprecationRulesInstalled = $deprecationRulesInstalled;
$this->fallbackPhpVersion = $phpVersion;
$this->warnAboutIncompleteVersion = $warnAboutIncompleteVersion;
$this->bleedingEdge = $bleedingEdge;
$this->attributeVersionRequirementHelper = $attributeVersionRequirementHelper;
}

public function getNodeType(): string
Expand All @@ -82,145 +44,10 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$phpstanPharIoVersions = $this->getAnalyzedPhpVersions($scope);
if ($phpstanPharIoVersions === []) {
return [];
}

$errors = [];
$parser = new VersionConstraintParser();
foreach ($reflectionMethod->getAttributesByName('PHPUnit\Framework\Attributes\RequiresPhp') as $attr) {
$args = $attr->getArguments();
if (count($args) !== 1) {
continue;
}

// the following block is mimicing PHPUnit version parsing
// see https://github.com/sebastianbergmann/phpunit/blob/43c2cd7b96ee1e800b35e4df23b419a88b53111d/src/Metadata/Version/Requirement.php

$versionRequirement = $args[0];

if ($this->warnAboutIncompleteVersion($versionRequirement)) {
$errors[] = RuleErrorBuilder::message(
sprintf('Version requirement is incomplete.'),
)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();
}

if (
!is_numeric($versionRequirement)
) {
if (!$this->bleedingEdge) {
continue;
}

try {
// check composer like version constraints, e.g. ^1 or ~2
$testPhpVersionConstraint = $parser->parse($versionRequirement);

foreach ($phpstanPharIoVersions as $pharIoVersion) {
if ($testPhpVersionConstraint->complies($pharIoVersion)) {
// one of the versions within range matched, check next attribute
continue 2;
}
}
} catch (UnsupportedVersionConstraintException $e) {
// test php-src builtin operators as in version_compare()
if (preg_match(self::VERSION_COMPARISON, $versionRequirement, $matches) <= 0) {
$errors[] = RuleErrorBuilder::message(
sprintf($e->getMessage()),
)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();

continue;
}

$operator = $matches['operator'] !== '' ? $matches['operator'] : '>=';

foreach ($phpstanPharIoVersions as $pharIoVersion) {
if (version_compare($pharIoVersion->getVersionString(), $matches['version'], $operator)) {
// one of the versions within range matched, check next attribute
continue 2;
}
}
}

$errors[] = RuleErrorBuilder::message(
sprintf('Version requirement will always evaluate to false.'),
)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();

continue;
}

if ($this->PHPUnitVersion->requiresPhpversionAttributeWithOperator()->yes()) {
$errors[] = RuleErrorBuilder::message(
sprintf('Version requirement is missing operator.'),
)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();
} elseif (
$this->deprecationRulesInstalled
&& $this->PHPUnitVersion->deprecatesPhpversionAttributeWithoutOperator()->yes()
) {
$errors[] = RuleErrorBuilder::message(
sprintf('Version requirement without operator is deprecated.'),
)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();
}
}

return $errors;
}

/**
* @return Version[]
*/
private function getAnalyzedPhpVersions(Scope $scope): array
{
$scopePhpVersion = $scope->getPhpVersion()->getType();
if ($scopePhpVersion instanceof ConstantIntegerType) {
$v = new PhpVersion($scopePhpVersion->getValue());
return [new Version($v->getVersionString())];
} elseif ($scopePhpVersion instanceof IntegerRangeType) {
if ($scopePhpVersion->getMin() === null || $scopePhpVersion->getMax() === null) {
return [];
}

$versions = [];
$minorVersionIterator = new PhpMinorVersionIterator(
new PhpVersion($scopePhpVersion->getMin()),
new PhpVersion($scopePhpVersion->getMax()),
);
foreach ($minorVersionIterator as $phpstanVersion) {
$versions[] = new Version($phpstanVersion->getVersionString());
}
return $versions;
}

return [new Version($this->fallbackPhpVersion->getVersionString())];
}

// see https://github.com/sebastianbergmann/phpunit/issues/6451
private function warnAboutIncompleteVersion(string $versionRequirement): bool
{
if (!$this->bleedingEdge) {
return false;
}

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

if (!$this->PHPUnitVersion->warnsAboutIncompleteVersion()->yes()) {
return false;
}

return substr_count($versionRequirement, '.') !== 2;
return $this->attributeVersionRequirementHelper->checkRequiresPhpVersion(
$reflectionMethod->getAttributesByName('PHPUnit\Framework\Attributes\RequiresPhp'),
$scope,
);
}

}
Loading
Loading