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..68ee7ec 100644 --- a/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php +++ b/src/Rules/PHPUnit/AttributeVersionRequirementHelper.php @@ -2,22 +2,18 @@ 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\InvalidVersionOperatorException; +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,74 +59,56 @@ 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) { 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(); + + continue; } - if ( - !is_numeric($versionRequirement) - ) { - if (!$this->bleedingEdge) { + if (!$requirement->isComplete()) { + if (!$this->warnAboutIncompleteVersion) { continue; } - $pharIoVersions = strpos($attr->getName(), 'RequiresPhpunit') !== false - ? $this->PHPUnitVersion->getPharIoVersions() - : $this->getAnalyzedPhpVersions(); - if ($pharIoVersions === []) { + if (!$this->PHPUnitVersion->warnsAboutIncompleteVersion()->yes()) { continue; } - try { - // check composer like version constraints, e.g. ^1 or ~2 - $testPhpVersionConstraint = $parser->parse($versionRequirement); - - foreach ($pharIoVersions 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 ($pharIoVersions 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.'), + sprintf('Version requirement is incomplete.'), ) ->identifier('phpunit.attributeRequiresPhpVersion') ->build(); @@ -138,28 +116,31 @@ 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; } /** - * @return Version[] + * @return list */ private function getAnalyzedPhpVersions(): array { @@ -172,7 +153,7 @@ private function getAnalyzedPhpVersions(): array $maxVersion, ); foreach ($minorVersionIterator as $phpstanVersion) { - $versions[] = new Version($phpstanVersion->getVersionString()); + $versions[] = $phpstanVersion->getVersionString(); } return $versions; } @@ -180,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; - } - } 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), ]; } 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, ], ]);