diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6f640432a87..c46e3fb8f08 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -123,6 +123,7 @@ use function is_string; use function ltrim; use function md5; +use function min; use function preg_match; use function preg_match_all; use function spl_object_id; @@ -5877,25 +5878,56 @@ public function getPhpVersion(): PhpVersions { $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); if ($constType !== null && !$this->isOverallPhpVersionRange($constType)) { + if ( + $constType instanceof IntegerRangeType + && $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID + && $constType->getMax() !== null + && $this->getConfiguredPhpVersionRange() === null + ) { + // A guard like `PHP_VERSION_ID < 80400` only gives an upper bound. Without + // a configured version range, the lower bound is merely the lowest version + // PHPStan knows, while outside the guard the single analysed PHP version + // applies. Keep the lower bound at that version, or as close to it as + // the guard allows. + $maxVersionId = $constType->getMax(); + return new PhpVersions(IntegerRangeType::fromInterval( + min($this->services->phpVersion->getVersionId(), $maxVersionId), + $maxVersionId, + )); + } + return new PhpVersions($constType); } - // The analysed PHP version range comes either from the NEON phpVersion min/max - // config or from the composer.json "require.php" constraint - the very same - // source ConstantResolver narrows PHP_VERSION_ID with, so that - // Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant. + $configuredRange = $this->getConfiguredPhpVersionRange(); + if ($configuredRange !== null) { + return new PhpVersions($configuredRange); + } + + return new PhpVersions(new ConstantIntegerType($this->services->phpVersion->getVersionId())); + } + + /** + * The analysed PHP version range comes either from the NEON phpVersion min/max + * config or from the composer.json "require.php" constraint - the very same + * source ConstantResolver narrows PHP_VERSION_ID with, so that + * Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant. + * Null when neither is set and a single PHP version is analysed. + */ + private function getConfiguredPhpVersionRange(): ?Type + { [$minPhpVersion, $maxPhpVersion] = $this->services->configuredPhpVersionRangeHelper->getVersionRange(); if ( - $minPhpVersion !== null - || ($maxPhpVersion !== null && $maxPhpVersion->getVersionId() !== PhpVersionFactory::MAX_PHP_VERSION) + $minPhpVersion === null + && ($maxPhpVersion === null || $maxPhpVersion->getVersionId() === PhpVersionFactory::MAX_PHP_VERSION) ) { - return new PhpVersions(IntegerRangeType::fromInterval( - $minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, - $maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null, - )); + return null; } - return new PhpVersions(new ConstantIntegerType($this->services->phpVersion->getVersionId())); + return IntegerRangeType::fromInterval( + $minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, + $maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null, + ); } /** diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index c9740cb393d..a513f50e968 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -34,7 +34,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '04247a5'; + public const EXPECTED_EXTENSION_VERSION = 'ff7ba39'; private static bool $active = false; diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRulePhpVersionRangeTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRulePhpVersionRangeTest.php new file mode 100644 index 00000000000..a08a70b713b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRulePhpVersionRangeTest.php @@ -0,0 +1,38 @@ + + */ +class InvalidPromotedPropertiesRulePhpVersionRangeTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidPromotedPropertiesRule(); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug15425(): void + { + $this->analyse([__DIR__ . '/data/bug-15425-range.php'], [ + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 10, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/invalidPromotedPropertiesPhpVersionRange.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index e856be21354..5692d594e11 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -141,4 +141,15 @@ public function testConditionallyDeclaredClass(): void $this->analyse([__DIR__ . '/data/promoted-properties-php-versions.php'], $errors); } + #[RequiresPhp('>= 8.0.0')] + public function testBug15425(): void + { + $this->analyse([__DIR__ . '/data/bug-15425.php'], [ + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ReadOnlyClassRulePhp81Test.php b/tests/PHPStan/Rules/Classes/ReadOnlyClassRulePhp81Test.php new file mode 100644 index 00000000000..d1ac1f2b520 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ReadOnlyClassRulePhp81Test.php @@ -0,0 +1,38 @@ + + */ +class ReadOnlyClassRulePhp81Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyClassRule(); + } + + #[RequiresPhp('>= 8.2.0')] + public function testBug15425(): void + { + $this->analyse([__DIR__ . '/data/bug-15425-readonly.php'], [ + [ + 'Readonly classes are supported only on PHP 8.2 and later.', + 8, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/readOnlyClassPhp81.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-15425-composer/composer.json b/tests/PHPStan/Rules/Classes/data/bug-15425-composer/composer.json new file mode 100644 index 00000000000..04cb27ac932 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15425-composer/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "<8.4" + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-15425-range.php b/tests/PHPStan/Rules/Classes/data/bug-15425-range.php new file mode 100644 index 00000000000..bd9fb78b2d6 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15425-range.php @@ -0,0 +1,14 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15425Range; + +if (PHP_VERSION_ID < 80300) { + final class Foo + { + public function __construct(public ?string $message = null) + { + } + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-15425-readonly.php b/tests/PHPStan/Rules/Classes/data/bug-15425-readonly.php new file mode 100644 index 00000000000..3f5a34cb1c7 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15425-readonly.php @@ -0,0 +1,11 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug15425Readonly; + +if (PHP_VERSION_ID < 80400) { + final readonly class Foo + { + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-15425.php b/tests/PHPStan/Rules/Classes/data/bug-15425.php new file mode 100644 index 00000000000..cc579e4ac1a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15425.php @@ -0,0 +1,23 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15425; + +if (PHP_VERSION_ID < 80400) { + final class Foo + { + public function __construct(public ?string $message = null) + { + } + } +} + +if (PHP_VERSION_ID < 80000) { + final class Bar + { + public function __construct(public ?string $message = null) + { + } + } +} diff --git a/tests/PHPStan/Rules/Classes/invalidPromotedPropertiesPhpVersionRange.neon b/tests/PHPStan/Rules/Classes/invalidPromotedPropertiesPhpVersionRange.neon new file mode 100644 index 00000000000..c8ede5a79bc --- /dev/null +++ b/tests/PHPStan/Rules/Classes/invalidPromotedPropertiesPhpVersionRange.neon @@ -0,0 +1,4 @@ +parameters: + # composer.json "php": "<8.4" gives a version range without a lower bound + composerAutoloaderProjectPaths: + - %rootDir%/tests/PHPStan/Rules/Classes/data/bug-15425-composer diff --git a/tests/PHPStan/Rules/Classes/readOnlyClassPhp81.neon b/tests/PHPStan/Rules/Classes/readOnlyClassPhp81.neon new file mode 100644 index 00000000000..181c74319a3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/readOnlyClassPhp81.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80100 diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 26b8d2767f5..154164f2ab5 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -3678,11 +3678,14 @@ class MutatingScope if (UNEXPECTED(constType.isUndef())) return zv::Val(); bool isOverallPhpVersionRange = false; + bool hasMinAnalyzableVersion = false; + zv::Val max; if (constType.ref().isObject() && constType.ref().instanceOf(pt_ce_integer_range_type)) { zv::Val min = pt_type_call(constType.ref().asObject(), PT_LC("getmin"), 0, NULL); if (UNEXPECTED(min.isUndef())) return zv::Val(); if (min.ref().isLong() && min.ref().asLong() == PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID) { - zv::Val max = pt_type_call(constType.ref().asObject(), PT_LC("getmax"), 0, NULL); + hasMinAnalyzableVersion = true; + max = pt_type_call(constType.ref().asObject(), PT_LC("getmax"), 0, NULL); if (UNEXPECTED(max.isUndef())) return zv::Val(); if (max.isNull() || (max.ref().isLong() && max.ref().asLong() == PT_MS_MAX_PHP_VERSION)) { isOverallPhpVersionRange = true; @@ -3690,12 +3693,56 @@ class MutatingScope } } - if (!constType.isNull() && !isOverallPhpVersionRange) return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constType.raw()); + if (!constType.isNull() && !isOverallPhpVersionRange) { + if (hasMinAnalyzableVersion && max.ref().isLong()) { + zv::Val configuredRange = getConfiguredPhpVersionRange(); + if (UNEXPECTED(configuredRange.isUndef())) return zv::Val(); + if (configuredRange.isNull()) { + // A guard like `PHP_VERSION_ID < 80400` only gives an upper bound. Without + // a configured version range, the lower bound is merely the lowest version + // PHPStan knows, while outside the guard the single analysed PHP version + // applies. Keep the lower bound at that version, or as close to it as + // the guard allows. + zv::Ref phpVersion = service(PT_MSS_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) return uninitializedProperty("phpVersion"); + zv::Val versionId = pt_php_version_get_version_id(phpVersion.raw()); + if (UNEXPECTED(versionId.isUndef())) return zv::Val(); + zend_long maxVersionId = max.ref().asLong(); + zend_long minVersionId = zval_get_long(versionId.raw()); + zval interval[2]; + ZVAL_LONG(&interval[0], minVersionId < maxVersionId ? minVersionId : maxVersionId); + ZVAL_LONG(&interval[1], maxVersionId); + zv::Val versionRange = pt_type_call_static_ce(pt_ce_integer_range_type, PT_LC("frominterval"), 2, interval); + if (UNEXPECTED(versionRange.isUndef())) return zv::Val(); + return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, versionRange.raw()); + } + } + + return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constType.raw()); + } + + zv::Val configuredRange = getConfiguredPhpVersionRange(); + if (UNEXPECTED(configuredRange.isUndef())) return zv::Val(); + if (!configuredRange.isNull()) return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, configuredRange.raw()); + + zv::Ref phpVersion = service(PT_MSS_PHP_VERSION); + if (UNEXPECTED(!phpVersion.isObject())) return uninitializedProperty("phpVersion"); + zv::Val versionId = pt_php_version_get_version_id(phpVersion.raw()); + if (UNEXPECTED(versionId.isUndef())) return zv::Val(); + zval constantInteger; + if (UNEXPECTED(!pt_constant_integer_type_new(&constantInteger, zval_get_long(versionId.raw())))) return zv::Val(); + zv::Val constantIntegerType = zv::Val::adopt(constantInteger); + return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constantIntegerType.raw()); + } - // The analysed PHP version range comes either from the NEON phpVersion min/max - // config or from the composer.json "require.php" constraint - the very same - // source ConstantResolver narrows PHP_VERSION_ID with, so that - // Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant. + /* The analysed PHP version range comes either from the NEON phpVersion min/max + * config or from the composer.json "require.php" constraint - the very same + * source ConstantResolver narrows PHP_VERSION_ID with, so that + * Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant. + * NULL when neither is set and a single PHP version is analysed; UNDEF = + * pending exception */ + zv::Val getConfiguredPhpVersionRange() + { if (UNEXPECTED(!requireService())) return zv::Val(); zv::Ref rangeHelper = service(PT_MSS_CONFIGURED_PHP_VERSION_RANGE_HELPER); zv::Val range = pt_type_call(rangeHelper.asObject(), PT_LC("getversionrange"), 0, NULL); @@ -3722,36 +3769,25 @@ class MutatingScope if (UNEXPECTED(maxVersionId.isUndef())) return zv::Val(); narrowed = !(maxVersionId.ref().isLong() && maxVersionId.ref().asLong() == PT_MS_MAX_PHP_VERSION); } - if (narrowed) { - zval interval[2]; - zv::Val minVersionId, maxVersionId; - if (minPhpVersion != NULL) { - minVersionId = pt_php_version_get_version_id(minPhpVersion); - if (UNEXPECTED(minVersionId.isUndef())) return zv::Val(); - ZVAL_COPY_VALUE(&interval[0], minVersionId.raw()); - } else { - ZVAL_LONG(&interval[0], PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID); - } - if (maxPhpVersion != NULL) { - maxVersionId = pt_php_version_get_version_id(maxPhpVersion); - if (UNEXPECTED(maxVersionId.isUndef())) return zv::Val(); - ZVAL_COPY_VALUE(&interval[1], maxVersionId.raw()); - } else { - ZVAL_NULL(&interval[1]); - } - zv::Val versionRange = pt_type_call_static_ce(pt_ce_integer_range_type, PT_LC("frominterval"), 2, interval); - if (UNEXPECTED(versionRange.isUndef())) return zv::Val(); - return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, versionRange.raw()); - } + if (!narrowed) return zv::Val::null(); - zv::Ref phpVersion = service(PT_MSS_PHP_VERSION); - if (UNEXPECTED(!phpVersion.isObject())) return uninitializedProperty("phpVersion"); - zv::Val versionId = pt_php_version_get_version_id(phpVersion.raw()); - if (UNEXPECTED(versionId.isUndef())) return zv::Val(); - zval constantInteger; - if (UNEXPECTED(!pt_constant_integer_type_new(&constantInteger, zval_get_long(versionId.raw())))) return zv::Val(); - zv::Val constantIntegerType = zv::Val::adopt(constantInteger); - return pt_type_new(PT_CLASS_PHP_VERSIONS, 1, constantIntegerType.raw()); + zval interval[2]; + zv::Val minVersionId, maxVersionId; + if (minPhpVersion != NULL) { + minVersionId = pt_php_version_get_version_id(minPhpVersion); + if (UNEXPECTED(minVersionId.isUndef())) return zv::Val(); + ZVAL_COPY_VALUE(&interval[0], minVersionId.raw()); + } else { + ZVAL_LONG(&interval[0], PT_MS_PHP_MIN_ANALYZABLE_VERSION_ID); + } + if (maxPhpVersion != NULL) { + maxVersionId = pt_php_version_get_version_id(maxPhpVersion); + if (UNEXPECTED(maxVersionId.isUndef())) return zv::Val(); + ZVAL_COPY_VALUE(&interval[1], maxVersionId.raw()); + } else { + ZVAL_NULL(&interval[1]); + } + return pt_type_call_static_ce(pt_ce_integer_range_type, PT_LC("frominterval"), 2, interval); } /* $this->getPhpVersion()->supportsNamedArguments()->no() negated — the diff --git a/turbo-ext/src/generated/MutatingScope.h b/turbo-ext/src/generated/MutatingScope.h index a3c34796646..e01ebcdf60c 100644 --- a/turbo-ext/src/generated/MutatingScope.h +++ b/turbo-ext/src/generated/MutatingScope.h @@ -477,21 +477,22 @@ inline constexpr char strings[] = "getIterableValueType\0" /* 8442 */ "PHPStan\\Php\\PhpVersions\0" /* 8463 */ "getPhpVersion\0" /* 8487 */ - "isOverallPhpVersionRange\0" /* 8501 */ - "PhpParser\\Node\0" /* 8526 */ - "invokeNodeCallback\0" /* 8541 */ - "collectorType\0" /* 8560 */ - "data\0" /* 8574 */ - "emitCollectedData\0" /* 8579 */ - "extensionClass\0" /* 8597 */ - "trackValueDependency\0" /* 8612 */ - "file\0" /* 8633 */ - "trackFileDependency\0" /* 8638 */ - "directory\0" /* 8658 */ - "pattern\0" /* 8668 */ - "'*'\0" /* 8676 */ - "trackDirectoryDependency\0" /* 8680 */ - "trackClassDependency"; /* 8705 */ + "getConfiguredPhpVersionRange\0" /* 8501 */ + "isOverallPhpVersionRange\0" /* 8530 */ + "PhpParser\\Node\0" /* 8555 */ + "invokeNodeCallback\0" /* 8570 */ + "collectorType\0" /* 8589 */ + "data\0" /* 8603 */ + "emitCollectedData\0" /* 8608 */ + "extensionClass\0" /* 8626 */ + "trackValueDependency\0" /* 8641 */ + "file\0" /* 8662 */ + "trackFileDependency\0" /* 8667 */ + "directory\0" /* 8687 */ + "pattern\0" /* 8697 */ + "'*'\0" /* 8705 */ + "trackDirectoryDependency\0" /* 8709 */ + "trackClassDependency"; /* 8734 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 9), /* __construct $services */ reg::packed(40, 0, 53), /* __construct $scopeFactory */ @@ -1004,20 +1005,21 @@ inline constexpr reg::PackedArg args[] = { reg::packed(5138, 0, 1424), /* getIterableValueType $iteratee */ reg::packed(738, 0, 1424), /* getIterableValueType return */ reg::packed(738, 0, 8463), /* getPhpVersion return */ + reg::packed(738, MAY_BE_NULL, 1424), /* getConfiguredPhpVersionRange return */ reg::packed(3874, 0, 1424), /* isOverallPhpVersionRange $type */ reg::packed(738, MAY_BE_BOOL), /* isOverallPhpVersionRange return */ - reg::packed(1707, 0, 8526), /* invokeNodeCallback $node */ + reg::packed(1707, 0, 8555), /* invokeNodeCallback $node */ reg::packed(738, MAY_BE_VOID), /* invokeNodeCallback return */ - reg::packed(8560, MAY_BE_STRING), /* emitCollectedData $collectorType */ - reg::packed(8574, MAY_BE_ANY), /* emitCollectedData $data */ + reg::packed(8589, MAY_BE_STRING), /* emitCollectedData $collectorType */ + reg::packed(8603, MAY_BE_ANY), /* emitCollectedData $data */ reg::packed(738, MAY_BE_VOID), /* emitCollectedData return */ - reg::packed(8597, MAY_BE_STRING), /* trackValueDependency $extensionClass */ + reg::packed(8626, MAY_BE_STRING), /* trackValueDependency $extensionClass */ reg::packed(2540, MAY_BE_STRING), /* trackValueDependency $key */ reg::packed(738, MAY_BE_VOID), /* trackValueDependency return */ - reg::packed(8633, MAY_BE_STRING), /* trackFileDependency $file */ + reg::packed(8662, MAY_BE_STRING), /* trackFileDependency $file */ reg::packed(738, MAY_BE_VOID), /* trackFileDependency return */ - reg::packed(8658, MAY_BE_STRING), /* trackDirectoryDependency $directory */ - reg::packed(8668, MAY_BE_STRING, reg::NoString, false, false, 8676), /* trackDirectoryDependency $pattern */ + reg::packed(8687, MAY_BE_STRING), /* trackDirectoryDependency $directory */ + reg::packed(8697, MAY_BE_STRING, reg::NoString, false, false, 8705), /* trackDirectoryDependency $pattern */ reg::packed(738, MAY_BE_VOID), /* trackDirectoryDependency return */ reg::packed(2977, MAY_BE_STRING), /* trackClassDependency $className */ reg::packed(738, MAY_BE_VOID), /* trackClassDependency return */ @@ -1218,13 +1220,14 @@ inline constexpr sigtab::Sig getNativeConstantTypes = { { 8400 /* getNativeConst inline constexpr sigtab::Sig getIterableKeyType = { { 8423 /* getIterableKeyType */, 1, 506, 1, 507, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getIterableValueType = { { 8442 /* getIterableValueType */, 1, 508, 1, 509, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getPhpVersion = { { 8487 /* getPhpVersion */, 0, 510, 0, 510, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8501 /* isOverallPhpVersionRange */, 1, 511, 1, 512, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig invokeNodeCallback = { { 8541 /* invokeNodeCallback */, 1, 513, 1, 514, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig emitCollectedData = { { 8579 /* emitCollectedData */, 2, 515, 2, 517, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackValueDependency = { { 8612 /* trackValueDependency */, 2, 518, 2, 520, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackFileDependency = { { 8638 /* trackFileDependency */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackDirectoryDependency = { { 8680 /* trackDirectoryDependency */, 1, 523, 2, 525, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackClassDependency = { { 8705 /* trackClassDependency */, 1, 526, 1, 527, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 8501 /* getConfiguredPhpVersionRange */, 0, 511, 0, 511, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8530 /* isOverallPhpVersionRange */, 1, 512, 1, 513, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig invokeNodeCallback = { { 8570 /* invokeNodeCallback */, 1, 514, 1, 515, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig emitCollectedData = { { 8608 /* emitCollectedData */, 2, 516, 2, 518, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackValueDependency = { { 8641 /* trackValueDependency */, 2, 519, 2, 521, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackFileDependency = { { 8667 /* trackFileDependency */, 1, 522, 1, 523, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackDirectoryDependency = { { 8709 /* trackDirectoryDependency */, 1, 524, 2, 526, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackClassDependency = { { 8734 /* trackClassDependency */, 1, 527, 1, 528, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::MutatingScope diff --git a/turbo-ext/tests/scope-family.php b/turbo-ext/tests/scope-family.php index faa70435cee..b60fc4cf039 100644 --- a/turbo-ext/tests/scope-family.php +++ b/turbo-ext/tests/scope-family.php @@ -643,6 +643,35 @@ public function exprRef(Node\Expr $expr): mixed return [$table, $table, []]; }; +// PHP_VERSION_ID under a guard that only gives an upper bound (`PHP_VERSION_ID < X`), +// with a single analysed version pinned to phpVersion: 80500 whatever PHP runs +// this: getPhpVersion() raises the lower bound to the analysed version, or as +// close to it as the guard allows - once with the analysed version above the +// guard's bound and once below it +$sfGuardedVersionScopes = []; +foreach ([80399, 80599] as $sfGuardedMax) { + $sfGuardedVersionFetch = new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID')); + $sfGuardedVersionArgs = $sfHarness->constructorArgs( + $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)) + ->assignExpression($sfGuardedVersionFetch, \PHPStan\Type\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, $sfGuardedMax), new \PHPStan\Type\IntegerType()), + ); + $sfGuardedVersionArgs['phpVersion'] = new \PHPStan\Php\PhpVersion(80500); + $sfGuardedVersionArgs['configuredPhpVersionRangeHelper'] = new \PHPStan\Php\ConfiguredPhpVersionRangeHelper(80500, $scContainer->getByType(\PHPStan\Php\ComposerPhpVersionFactory::class)); + $sfGuardedVersionScope = new \PHPStan\Analyser\MutatingScope(...array_values($sfGuardedVersionArgs)); + $sfScopes[spl_object_id($sfGuardedVersionScope)] = [$sfGuardedVersionScope, [$sfGuardedVersionFetch], null]; + $sfGuardedVersionScopes[] = [$sfGuardedVersionScope, static function (string $side) use ($sfHarness, $sfGuardedVersionFetch, $sfGuardedMax): array { + $native = $side === 'native'; + $range = $native + ? \PHPStanTurbo\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, $sfGuardedMax) + : \PHPStan\Type\IntegerRangeType::fromInterval(\PHPStan\Analyser\ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, $sfGuardedMax); + $holder = $native + ? new \PHPStanTurbo\ExpressionTypeHolder($sfGuardedVersionFetch, $range, \PHPStanTurbo\TrinaryLogic::createYes()) + : new \PHPStan\Analyser\ExpressionTypeHolder($sfGuardedVersionFetch, $range, \PHPStan\TrinaryLogic::createYes()); + $table = [$sfHarness->key($sfGuardedVersionFetch) => $holder]; + + return [$table, $table, []]; + }]; +} foreach ([80500, 70400] as $sfVersionId) { $sfVersionScope = $sfScopeFactory->create(\PHPStan\Analyser\ScopeContext::create($sfFile)->enterClass($sfReflectionProvider->getClass(\ScopeFamilyFixture\Holder::class))) ->assignExpression(new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('PHP_VERSION_ID')), new \PHPStan\Type\Constant\ConstantIntegerType($sfVersionId), new \PHPStan\Type\IntegerType()); @@ -759,6 +788,9 @@ public function exprRef(Node\Expr $expr): mixed // the scopes whose tables are built per side (each side's own Type classes) $sfScopes[spl_object_id($sfSyntheticScope)][3] = $sfSyntheticTables; $sfScopes[spl_object_id($sfOverallVersionScope)][3] = $sfOverallVersionTables; +foreach ($sfGuardedVersionScopes as [$sfGuardedVersionScope, $sfGuardedVersionTables]) { + $sfScopes[spl_object_id($sfGuardedVersionScope)][3] = $sfGuardedVersionTables; +} $sfPromotedArgs = $sfHarness->constructorArgs($sfSyntheticScope); $sfPromotedArgs['nativeTypesPromoted'] = true; $sfPromotedScope = new \PHPStan\Analyser\MutatingScope(...array_values($sfPromotedArgs));