Skip to content

Commit 9461c7e

Browse files
committed
Sanity check #[RequiresPhp] value and range
1 parent cda423c commit 9461c7e

File tree

5 files changed

+119
-13
lines changed

5 files changed

+119
-13
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
],
88
"require": {
99
"php": "^7.4 || ^8.0",
10+
"composer/semver": "^3.4",
1011
"phpstan/phpstan": "^2.1.32"
1112
},
1213
"conflict": {

src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
namespace PHPStan\Rules\PHPUnit;
44

5+
use Composer\Semver\Constraint\ConstraintInterface;
6+
use Composer\Semver\VersionParser;
57
use PhpParser\Node;
68
use PHPStan\Analyser\Scope;
79
use PHPStan\Node\InClassMethodNode;
10+
use PHPStan\Php\PhpVersion;
811
use PHPStan\Rules\Rule;
912
use PHPStan\Rules\RuleErrorBuilder;
1013
use PHPUnit\Framework\TestCase;
14+
use UnexpectedValueException;
1115
use function count;
1216
use function is_numeric;
1317
use function method_exists;
@@ -19,6 +23,8 @@
1923
class AttributeRequiresPhpVersionRule implements Rule
2024
{
2125

26+
private ConstraintInterface $phpstanVersionConstraint;
27+
2228
private PHPUnitVersion $PHPUnitVersion;
2329

2430
private TestMethodsHelper $testMethodsHelper;
@@ -31,12 +37,16 @@ class AttributeRequiresPhpVersionRule implements Rule
3137
public function __construct(
3238
PHPUnitVersion $PHPUnitVersion,
3339
TestMethodsHelper $testMethodsHelper,
34-
bool $deprecationRulesInstalled
40+
bool $deprecationRulesInstalled,
41+
PhpVersion $phpVersion
3542
)
3643
{
3744
$this->PHPUnitVersion = $PHPUnitVersion;
3845
$this->testMethodsHelper = $testMethodsHelper;
3946
$this->deprecationRulesInstalled = $deprecationRulesInstalled;
47+
48+
$parser = new VersionParser();
49+
$this->phpstanVersionConstraint = $parser->parseConstraints($phpVersion->getVersionString());
4050
}
4151

4252
public function getNodeType(): string
@@ -62,35 +72,57 @@ public function processNode(Node $node, Scope $scope): array
6272
}
6373

6474
$errors = [];
75+
$parser = new VersionParser();
6576
foreach ($reflectionMethod->getAttributes('PHPUnit\Framework\Attributes\RequiresPhp') as $attr) {
6677
$args = $attr->getArguments();
6778
if (count($args) !== 1) {
6879
continue;
6980
}
7081

7182
if (
72-
!is_numeric($args[0])
83+
is_numeric($args[0])
7384
) {
85+
if ($this->PHPUnitVersion->requiresPhpversionAttributeWithOperator()->yes()) {
86+
$errors[] = RuleErrorBuilder::message(
87+
sprintf('Version requirement is missing operator.'),
88+
)
89+
->identifier('phpunit.attributeRequiresPhpVersion')
90+
->build();
91+
} elseif (
92+
$this->deprecationRulesInstalled
93+
&& $this->PHPUnitVersion->deprecatesPhpversionAttributeWithoutOperator()->yes()
94+
) {
95+
$errors[] = RuleErrorBuilder::message(
96+
sprintf('Version requirement without operator is deprecated.'),
97+
)
98+
->identifier('phpunit.attributeRequiresPhpVersion')
99+
->build();
100+
}
101+
74102
continue;
75103
}
76104

77-
if ($this->PHPUnitVersion->requiresPhpversionAttributeWithOperator()->yes()) {
105+
try {
106+
$testPhpVersionConstraint = $parser->parseConstraints($args[0]);
107+
} catch (UnexpectedValueException $e) {
78108
$errors[] = RuleErrorBuilder::message(
79-
sprintf('Version requirement is missing operator.'),
80-
)
81-
->identifier('phpunit.attributeRequiresPhpVersion')
82-
->build();
83-
} elseif (
84-
$this->deprecationRulesInstalled
85-
&& $this->PHPUnitVersion->deprecatesPhpversionAttributeWithoutOperator()->yes()
86-
) {
87-
$errors[] = RuleErrorBuilder::message(
88-
sprintf('Version requirement without operator is deprecated.'),
109+
sprintf($e->getMessage()),
89110
)
90111
->identifier('phpunit.attributeRequiresPhpVersion')
91112
->build();
113+
114+
continue;
115+
}
116+
117+
if ($this->phpstanVersionConstraint->matches($testPhpVersionConstraint)) {
118+
continue;
92119
}
93120

121+
$errors[] = RuleErrorBuilder::message(
122+
sprintf('Version requirement will always evaluate to false.'),
123+
)
124+
->identifier('phpunit.attributeRequiresPhpVersion')
125+
->build();
94126
}
95127

96128
return $errors;

tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\PHPUnit;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
78
use PHPStan\Type\FileTypeMapper;
@@ -12,6 +13,8 @@
1213
final class AttributeRequiresPhpVersionRuleTest extends RuleTestCase
1314
{
1415

16+
private int $phpVersion = 80500;
17+
1518
private ?int $phpunitMajorVersion;
1619

1720
private ?int $phpunitMinorVersion;
@@ -78,6 +81,34 @@ public function testRuleOnPHPUnit13(): void
7881
]);
7982
}
8083

84+
public function testPhpVersionMismatch(): void
85+
{
86+
$this->phpunitMajorVersion = 12;
87+
$this->phpunitMinorVersion = 4;
88+
$this->deprecationRulesInstalled = false;
89+
90+
$this->analyse([__DIR__ . '/data/requires-php-version-mismatch.php'], [
91+
[
92+
'Version requirement will always evaluate to false.',
93+
12,
94+
],
95+
]);
96+
}
97+
98+
public function testInvalidPhpVersion(): void
99+
{
100+
$this->phpunitMajorVersion = 12;
101+
$this->phpunitMinorVersion = 4;
102+
$this->deprecationRulesInstalled = false;
103+
104+
$this->analyse([__DIR__ . '/data/requires-php-version-invalid.php'], [
105+
[
106+
'Could not parse version constraint abc: Invalid version string "abc"',
107+
12,
108+
],
109+
]);
110+
}
111+
81112
protected function getRule(): Rule
82113
{
83114
$phpunitVersion = new PHPUnitVersion($this->phpunitMajorVersion, $this->phpunitMinorVersion);
@@ -89,6 +120,7 @@ protected function getRule(): Rule
89120
$phpunitVersion,
90121
),
91122
$this->deprecationRulesInstalled,
123+
new PhpVersion($this->phpVersion),
92124
);
93125
}
94126

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace RequiresPhpVersionMismatch;
4+
5+
use PHPUnit\Framework\Attributes\DataProvider;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use PHPUnit\Framework\TestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
10+
class InvalidConstraint extends TestCase
11+
{
12+
#[RequiresPhp('abc')]
13+
public function testFoo(): void {
14+
15+
}
16+
}
17+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace RequiresPhpVersionMismatch;
4+
5+
use PHPUnit\Framework\Attributes\DataProvider;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use PHPUnit\Framework\TestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
10+
class RequiresPhp5 extends TestCase
11+
{
12+
#[RequiresPhp('< 7.0')]
13+
public function testFoo(): void {
14+
15+
}
16+
}
17+
18+
class RequiresPhp8 extends TestCase
19+
{
20+
#[RequiresPhp('>=8.0')]
21+
public function testFoo(): void {
22+
23+
}
24+
}

0 commit comments

Comments
 (0)