Skip to content

Commit f4b2863

Browse files
authored
Keep the analysed PHP version as the floor of a PHP_VERSION_ID < X guard (#6746)
1 parent bb2b515 commit f4b2863

13 files changed

Lines changed: 324 additions & 75 deletions

‎src/Analyser/MutatingScope.php‎

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
use function is_string;
124124
use function ltrim;
125125
use function md5;
126+
use function min;
126127
use function preg_match;
127128
use function preg_match_all;
128129
use function spl_object_id;
@@ -5877,25 +5878,56 @@ public function getPhpVersion(): PhpVersions
58775878
{
58785879
$constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID'));
58795880
if ($constType !== null && !$this->isOverallPhpVersionRange($constType)) {
5881+
if (
5882+
$constType instanceof IntegerRangeType
5883+
&& $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID
5884+
&& $constType->getMax() !== null
5885+
&& $this->getConfiguredPhpVersionRange() === null
5886+
) {
5887+
// A guard like `PHP_VERSION_ID < 80400` only gives an upper bound. Without
5888+
// a configured version range, the lower bound is merely the lowest version
5889+
// PHPStan knows, while outside the guard the single analysed PHP version
5890+
// applies. Keep the lower bound at that version, or as close to it as
5891+
// the guard allows.
5892+
$maxVersionId = $constType->getMax();
5893+
return new PhpVersions(IntegerRangeType::fromInterval(
5894+
min($this->services->phpVersion->getVersionId(), $maxVersionId),
5895+
$maxVersionId,
5896+
));
5897+
}
5898+
58805899
return new PhpVersions($constType);
58815900
}
58825901

5883-
// The analysed PHP version range comes either from the NEON phpVersion min/max
5884-
// config or from the composer.json "require.php" constraint - the very same
5885-
// source ConstantResolver narrows PHP_VERSION_ID with, so that
5886-
// Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant.
5902+
$configuredRange = $this->getConfiguredPhpVersionRange();
5903+
if ($configuredRange !== null) {
5904+
return new PhpVersions($configuredRange);
5905+
}
5906+
5907+
return new PhpVersions(new ConstantIntegerType($this->services->phpVersion->getVersionId()));
5908+
}
5909+
5910+
/**
5911+
* The analysed PHP version range comes either from the NEON phpVersion min/max
5912+
* config or from the composer.json "require.php" constraint - the very same
5913+
* source ConstantResolver narrows PHP_VERSION_ID with, so that
5914+
* Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant.
5915+
* Null when neither is set and a single PHP version is analysed.
5916+
*/
5917+
private function getConfiguredPhpVersionRange(): ?Type
5918+
{
58875919
[$minPhpVersion, $maxPhpVersion] = $this->services->configuredPhpVersionRangeHelper->getVersionRange();
58885920
if (
5889-
$minPhpVersion !== null
5890-
|| ($maxPhpVersion !== null && $maxPhpVersion->getVersionId() !== PhpVersionFactory::MAX_PHP_VERSION)
5921+
$minPhpVersion === null
5922+
&& ($maxPhpVersion === null || $maxPhpVersion->getVersionId() === PhpVersionFactory::MAX_PHP_VERSION)
58915923
) {
5892-
return new PhpVersions(IntegerRangeType::fromInterval(
5893-
$minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID,
5894-
$maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null,
5895-
));
5924+
return null;
58965925
}
58975926

5898-
return new PhpVersions(new ConstantIntegerType($this->services->phpVersion->getVersionId()));
5927+
return IntegerRangeType::fromInterval(
5928+
$minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID,
5929+
$maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null,
5930+
);
58995931
}
59005932

59015933
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\RequiresPhp;
8+
9+
/**
10+
* @extends RuleTestCase<InvalidPromotedPropertiesRule>
11+
*/
12+
class InvalidPromotedPropertiesRulePhpVersionRangeTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new InvalidPromotedPropertiesRule();
18+
}
19+
20+
#[RequiresPhp('>= 8.0.0')]
21+
public function testBug15425(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/bug-15425-range.php'], [
24+
[
25+
'Promoted properties are supported only on PHP 8.0 and later.',
26+
10,
27+
],
28+
]);
29+
}
30+
31+
public static function getAdditionalConfigFiles(): array
32+
{
33+
return [
34+
__DIR__ . '/invalidPromotedPropertiesPhpVersionRange.neon',
35+
];
36+
}
37+
38+
}

‎tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,15 @@ public function testConditionallyDeclaredClass(): void
141141
$this->analyse([__DIR__ . '/data/promoted-properties-php-versions.php'], $errors);
142142
}
143143

144+
#[RequiresPhp('>= 8.0.0')]
145+
public function testBug15425(): void
146+
{
147+
$this->analyse([__DIR__ . '/data/bug-15425.php'], [
148+
[
149+
'Promoted properties are supported only on PHP 8.0 and later.',
150+
19,
151+
],
152+
]);
153+
}
154+
144155
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\RequiresPhp;
8+
9+
/**
10+
* @extends RuleTestCase<ReadOnlyClassRule>
11+
*/
12+
class ReadOnlyClassRulePhp81Test extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ReadOnlyClassRule();
18+
}
19+
20+
#[RequiresPhp('>= 8.2.0')]
21+
public function testBug15425(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/bug-15425-readonly.php'], [
24+
[
25+
'Readonly classes are supported only on PHP 8.2 and later.',
26+
8,
27+
],
28+
]);
29+
}
30+
31+
public static function getAdditionalConfigFiles(): array
32+
{
33+
return [
34+
__DIR__ . '/readOnlyClassPhp81.neon',
35+
];
36+
}
37+
38+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"require": {
3+
"php": "<8.4"
4+
}
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15425Range;
6+
7+
if (PHP_VERSION_ID < 80300) {
8+
final class Foo
9+
{
10+
public function __construct(public ?string $message = null)
11+
{
12+
}
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15425Readonly;
6+
7+
if (PHP_VERSION_ID < 80400) {
8+
final readonly class Foo
9+
{
10+
}
11+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15425;
6+
7+
if (PHP_VERSION_ID < 80400) {
8+
final class Foo
9+
{
10+
public function __construct(public ?string $message = null)
11+
{
12+
}
13+
}
14+
}
15+
16+
if (PHP_VERSION_ID < 80000) {
17+
final class Bar
18+
{
19+
public function __construct(public ?string $message = null)
20+
{
21+
}
22+
}
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
# composer.json "php": "<8.4" gives a version range without a lower bound
3+
composerAutoloaderProjectPaths:
4+
- %rootDir%/tests/PHPStan/Rules/Classes/data/bug-15425-composer
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
parameters:
2+
phpVersion: 80100

0 commit comments

Comments
 (0)