Skip to content
Open
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
],
"require": {
"php": "^7.4 || ^8.0",
"composer/semver": "^3.4",
"phpstan/phpstan": "^2.1.32"
},
"conflict": {
Expand Down
37 changes: 35 additions & 2 deletions src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

namespace PHPStan\Rules\PHPUnit;

use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\VersionParser;
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 UnexpectedValueException;
use function count;
use function is_numeric;
use function method_exists;
Expand All @@ -19,6 +23,8 @@
class AttributeRequiresPhpVersionRule implements Rule
{

private ConstraintInterface $phpstanVersionConstraint;

private PHPUnitVersion $PHPUnitVersion;

private TestMethodsHelper $testMethodsHelper;
Expand All @@ -31,12 +37,16 @@ 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;

$parser = new VersionParser();
$this->phpstanVersionConstraint = $parser->parseConstraints($phpVersion->getVersionString());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should compare against the composer.json min version instead of the phpstan.neon version?

}

public function getNodeType(): string
Expand All @@ -62,6 +72,7 @@ public function processNode(Node $node, Scope $scope): array
}

$errors = [];
$parser = new VersionParser();
foreach ($reflectionMethod->getAttributes('PHPUnit\Framework\Attributes\RequiresPhp') as $attr) {
$args = $attr->getArguments();
if (count($args) !== 1) {
Expand All @@ -71,6 +82,29 @@ public function processNode(Node $node, Scope $scope): array
if (
!is_numeric($args[0])
) {

try {
$testPhpVersionConstraint = $parser->parseConstraints($args[0]);
} catch (UnexpectedValueException $e) {
$errors[] = RuleErrorBuilder::message(
sprintf($e->getMessage()),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure we want to 1:1 print the message from composer/semver as a phpstan rule

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternative approach could be to somehow analyse #[RequiresPhp('>=8.0')] as if it would have the AST of PHP_VERSION_ID >= 80000 - which would re-use the already existing PHP_VERSION_ID checking capabilities

)
->identifier('phpunit.attributeRequiresPhpVersion')
->build();

continue;
}

if ($this->phpstanVersionConstraint->matches($testPhpVersionConstraint)) {
continue;
}

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

continue;
}

Expand All @@ -90,7 +124,6 @@ public function processNode(Node $node, Scope $scope): array
->identifier('phpunit.attributeRequiresPhpVersion')
->build();
}

}

return $errors;
Expand Down
32 changes: 32 additions & 0 deletions tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\FileTypeMapper;
Expand All @@ -12,6 +13,8 @@
final class AttributeRequiresPhpVersionRuleTest extends RuleTestCase
{

private int $phpVersion = 80500;

private ?int $phpunitMajorVersion;

private ?int $phpunitMinorVersion;
Expand Down Expand Up @@ -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'], [
[
'Could not parse version constraint abc: Invalid version string "abc"',
12,
],
]);
}

protected function getRule(): Rule
{
$phpunitVersion = new PHPUnitVersion($this->phpunitMajorVersion, $this->phpunitMinorVersion);
Expand All @@ -89,6 +120,7 @@ protected function getRule(): Rule
$phpunitVersion,
),
$this->deprecationRulesInstalled,
new PhpVersion($this->phpVersion),
);
}

Expand Down
17 changes: 17 additions & 0 deletions tests/Rules/PHPUnit/data/requires-php-version-invalid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace RequiresPhpVersionMismatch;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

class InvalidConstraint extends TestCase
{
#[RequiresPhp('abc')]
public function testFoo(): void {

}
}

24 changes: 24 additions & 0 deletions tests/Rules/PHPUnit/data/requires-php-version-mismatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace RequiresPhpVersionMismatch;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

class RequiresPhp5 extends TestCase
{
#[RequiresPhp('< 7.0')]
public function testFoo(): void {

}
}

class RequiresPhp8 extends TestCase
{
#[RequiresPhp('>=8.0')]
public function testFoo(): void {

}
}