From b1d51b116062f27c6a3f0972dc70ac6e924908f9 Mon Sep 17 00:00:00 2001 From: HawkForge <75696486+HawkForge@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:28:29 +0600 Subject: [PATCH 1/2] Keep the analysed PHP version as the floor of a PHP_VERSION_ID < X guard When a single PHP version is analysed (a fixed `phpVersion`, or no version range from the config or composer.json), PHP_VERSION_ID is int<50207, max> and Scope::getPhpVersion() treats that as no information and answers with the analysed version. A guard like `if (PHP_VERSION_ID < 80400)` narrows the constant to int<50207, 80399>, and getPhpVersion() returned that range as is, so the version dropped to PHP 5.2.7 inside the branch. Since the "supported only on PHP X" rules read the version from the scope, a class with promoted properties in such a branch got a non-ignorable property.promotedNotSupported error with `phpVersion: 80500`. Only the upper bound of that range comes from the guard. Raise the lower bound to the analysed version, or to the guard's upper bound when the guard excludes the analysed version. Guards with a lower bound (`PHP_VERSION_ID >= 80300`) and configured version ranges are unchanged. The turbo-ext mirror gets the same change. Closes https://github.com/phpstan/phpstan/issues/15425 --- src/Analyser/MutatingScope.php | 54 +++++++-- .../InvalidPromotedPropertiesRuleTest.php | 6 + .../PHPStan/Rules/Classes/data/bug-15425.php | 16 +++ turbo-ext/src/MutatingScope.cpp | 106 ++++++++++++------ turbo-ext/src/generated/MutatingScope.h | 61 +++++----- turbo-ext/tests/scope-family.php | 32 ++++++ 6 files changed, 200 insertions(+), 75 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-15425.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b31ccecf985..ba9572e4fa5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -136,6 +136,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; @@ -6020,25 +6021,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->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->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->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->phpVersion->getVersionId())); + return IntegerRangeType::fromInterval( + $minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, + $maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null, + ); } /** diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index e856be21354..4c109e94659 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -141,4 +141,10 @@ 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'], []); + } + } 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..80a74c039bc --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15425.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15425; + +if (PHP_VERSION_ID < 80400) { + \PHPStan\dumpType(PHP_VERSION_ID); + + final class Foo + { + public function __construct(public ?string $message = null) + { + } + } +} diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 66e26133896..4a810496c9f 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -3908,11 +3908,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; @@ -3920,12 +3923,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 = slot(PT_MS_PROP_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 = slot(PT_MS_PROP_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(!requireSlot(PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER, "configuredPhpVersionRangeHelper"))) return zv::Val(); zv::Ref rangeHelper = slot(PT_MS_PROP_CONFIGURED_PHP_VERSION_RANGE_HELPER); zv::Val range = pt_type_call(rangeHelper.asObject(), PT_LC("getversionrange"), 0, NULL); @@ -3952,36 +3999,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 = slot(PT_MS_PROP_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 832eb9a9226..4e3ae75c523 100644 --- a/turbo-ext/src/generated/MutatingScope.h +++ b/turbo-ext/src/generated/MutatingScope.h @@ -528,21 +528,22 @@ inline constexpr char strings[] = "getIterableValueType\0" /* 9182 */ "PHPStan\\Php\\PhpVersions\0" /* 9203 */ "getPhpVersion\0" /* 9227 */ - "isOverallPhpVersionRange\0" /* 9241 */ - "PhpParser\\Node\0" /* 9266 */ - "invokeNodeCallback\0" /* 9281 */ - "collectorType\0" /* 9300 */ - "data\0" /* 9314 */ - "emitCollectedData\0" /* 9319 */ - "extensionClass\0" /* 9337 */ - "trackValueDependency\0" /* 9352 */ - "file\0" /* 9373 */ - "trackFileDependency\0" /* 9378 */ - "directory\0" /* 9398 */ - "pattern\0" /* 9408 */ - "'*'\0" /* 9416 */ - "trackDirectoryDependency\0" /* 9420 */ - "trackClassDependency"; /* 9445 */ + "getConfiguredPhpVersionRange\0" /* 9241 */ + "isOverallPhpVersionRange\0" /* 9270 */ + "PhpParser\\Node\0" /* 9295 */ + "invokeNodeCallback\0" /* 9310 */ + "collectorType\0" /* 9329 */ + "data\0" /* 9343 */ + "emitCollectedData\0" /* 9348 */ + "extensionClass\0" /* 9366 */ + "trackValueDependency\0" /* 9381 */ + "file\0" /* 9402 */ + "trackFileDependency\0" /* 9407 */ + "directory\0" /* 9427 */ + "pattern\0" /* 9437 */ + "'*'\0" /* 9445 */ + "trackDirectoryDependency\0" /* 9449 */ + "trackClassDependency"; /* 9474 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 10), /* __construct $container */ reg::packed(48, 0, 61), /* __construct $scopeFactory */ @@ -1070,20 +1071,21 @@ inline constexpr reg::PackedArg args[] = { reg::packed(5878, 0, 2162), /* getIterableValueType $iteratee */ reg::packed(1476, 0, 2162), /* getIterableValueType return */ reg::packed(1476, 0, 9203), /* getPhpVersion return */ + reg::packed(1476, MAY_BE_NULL, 2162), /* getConfiguredPhpVersionRange return */ reg::packed(4614, 0, 2162), /* isOverallPhpVersionRange $type */ reg::packed(1476, MAY_BE_BOOL), /* isOverallPhpVersionRange return */ - reg::packed(2500, 0, 9266), /* invokeNodeCallback $node */ + reg::packed(2500, 0, 9295), /* invokeNodeCallback $node */ reg::packed(1476, MAY_BE_VOID), /* invokeNodeCallback return */ - reg::packed(9300, MAY_BE_STRING), /* emitCollectedData $collectorType */ - reg::packed(9314, MAY_BE_ANY), /* emitCollectedData $data */ + reg::packed(9329, MAY_BE_STRING), /* emitCollectedData $collectorType */ + reg::packed(9343, MAY_BE_ANY), /* emitCollectedData $data */ reg::packed(1476, MAY_BE_VOID), /* emitCollectedData return */ - reg::packed(9337, MAY_BE_STRING), /* trackValueDependency $extensionClass */ + reg::packed(9366, MAY_BE_STRING), /* trackValueDependency $extensionClass */ reg::packed(3280, MAY_BE_STRING), /* trackValueDependency $key */ reg::packed(1476, MAY_BE_VOID), /* trackValueDependency return */ - reg::packed(9373, MAY_BE_STRING), /* trackFileDependency $file */ + reg::packed(9402, MAY_BE_STRING), /* trackFileDependency $file */ reg::packed(1476, MAY_BE_VOID), /* trackFileDependency return */ - reg::packed(9398, MAY_BE_STRING), /* trackDirectoryDependency $directory */ - reg::packed(9408, MAY_BE_STRING, reg::NoString, false, false, 9416), /* trackDirectoryDependency $pattern */ + reg::packed(9427, MAY_BE_STRING), /* trackDirectoryDependency $directory */ + reg::packed(9437, MAY_BE_STRING, reg::NoString, false, false, 9445), /* trackDirectoryDependency $pattern */ reg::packed(1476, MAY_BE_VOID), /* trackDirectoryDependency return */ reg::packed(3717, MAY_BE_STRING), /* trackClassDependency $className */ reg::packed(1476, MAY_BE_VOID), /* trackClassDependency return */ @@ -1285,13 +1287,14 @@ inline constexpr sigtab::Sig getNativeConstantTypes = { { 9140 /* getNativeConst inline constexpr sigtab::Sig getIterableKeyType = { { 9163 /* getIterableKeyType */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getIterableValueType = { { 9182 /* getIterableValueType */, 1, 523, 1, 524, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getPhpVersion = { { 9227 /* getPhpVersion */, 0, 525, 0, 525, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 9241 /* isOverallPhpVersionRange */, 1, 526, 1, 527, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig invokeNodeCallback = { { 9281 /* invokeNodeCallback */, 1, 528, 1, 529, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig emitCollectedData = { { 9319 /* emitCollectedData */, 2, 530, 2, 532, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackValueDependency = { { 9352 /* trackValueDependency */, 2, 533, 2, 535, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackFileDependency = { { 9378 /* trackFileDependency */, 1, 536, 1, 537, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackDirectoryDependency = { { 9420 /* trackDirectoryDependency */, 1, 538, 2, 540, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackClassDependency = { { 9445 /* trackClassDependency */, 1, 541, 1, 542, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 9241 /* getConfiguredPhpVersionRange */, 0, 526, 0, 526, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 9270 /* isOverallPhpVersionRange */, 1, 527, 1, 528, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig invokeNodeCallback = { { 9310 /* invokeNodeCallback */, 1, 529, 1, 530, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig emitCollectedData = { { 9348 /* emitCollectedData */, 2, 531, 2, 533, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackValueDependency = { { 9381 /* trackValueDependency */, 2, 534, 2, 536, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackFileDependency = { { 9407 /* trackFileDependency */, 1, 537, 1, 538, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackDirectoryDependency = { { 9449 /* trackDirectoryDependency */, 1, 539, 2, 541, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackClassDependency = { { 9474 /* trackClassDependency */, 1, 542, 1, 543, 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 a0fb591f0d5..4d085699dcc 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)); From dea756829b963c22d600c8b80bf0556e3ad72e0c Mon Sep 17 00:00:00 2001 From: HawkForge <75696486+HawkForge@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:28:29 +0600 Subject: [PATCH 2/2] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 374d29043e6..b05b57cc783 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'a0f7086'; + public const EXPECTED_EXTENSION_VERSION = 'b1d51b1'; private static bool $active = false;