diff --git a/composer.json b/composer.json index 39d7a03..20a7855 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", + "phar-io/version": "^3.2", "phpstan/phpstan": "^2.1.32" }, "conflict": { diff --git a/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php b/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php index f106bf8..a03a7c8 100644 --- a/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php +++ b/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php @@ -2,16 +2,22 @@ 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\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\TestCase; use function count; use function is_numeric; use function method_exists; +use function preg_match; use function sprintf; +use function version_compare; /** * @implements Rule @@ -19,6 +25,10 @@ class AttributeRequiresPhpVersionRule implements Rule { + private const VERSION_COMPARISON = "/(?P!=|<|<=|<>|=|==|>|>=)?\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m"; + + private Version $phpstanPhpVersion; + private PHPUnitVersion $PHPUnitVersion; private TestMethodsHelper $testMethodsHelper; @@ -31,12 +41,15 @@ class AttributeRequiresPhpVersionRule implements Rule public function __construct( PHPUnitVersion $PHPUnitVersion, TestMethodsHelper $testMethodsHelper, - bool $deprecationRulesInstalled + bool $deprecationRulesInstalled, + PhpVersion $phpVersion ) { $this->PHPUnitVersion = $PHPUnitVersion; $this->testMethodsHelper = $testMethodsHelper; $this->deprecationRulesInstalled = $deprecationRulesInstalled; + + $this->phpstanPhpVersion = new Version($phpVersion->getVersionString()); } public function getNodeType(): string @@ -62,6 +75,7 @@ public function processNode(Node $node, Scope $scope): array } $errors = []; + $parser = new VersionConstraintParser(); foreach ($reflectionMethod->getAttributes('PHPUnit\Framework\Attributes\RequiresPhp') as $attr) { $args = $attr->getArguments(); if (count($args) !== 1) { @@ -71,6 +85,36 @@ public function processNode(Node $node, Scope $scope): array if ( !is_numeric($args[0]) ) { + try { + $testPhpVersionConstraint = $parser->parse($args[0]); + + if ($testPhpVersionConstraint->complies($this->phpstanPhpVersion)) { + continue; + } + } catch (UnsupportedVersionConstraintException $e) { + if (preg_match(self::VERSION_COMPARISON, $args[0], $matches) <= 0) { + $errors[] = RuleErrorBuilder::message( + sprintf($e->getMessage()), + ) + ->identifier('phpunit.attributeRequiresPhpVersion') + ->build(); + + continue; + } + + $operator = $matches['operator'] !== '' ? $matches['operator'] : '>='; + + if (version_compare($this->phpstanPhpVersion->getVersionString(), $matches['version'], $operator)) { + continue; + } + } + + $errors[] = RuleErrorBuilder::message( + sprintf('Version requirement will always evaluate to false.'), + ) + ->identifier('phpunit.attributeRequiresPhpVersion') + ->build(); + continue; } @@ -90,7 +134,6 @@ public function processNode(Node $node, Scope $scope): array ->identifier('phpunit.attributeRequiresPhpVersion') ->build(); } - } return $errors; diff --git a/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php index 92d9715..259dcd1 100644 --- a/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php +++ b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -12,6 +13,8 @@ final class AttributeRequiresPhpVersionRuleTest extends RuleTestCase { + private int $phpVersion = 80500; + private ?int $phpunitMajorVersion; private ?int $phpunitMinorVersion; @@ -78,6 +81,34 @@ public function testRuleOnPHPUnit13(): void ]); } + public function testPhpVersionMismatch(): void + { + $this->phpunitMajorVersion = 12; + $this->phpunitMinorVersion = 4; + $this->deprecationRulesInstalled = false; + + $this->analyse([__DIR__ . '/data/requires-php-version-mismatch.php'], [ + [ + 'Version requirement will always evaluate to false.', + 12, + ], + ]); + } + + public function testInvalidPhpVersion(): void + { + $this->phpunitMajorVersion = 12; + $this->phpunitMinorVersion = 4; + $this->deprecationRulesInstalled = false; + + $this->analyse([__DIR__ . '/data/requires-php-version-invalid.php'], [ + [ + 'Version constraint abc is not supported.', + 12, + ], + ]); + } + protected function getRule(): Rule { $phpunitVersion = new PHPUnitVersion($this->phpunitMajorVersion, $this->phpunitMinorVersion); @@ -89,6 +120,7 @@ protected function getRule(): Rule $phpunitVersion, ), $this->deprecationRulesInstalled, + new PhpVersion($this->phpVersion), ); } diff --git a/tests/Rules/PHPUnit/data/requires-php-version-invalid.php b/tests/Rules/PHPUnit/data/requires-php-version-invalid.php new file mode 100644 index 0000000..a3d57b4 --- /dev/null +++ b/tests/Rules/PHPUnit/data/requires-php-version-invalid.php @@ -0,0 +1,17 @@ +=8.0')] + public function testFoo(): void { + + } +}