From 2178b239c0289d336b77e6f1b249d0c03e387785 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 11 Jul 2026 11:33:54 +0200 Subject: [PATCH 1/3] Use sebastian/version-requirement --- composer.json | 5 +- .../AttributeVersionRequirementHelper.php | 50 +++++++------------ src/Rules/PHPUnit/PHPUnitVersion.php | 9 ++-- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/composer.json b/composer.json index 53028f0..774f44a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "require": { "php": "^7.4 || ^8.0", "phar-io/version": "^3.2", - "phpstan/phpstan": "^2.2.6" + "phpstan/phpstan": "^2.2.6", + "sebastian/version-requirement": "dev-main" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -19,7 +20,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^12", "shipmonk/name-collision-detector": "^2.1" }, "config": { diff --git a/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php b/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php index 7e72e44..5ddb08a 100644 --- a/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php +++ b/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php @@ -2,22 +2,19 @@ namespace PHPStan\Rules\PHPUnit; -use PharIo\Version\UnsupportedVersionConstraintException; -use PharIo\Version\Version; -use PharIo\Version\VersionConstraintParser; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflection\ReflectionAttribute; use PHPStan\Php\ConfiguredPhpVersionRangeHelper; use PHPStan\Php\PhpMinorVersionIterator; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; +use SebastianBergmann\VersionRequirement\InvalidVersionRequirementException; +use SebastianBergmann\VersionRequirement\Requirement; use function count; use function is_numeric; -use function preg_match; use function sprintf; use function strpos; use function substr_count; -use function version_compare; final class AttributeVersionRequirementHelper { @@ -63,7 +60,6 @@ public function __construct( public function checkVersionRequirement(array $attributes, Scope $scope): array { $errors = []; - $parser = new VersionConstraintParser(); foreach ($attributes as $attr) { $args = $attr->getArguments(); if (count($args) !== 1) { @@ -90,43 +86,31 @@ public function checkVersionRequirement(array $attributes, Scope $scope): array continue; } - $pharIoVersions = strpos($attr->getName(), 'RequiresPhpunit') !== false - ? $this->PHPUnitVersion->getPharIoVersions() + $versionStrings = strpos($attr->getName(), 'RequiresPhpunit') !== false + ? $this->PHPUnitVersion->getMinMaxVersion() : $this->getAnalyzedPhpVersions(); - if ($pharIoVersions === []) { + if ($versionStrings === []) { continue; } try { // check composer like version constraints, e.g. ^1 or ~2 - $testPhpVersionConstraint = $parser->parse($versionRequirement); + $requirement = Requirement::from($versionRequirement); - foreach ($pharIoVersions as $pharIoVersion) { - if ($testPhpVersionConstraint->complies($pharIoVersion)) { + foreach ($versionStrings as $versionString) { + if ($requirement->isSatisfiedBy($versionString)) { // 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'] : '>='; + } catch (InvalidVersionRequirementException $e) { + $errors[] = RuleErrorBuilder::message( + sprintf($e->getMessage()), + ) + ->identifier('phpunit.attributeRequiresPhpVersion') + ->build(); - foreach ($pharIoVersions as $pharIoVersion) { - if (version_compare($pharIoVersion->getVersionString(), $matches['version'], $operator)) { - // one of the versions within range matched, check next attribute - continue 2; - } - } + continue; } $errors[] = RuleErrorBuilder::message( @@ -159,7 +143,7 @@ public function checkVersionRequirement(array $attributes, Scope $scope): array } /** - * @return Version[] + * @return list */ private function getAnalyzedPhpVersions(): array { @@ -172,7 +156,7 @@ private function getAnalyzedPhpVersions(): array $maxVersion, ); foreach ($minorVersionIterator as $phpstanVersion) { - $versions[] = new Version($phpstanVersion->getVersionString()); + $versions[] = $phpstanVersion->getVersionString(); } return $versions; } diff --git a/src/Rules/PHPUnit/PHPUnitVersion.php b/src/Rules/PHPUnit/PHPUnitVersion.php index e6176b0..8789395 100644 --- a/src/Rules/PHPUnit/PHPUnitVersion.php +++ b/src/Rules/PHPUnit/PHPUnitVersion.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\PHPUnit; -use PharIo\Version\Version; use PHPStan\TrinaryLogic; use function sprintf; @@ -20,17 +19,17 @@ public function __construct(?int $majorVersion, ?int $minorVersion) } /** - * @return array{}|array{Version, Version} + * @return array{}|array{string, string} */ - public function getPharIoVersions(): array + public function getMinMaxVersion(): array { if ($this->majorVersion === null || $this->minorVersion === null) { return []; } return [ - new Version(sprintf('%d.%d.0', $this->majorVersion, $this->minorVersion)), - new Version(sprintf('%d.%d.99', $this->majorVersion, $this->minorVersion)), + sprintf('%d.%d.0', $this->majorVersion, $this->minorVersion), + sprintf('%d.%d.99', $this->majorVersion, $this->minorVersion), ]; } From d441e937a9e7a25b16e6ea63c16e603c7cb639a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 11 Jul 2026 12:02:47 +0200 Subject: [PATCH 2/3] adjust expectations --- tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php index 548ad99..57ee919 100644 --- a/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php +++ b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php @@ -133,7 +133,7 @@ public function testInvalidPhpVersion(): void $this->analyse([__DIR__ . '/data/requires-php-version-invalid.php'], [ [ - 'Version constraint abc is not supported.', + '"abc" is not a valid version requirement: expected a version constraint (such as "^8.1", "~8.1.0", or "8.1.*") or a version comparison (such as ">= 8.1.0")', 12, ], ]); From 3eefc863e829292df6b6618a526b7ed9e8b7e356 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 11 Jul 2026 12:40:52 +0200 Subject: [PATCH 3/3] rework --- .../AttributeVersionRequirementHelper.php | 117 +++++++----------- 1 file changed, 48 insertions(+), 69 deletions(-) diff --git a/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php b/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php index 5ddb08a..68ee7ec 100644 --- a/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php +++ b/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php @@ -8,13 +8,12 @@ use PHPStan\Php\PhpMinorVersionIterator; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; +use SebastianBergmann\VersionRequirement\InvalidVersionOperatorException; use SebastianBergmann\VersionRequirement\InvalidVersionRequirementException; use SebastianBergmann\VersionRequirement\Requirement; use function count; -use function is_numeric; use function sprintf; use function strpos; -use function substr_count; final class AttributeVersionRequirementHelper { @@ -66,55 +65,50 @@ public function checkVersionRequirement(array $attributes, Scope $scope): array 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)) { + try { + $requirement = Requirement::from($versionRequirement); + } catch (InvalidVersionOperatorException $e) { + 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(); + } + + continue; + } catch (InvalidVersionRequirementException $e) { $errors[] = RuleErrorBuilder::message( - sprintf('Version requirement is incomplete.'), + sprintf($e->getMessage()), ) ->identifier('phpunit.attributeRequiresPhpVersion') ->build(); - } - if ( - !is_numeric($versionRequirement) - ) { - if (!$this->bleedingEdge) { - continue; - } + continue; + } - $versionStrings = strpos($attr->getName(), 'RequiresPhpunit') !== false - ? $this->PHPUnitVersion->getMinMaxVersion() - : $this->getAnalyzedPhpVersions(); - if ($versionStrings === []) { + if (!$requirement->isComplete()) { + if (!$this->warnAboutIncompleteVersion) { continue; } - try { - // check composer like version constraints, e.g. ^1 or ~2 - $requirement = Requirement::from($versionRequirement); - - foreach ($versionStrings as $versionString) { - if ($requirement->isSatisfiedBy($versionString)) { - // one of the versions within range matched, check next attribute - continue 2; - } - } - } catch (InvalidVersionRequirementException $e) { - $errors[] = RuleErrorBuilder::message( - sprintf($e->getMessage()), - ) - ->identifier('phpunit.attributeRequiresPhpVersion') - ->build(); - + if (!$this->PHPUnitVersion->warnsAboutIncompleteVersion()->yes()) { continue; } $errors[] = RuleErrorBuilder::message( - sprintf('Version requirement will always evaluate to false.'), + sprintf('Version requirement is incomplete.'), ) ->identifier('phpunit.attributeRequiresPhpVersion') ->build(); @@ -122,23 +116,26 @@ public function checkVersionRequirement(array $attributes, Scope $scope): array 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(); + $versionStrings = strpos($attr->getName(), 'RequiresPhpunit') !== false + ? $this->PHPUnitVersion->getMinMaxVersion() + : $this->getAnalyzedPhpVersions(); + if ($versionStrings === []) { + continue; + } + foreach ($versionStrings as $versionString) { + if ($requirement->isSatisfiedBy($versionString)) { + // 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(); } + return $errors; } @@ -164,22 +161,4 @@ private function getAnalyzedPhpVersions(): array return []; } - // 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; - } - }