Narrow PHP_VERSION_ID in scope based on version_compare(PHP_VERSION, ...) conditions#5609
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Open
Narrow PHP_VERSION_ID in scope based on version_compare(PHP_VERSION, ...) conditions#5609phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
PHP_VERSION_ID in scope based on version_compare(PHP_VERSION, ...) conditions#5609phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…N, ...)` conditions - Add `VersionCompareFunctionTypeSpecifyingExtension` for the 3-arg form (`version_compare(PHP_VERSION, '8.4', '<')`) — translates the comparison operator and version string into a synthetic `PHP_VERSION_ID` comparison and delegates to TypeSpecifier - Handle the 2-arg form (`version_compare(PHP_VERSION, '8.4') === 1`, `>= 0`, `< 0`, etc.) in TypeSpecifier's `resolveNormalizedIdentical` and Smaller/SmallerOrEqual branches by mapping the result set to an equivalent `PHP_VERSION_ID` comparison - Extract `VersionCompareHelper` with shared parsing, version-string-to-ID conversion, operator mapping, and `VALID_OPERATORS` constant - Extend `RemoveUnusedCodeByPhpVersionIdVisitor` to also remove dead branches guarded by `version_compare(PHP_VERSION, '8.1', '>=')` at parse time - All operator aliases (`lt`, `le`, `gt`, `ge`, `eq`, `ne`) are supported - Both argument orders (PHP_VERSION as first or second arg) are supported
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PHPStan already narrows the PHP version in scope when code uses
if (PHP_VERSION_ID >= 80000), but conditions usingversion_compare(PHP_VERSION, '8.0', '>=')orversion_compare(PHP_VERSION, '8.0') === 1were not narrowingPHP_VERSION_ID. This PR teaches PHPStan to recognize both the 3-argument and 2-argument forms ofversion_comparewithPHP_VERSIONand narrow the scope's PHP version accordingly.Changes
New
VersionCompareFunctionTypeSpecifyingExtension(src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php): Handles the 3-arg formversion_compare(PHP_VERSION, 'x.y', op). Translates the operator and version string into a syntheticPHP_VERSION_ID op versionIdcomparison expression and delegates toTypeSpecifier::specifyTypesInCondition().New
VersionCompareHelper(src/Type/Php/VersionCompareHelper.php): Shared utility with:parseVersionCompareFuncCall()— detectsversion_comparecalls withPHP_VERSIONas an argumentversionStringToId()— converts version strings like'8.4'to PHP_VERSION_ID integers (80400)operatorToComparisonClass()— maps operator strings to AST comparison node classesresultSetToPhpVersionIdComparison()— maps a subset of{-1, 0, 1}to equivalentPHP_VERSION_IDcomparison expressions (used by the 2-arg form)VALID_OPERATORSconstant — moved fromVersionCompareFunctionDynamicReturnTypeExtensionTypeSpecifier changes (
src/Analyser/TypeSpecifier.php):version_compare(PHP_VERSION, 'x.y') === Nhandling inresolveNormalizedIdenticalfor N in {-1, 0, 1}specifyTypesForVersionCompare2Arg()private method for handling comparisons likeversion_compare(PHP_VERSION, 'x.y') < 0andversion_compare(PHP_VERSION, 'x.y') >= 0in the Smaller/SmallerOrEqual branchRemoveUnusedCodeByPhpVersionIdVisitor(src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php): Extended to evaluateversion_compare(PHP_VERSION, 'x.y', op)conditions at parse time and remove dead branches, matching existing behavior forPHP_VERSION_IDcomparisons.Removed dead
VALID_OPERATORSfromVersionCompareFunctionDynamicReturnTypeExtension(moved to helper)Root cause
The existing PHP version narrowing logic in TypeSpecifier worked by narrowing the
PHP_VERSION_IDconstant expression when encountering direct integer comparisons (e.g.,PHP_VERSION_ID >= 80000). TheMutatingScope::getPhpVersion()method then reads the narrowedPHP_VERSION_IDtype to determine the PHP version in scope. However,version_compare()calls were never translated into equivalentPHP_VERSION_IDnarrowing, so the scope's PHP version remained unaffected.The fix bridges this gap by translating
version_compare(PHP_VERSION, ...)calls into syntheticPHP_VERSION_IDcomparison expressions that feed into the existing narrowing machinery.Test
tests/PHPStan/Analyser/nsrt/version-compare-php-version-scope.php: Comprehensive NSRT test covering:>=,<,>,<=,==operatorsversion_compare('8.0', PHP_VERSION, '<='))'8.0.1')ge,lt)'8')=== 1,=== -1,=== 0,>= 0,< 0,> 0,<= 0tests/PHPStan/Parser/CleaningParserTest.php: Tests forRemoveUnusedCodeByPhpVersionIdVisitorwithversion_compareconditions (PHP 8.1 removes else branch, PHP 7.4 removes if branch)Fixes phpstan/phpstan#13904