From 4c33c517a9c7c2dcbdb56c02f37d11a957a5522b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 9 Feb 2026 18:14:12 +0100 Subject: [PATCH 01/13] Narrow PHP_VERSION_ID on version_compare() --- src/Php/SimplePhpVersionParser.php | 26 ++++ ...narrow-phpversionid-on-version-compare.php | 142 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/Php/SimplePhpVersionParser.php create mode 100644 tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php diff --git a/src/Php/SimplePhpVersionParser.php b/src/Php/SimplePhpVersionParser.php new file mode 100644 index 00000000000..37cc7cdf9d6 --- /dev/null +++ b/src/Php/SimplePhpVersionParser.php @@ -0,0 +1,26 @@ +', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'lt' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '<=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'le' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } +} + +function greater(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'gt' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '>=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80400, max>', $x); + } + if ( + version_compare( PHP_VERSION, '8.4', 'ge' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80400, max>', $x); + } +} + +function equal(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '=' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '==' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'eq' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } +} + + +function not(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '!=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '<>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'ne' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } +} + +function inverseOperandLower(): void +{ + if ( + version_compare( '8.3.12', PHP_VERSION, '<' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } + + if ( + version_compare( '8.3.12', PHP_VERSION, '>=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80312, max>', $x); + } + + if ( + version_compare( '8.3.12', PHP_VERSION, '>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80313, max>', $x); + } + +} From d198624f6514caec6fc025500dda4c6b9167c015 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 07:03:45 +0100 Subject: [PATCH 02/13] WIP: Create VersionCompareFunctionTypeSpecifyingExtension.php --- ...CompareFunctionTypeSpecifyingExtension.php | 149 ++++++++++++++++++ ...narrow-phpversionid-on-version-compare.php | 20 ++- 2 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php new file mode 100644 index 00000000000..594bc30c5b5 --- /dev/null +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -0,0 +1,149 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return strtolower($functionReflection->getName())==='version_compare' && $context->true(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$node->name instanceof Name) { + return new SpecifiedTypes(); + } + + $args = $node->getArgs(); + if (count($args) < 2) { + return new SpecifiedTypes(); + } + + $version1 = $args[0]->value; + $version2 = $args[1]->value; + + if ($scope->getNamespace() !== null) { + $constName = 'PHP_VERSION_ID'; + } else { + $constName = '\\PHP_VERSION_ID'; + } + + if ( + $version1 instanceof ConstFetch + && $version1->name->name === 'PHP_VERSION' + && $version2 instanceof String_ + ) { + $integerVersionRange = $this->getComparePhpVersionType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope); + + if ($integerVersionRange !== null) { + return $this->typeSpecifier->create( + new ConstFetch(new Name($constName)), + TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange), + $context, + $scope, + ); + } + } + + if ( + $version2 instanceof ConstFetch + && $version2->name->name === 'PHP_VERSION' + && $version1 instanceof String_ + ) { + $integerVersionRange = $this->getComparePhpVersionType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope); + if ($integerVersionRange !== null) { + return $this->typeSpecifier->create( + new ConstFetch(new Name($constName)), + TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange), + $context, + $scope, + ); + } + } + + return new SpecifiedTypes(); + } + + private function getComparePhpVersionType(string $value, ?Expr $operator, Scope $scope): ?Type + { + $parsedVersion = SimplePhpVersionParser::parseVersion($value); + if ($parsedVersion === null) { + return null; + } + + if ($operator !== null) { + $operators = $scope->getType($operator)->getConstantStrings(); + if (count($operators) !== 1) { + return null; + } + + $operatorString = $operators[0]->getValue(); + } else { + $operatorString = '<'; + } + + if (!in_array($operatorString, VersionCompareFunctionDynamicReturnTypeExtension::VALID_OPERATORS, true)) { + return null; + } + + if (in_array($operatorString, ['<', 'lt'], true)) { + return IntegerRangeType::fromInterval(null, $parsedVersion->getVersionId() - 1); + } + if (in_array($operatorString, ['<=', 'le'], true)) { + return IntegerRangeType::fromInterval(null, $parsedVersion->getVersionId()); + } + + if (in_array($operatorString, ['>', 'gt'], true)) { + return IntegerRangeType::fromInterval($parsedVersion->getVersionId() + 1, null); + } + if (in_array($operatorString, ['>=', 'ge'], true)) { + return IntegerRangeType::fromInterval($parsedVersion->getVersionId(), null); + } + + if ( + in_array($operatorString, ['==', '=', 'eq'], true) + ) { + return new ConstantIntegerType($parsedVersion->getVersionId()); + } + + return TypeCombinator::remove(new IntegerType(), new ConstantIntegerType($parsedVersion->getVersionId())); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php b/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php index d610c4c4280..66303526aae 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php @@ -12,28 +12,28 @@ function lower(): void version_compare( PHP_VERSION, '8.4', '<' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<50207, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'lt' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<50207, 80399>', $x); } if ( version_compare( PHP_VERSION, '8.4', '<=' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<50207, 80400>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'le' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<50207, 80400>', $x); } } @@ -138,5 +138,17 @@ function inverseOperandLower(): void $x = PHP_VERSION_ID; assertType('int<80313, max>', $x); } +} + +function narrow(): void { + if (PHP_VERSION_ID < 80000) { + return; + } + if ( + version_compare( PHP_VERSION, '8.4', '<' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } } From 07116d80ad4b06ea7c71f94d0e647f982b288a2a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:46:38 +0100 Subject: [PATCH 03/13] Update VersionCompareFunctionTypeSpecifyingExtension.php --- .../Php/VersionCompareFunctionTypeSpecifyingExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php index 594bc30c5b5..14caca4591a 100644 --- a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -71,7 +71,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && $version1->name->name === 'PHP_VERSION' && $version2 instanceof String_ ) { - $integerVersionRange = $this->getComparePhpVersionType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope); + $integerVersionRange = $this->getVersionCompareType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope); if ($integerVersionRange !== null) { return $this->typeSpecifier->create( @@ -88,7 +88,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && $version2->name->name === 'PHP_VERSION' && $version1 instanceof String_ ) { - $integerVersionRange = $this->getComparePhpVersionType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope); + $integerVersionRange = $this->getVersionCompareType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope); if ($integerVersionRange !== null) { return $this->typeSpecifier->create( new ConstFetch(new Name($constName)), @@ -102,7 +102,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } - private function getComparePhpVersionType(string $value, ?Expr $operator, Scope $scope): ?Type + private function getVersionCompareType(string $value, ?Expr $operator, Scope $scope): ?Type { $parsedVersion = SimplePhpVersionParser::parseVersion($value); if ($parsedVersion === null) { From e7e0d5285f503952a022429ddb128a8972ed44f5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:49:38 +0100 Subject: [PATCH 04/13] Update VersionCompareFunctionTypeSpecifyingExtension.php --- .../Php/VersionCompareFunctionTypeSpecifyingExtension.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php index 14caca4591a..ee0aa269573 100644 --- a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -74,9 +74,10 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $integerVersionRange = $this->getVersionCompareType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope); if ($integerVersionRange !== null) { + $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); return $this->typeSpecifier->create( new ConstFetch(new Name($constName)), - TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange), + $narrowedVersion, $context, $scope, ); @@ -90,9 +91,10 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ) { $integerVersionRange = $this->getVersionCompareType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope); if ($integerVersionRange !== null) { + $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); return $this->typeSpecifier->create( new ConstFetch(new Name($constName)), - TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange), + $narrowedVersion, $context, $scope, ); From b5ea2fac0e74821f66ff6d69e2a0eee2fabbf983 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:51:30 +0100 Subject: [PATCH 05/13] Create narrow-phpversionid-on-version-compare-global-scope.php --- ...sionid-on-version-compare-global-scope.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 narrow-phpversionid-on-version-compare-global-scope.php diff --git a/narrow-phpversionid-on-version-compare-global-scope.php b/narrow-phpversionid-on-version-compare-global-scope.php new file mode 100644 index 00000000000..8d610f85ea3 --- /dev/null +++ b/narrow-phpversionid-on-version-compare-global-scope.php @@ -0,0 +1,21 @@ +', $x); + + if ( + version_compare( PHP_VERSION, '8.4', '<' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80000, 80399>', $x); + } +} + From 7730f94db64f6f157831ae04b117017cfeb04e0c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:53:55 +0100 Subject: [PATCH 06/13] Update narrow-phpversionid-on-version-compare.php --- .../nsrt/narrow-phpversionid-on-version-compare.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php b/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php index 66303526aae..633e28ade8c 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php @@ -8,11 +8,14 @@ function lower(): void { + $x = PHP_VERSION_ID; + assertType('int<50207, 80599>', $x); + if ( version_compare( PHP_VERSION, '8.4', '<' ) ) { $x = PHP_VERSION_ID; - assertType('int<50207, 80599>', $x); + assertType('int<50207, 80399>', $x); } if ( @@ -149,6 +152,6 @@ function narrow(): void { version_compare( PHP_VERSION, '8.4', '<' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<80000, 80399>', $x); } } From f2d2c82aad76006b03584d5d4a53358a885c0606 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:08:23 +0100 Subject: [PATCH 07/13] move test into e2e --- .github/workflows/e2e-tests.yml | 4 ++++ e2e/version-compare-phpversionid/composer.json | 5 +++++ .../narrow-phpversionid-on-version-compare.php | 0 3 files changed, 9 insertions(+) create mode 100644 e2e/version-compare-phpversionid/composer.json rename {tests/PHPStan/Analyser/nsrt => e2e/version-compare-phpversionid}/narrow-phpversionid-on-version-compare.php (100%) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 99db5336ea1..217fdc83164 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -2042,6 +2042,10 @@ jobs: cd e2e/composer-version-config composer install ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/version-compare-phpversionid + composer install + ../../bin/phpstan analyze narrow-phpversionid-on-version-compare.php --level=0 - script: | cd e2e/bug13425 timeout 15 ../bashunit assert exit_code "1" "../../bin/phpstan analyze src/ plugins/" diff --git a/e2e/version-compare-phpversionid/composer.json b/e2e/version-compare-phpversionid/composer.json new file mode 100644 index 00000000000..e5f83a420c5 --- /dev/null +++ b/e2e/version-compare-phpversionid/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^8" + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php similarity index 100% rename from tests/PHPStan/Analyser/nsrt/narrow-phpversionid-on-version-compare.php rename to e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php From 5ecceb05bce91df4522fcc5168a0aefa0539c74c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:15:03 +0100 Subject: [PATCH 08/13] adjust --- e2e/version-compare-phpversionid/.gitignore | 2 ++ .../narrow-phpversionid-on-version-compare.php | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 e2e/version-compare-phpversionid/.gitignore diff --git a/e2e/version-compare-phpversionid/.gitignore b/e2e/version-compare-phpversionid/.gitignore new file mode 100644 index 00000000000..3a9875b460f --- /dev/null +++ b/e2e/version-compare-phpversionid/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php index 633e28ade8c..e680fd01445 100644 --- a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php +++ b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php @@ -8,35 +8,42 @@ function lower(): void { + // add a upper bound, so we don't need to adjust + // the test when PHPStan adds support for PHP8.6+ + if (PHP_VERSION_ID > 80599) { + return; + } + + // lower limit inferred from composer.json $x = PHP_VERSION_ID; - assertType('int<50207, 80599>', $x); + assertType('int<80000, 80599>', $x); if ( version_compare( PHP_VERSION, '8.4', '<' ) ) { $x = PHP_VERSION_ID; - assertType('int<50207, 80399>', $x); + assertType('int<80000, 80399>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'lt' ) ) { $x = PHP_VERSION_ID; - assertType('int<50207, 80399>', $x); + assertType('int<80000, 80399>', $x); } if ( version_compare( PHP_VERSION, '8.4', '<=' ) ) { $x = PHP_VERSION_ID; - assertType('int<50207, 80400>', $x); + assertType('int<80000, 80400>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'le' ) ) { $x = PHP_VERSION_ID; - assertType('int<50207, 80400>', $x); + assertType('int<80000, 80400>', $x); } } From b399a6d961938e931a9f42c4d3dd001cd4f4aa78 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:19:57 +0100 Subject: [PATCH 09/13] fix --- ...narrow-phpversionid-on-version-compare.php | 137 ------------------ ...CompareFunctionTypeSpecifyingExtension.php | 10 +- 2 files changed, 2 insertions(+), 145 deletions(-) diff --git a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php index e680fd01445..4335f9e55ca 100644 --- a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php +++ b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php @@ -24,141 +24,4 @@ function lower(): void $x = PHP_VERSION_ID; assertType('int<80000, 80399>', $x); } - - if ( - version_compare( PHP_VERSION, '8.4', 'lt' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80000, 80399>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', '<=' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80000, 80400>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', 'le' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80000, 80400>', $x); - } -} - -function greater(): void -{ - if ( - version_compare( PHP_VERSION, '8.4', '>' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80401, max>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', 'gt' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80401, max>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', '>=' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80400, max>', $x); - } - if ( - version_compare( PHP_VERSION, '8.4', 'ge' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80400, max>', $x); - } -} - -function equal(): void -{ - if ( - version_compare( PHP_VERSION, '8.4', '=' ) - ) { - $x = PHP_VERSION_ID; - assertType('80400', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', '==' ) - ) { - $x = PHP_VERSION_ID; - assertType('80400', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', 'eq' ) - ) { - $x = PHP_VERSION_ID; - assertType('80400', $x); - } -} - - -function not(): void -{ - if ( - version_compare( PHP_VERSION, '8.4', '!=' ) - ) { - $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', '<>' ) - ) { - $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); - } - - if ( - version_compare( PHP_VERSION, '8.4', 'ne' ) - ) { - $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); - } -} - -function inverseOperandLower(): void -{ - if ( - version_compare( '8.3.12', PHP_VERSION, '<' ) - ) { - $x = PHP_VERSION_ID; - assertType('int', $x); - } - - if ( - version_compare( '8.3.12', PHP_VERSION, '>=' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80312, max>', $x); - } - - if ( - version_compare( '8.3.12', PHP_VERSION, '>' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80313, max>', $x); - } -} - -function narrow(): void { - if (PHP_VERSION_ID < 80000) { - return; - } - - if ( - version_compare( PHP_VERSION, '8.4', '<' ) - ) { - $x = PHP_VERSION_ID; - assertType('int<80000, 80399>', $x); - } } diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php index ee0aa269573..f39f220eb65 100644 --- a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -60,12 +60,6 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $version1 = $args[0]->value; $version2 = $args[1]->value; - if ($scope->getNamespace() !== null) { - $constName = 'PHP_VERSION_ID'; - } else { - $constName = '\\PHP_VERSION_ID'; - } - if ( $version1 instanceof ConstFetch && $version1->name->name === 'PHP_VERSION' @@ -76,7 +70,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ($integerVersionRange !== null) { $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); return $this->typeSpecifier->create( - new ConstFetch(new Name($constName)), + new ConstFetch(new Name('\\PHP_VERSION_ID')), $narrowedVersion, $context, $scope, @@ -93,7 +87,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ($integerVersionRange !== null) { $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); return $this->typeSpecifier->create( - new ConstFetch(new Name($constName)), + new ConstFetch(new Name('\\PHP_VERSION_ID')), $narrowedVersion, $context, $scope, From 8c5758a899af291f5fa5697d9c06d233cfebbc3f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:26:04 +0100 Subject: [PATCH 10/13] cleanup --- ...VersionCompareFunctionTypeSpecifyingExtension.php | 12 ++++-------- tests/PHPStan/Analyser/data/bug-5639.php | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php index f39f220eb65..5b871413bab 100644 --- a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -16,7 +15,6 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\SimplePhpVersionParser; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerRangeType; @@ -25,16 +23,13 @@ use PHPStan\Type\TypeCombinator; use function count; use function in_array; +use function strtolower; #[AutowiredService] final class VersionCompareFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private TypeSpecifier $typeSpecifier; - - public function __construct(private ReflectionProvider $reflectionProvider) - { - } + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -43,7 +38,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { - return strtolower($functionReflection->getName())==='version_compare' && $context->true(); + return strtolower($functionReflection->getName()) === 'version_compare' && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes @@ -142,4 +137,5 @@ private function getVersionCompareType(string $value, ?Expr $operator, Scope $sc return TypeCombinator::remove(new IntegerType(), new ConstantIntegerType($parsedVersion->getVersionId())); } + } diff --git a/tests/PHPStan/Analyser/data/bug-5639.php b/tests/PHPStan/Analyser/data/bug-5639.php index c0eeed33704..740abfb9157 100644 --- a/tests/PHPStan/Analyser/data/bug-5639.php +++ b/tests/PHPStan/Analyser/data/bug-5639.php @@ -2,7 +2,7 @@ namespace Bug5639; -if (\version_compare(\PHP_VERSION, '7.0', '<')) +if (\version_compare(\PHP_VERSION, '7.0', '<')) // @phpstan-ignore function.impossibleType { class Foo extends \Exception { function __toString(): string { From 4056286401fe388660ea852606d6567417c31dc0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:55:20 +0100 Subject: [PATCH 11/13] Update narrow-phpversionid-on-version-compare.php --- ...narrow-phpversionid-on-version-compare.php | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php index 4335f9e55ca..e680fd01445 100644 --- a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php +++ b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php @@ -24,4 +24,141 @@ function lower(): void $x = PHP_VERSION_ID; assertType('int<80000, 80399>', $x); } + + if ( + version_compare( PHP_VERSION, '8.4', 'lt' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80000, 80399>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '<=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80000, 80400>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'le' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80000, 80400>', $x); + } +} + +function greater(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'gt' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '>=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80400, max>', $x); + } + if ( + version_compare( PHP_VERSION, '8.4', 'ge' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80400, max>', $x); + } +} + +function equal(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '=' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '==' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'eq' ) + ) { + $x = PHP_VERSION_ID; + assertType('80400', $x); + } +} + + +function not(): void +{ + if ( + version_compare( PHP_VERSION, '8.4', '!=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', '<>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } + + if ( + version_compare( PHP_VERSION, '8.4', 'ne' ) + ) { + $x = PHP_VERSION_ID; + assertType('int|int<80401, max>', $x); + } +} + +function inverseOperandLower(): void +{ + if ( + version_compare( '8.3.12', PHP_VERSION, '<' ) + ) { + $x = PHP_VERSION_ID; + assertType('int', $x); + } + + if ( + version_compare( '8.3.12', PHP_VERSION, '>=' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80312, max>', $x); + } + + if ( + version_compare( '8.3.12', PHP_VERSION, '>' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80313, max>', $x); + } +} + +function narrow(): void { + if (PHP_VERSION_ID < 80000) { + return; + } + + if ( + version_compare( PHP_VERSION, '8.4', '<' ) + ) { + $x = PHP_VERSION_ID; + assertType('int<80000, 80399>', $x); + } } From 60de2413be317a9358db5b800303321d68619042 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 09:58:33 +0100 Subject: [PATCH 12/13] mv --- .../narrow-phpversionid-on-version-compare-global-scope.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename narrow-phpversionid-on-version-compare-global-scope.php => e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare-global-scope.php (100%) diff --git a/narrow-phpversionid-on-version-compare-global-scope.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare-global-scope.php similarity index 100% rename from narrow-phpversionid-on-version-compare-global-scope.php rename to e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare-global-scope.php From b635d37770b7dc4c23c5d1f78591ad0ace8dc0cb Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Sun, 1 Nov 2026 12:08:05 +0000 Subject: [PATCH 13/13] Flip the operator when PHP_VERSION is the second version_compare() argument Bound the e2e expectations by the maximum supported PHP version, like lower() already does. Amp-Thread-ID: https://ampcode.com/threads/T-01a196f7-f93b-768c-b77e-ed3b410c5bf7 --- ...narrow-phpversionid-on-version-compare.php | 38 ++++++++++++++----- ...CompareFunctionTypeSpecifyingExtension.php | 22 +++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php index e680fd01445..643829eb6e1 100644 --- a/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php +++ b/e2e/version-compare-phpversionid/narrow-phpversionid-on-version-compare.php @@ -49,31 +49,37 @@ function lower(): void function greater(): void { + // add a upper bound, so we don't need to adjust + // the test when PHPStan adds support for PHP8.6+ + if (PHP_VERSION_ID > 80599) { + return; + } + if ( version_compare( PHP_VERSION, '8.4', '>' ) ) { $x = PHP_VERSION_ID; - assertType('int<80401, max>', $x); + assertType('int<80401, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'gt' ) ) { $x = PHP_VERSION_ID; - assertType('int<80401, max>', $x); + assertType('int<80401, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', '>=' ) ) { $x = PHP_VERSION_ID; - assertType('int<80400, max>', $x); + assertType('int<80400, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'ge' ) ) { $x = PHP_VERSION_ID; - assertType('int<80400, max>', $x); + assertType('int<80400, 80599>', $x); } } @@ -104,49 +110,61 @@ function equal(): void function not(): void { + // add a upper bound, so we don't need to adjust + // the test when PHPStan adds support for PHP8.6+ + if (PHP_VERSION_ID > 80599) { + return; + } + if ( version_compare( PHP_VERSION, '8.4', '!=' ) ) { $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); + assertType('int<80000, 80399>|int<80401, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', '<>' ) ) { $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); + assertType('int<80000, 80399>|int<80401, 80599>', $x); } if ( version_compare( PHP_VERSION, '8.4', 'ne' ) ) { $x = PHP_VERSION_ID; - assertType('int|int<80401, max>', $x); + assertType('int<80000, 80399>|int<80401, 80599>', $x); } } function inverseOperandLower(): void { + // add a upper bound, so we don't need to adjust + // the test when PHPStan adds support for PHP8.6+ + if (PHP_VERSION_ID > 80599) { + return; + } + if ( version_compare( '8.3.12', PHP_VERSION, '<' ) ) { $x = PHP_VERSION_ID; - assertType('int', $x); + assertType('int<80313, 80599>', $x); } if ( version_compare( '8.3.12', PHP_VERSION, '>=' ) ) { $x = PHP_VERSION_ID; - assertType('int<80312, max>', $x); + assertType('int<80000, 80312>', $x); } if ( version_compare( '8.3.12', PHP_VERSION, '>' ) ) { $x = PHP_VERSION_ID; - assertType('int<80313, max>', $x); + assertType('int<80000, 80311>', $x); } } diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php index 5b871413bab..c640c1e9d2c 100644 --- a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -29,6 +29,17 @@ final class VersionCompareFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { + private const INVERSE_OPERATORS = [ + '<' => '>', + 'lt' => 'gt', + '<=' => '>=', + 'le' => 'ge', + '>' => '<', + 'gt' => 'lt', + '>=' => '<=', + 'ge' => 'le', + ]; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -60,7 +71,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && $version1->name->name === 'PHP_VERSION' && $version2 instanceof String_ ) { - $integerVersionRange = $this->getVersionCompareType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope); + $integerVersionRange = $this->getVersionCompareType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope, false); if ($integerVersionRange !== null) { $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); @@ -78,7 +89,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && $version2->name->name === 'PHP_VERSION' && $version1 instanceof String_ ) { - $integerVersionRange = $this->getVersionCompareType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope); + $integerVersionRange = $this->getVersionCompareType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope, true); if ($integerVersionRange !== null) { $narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange); return $this->typeSpecifier->create( @@ -93,7 +104,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } - private function getVersionCompareType(string $value, ?Expr $operator, Scope $scope): ?Type + private function getVersionCompareType(string $value, ?Expr $operator, Scope $scope, bool $phpVersionIsSecondOperand): ?Type { $parsedVersion = SimplePhpVersionParser::parseVersion($value); if ($parsedVersion === null) { @@ -115,6 +126,11 @@ private function getVersionCompareType(string $value, ?Expr $operator, Scope $sc return null; } + if ($phpVersionIsSecondOperand) { + // version_compare('8.3', PHP_VERSION, '<') means PHP_VERSION > '8.3' + $operatorString = self::INVERSE_OPERATORS[$operatorString] ?? $operatorString; + } + if (in_array($operatorString, ['<', 'lt'], true)) { return IntegerRangeType::fromInterval(null, $parsedVersion->getVersionId() - 1); }