Skip to content

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
phpstan-bot:create-pull-request/patch-zzs5skm
Open

Narrow PHP_VERSION_ID in scope based on version_compare(PHP_VERSION, ...) conditions#5609
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-zzs5skm

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan already narrows the PHP version in scope when code uses if (PHP_VERSION_ID >= 80000), but conditions using version_compare(PHP_VERSION, '8.0', '>=') or version_compare(PHP_VERSION, '8.0') === 1 were not narrowing PHP_VERSION_ID. This PR teaches PHPStan to recognize both the 3-argument and 2-argument forms of version_compare with PHP_VERSION and narrow the scope's PHP version accordingly.

Changes

  • New VersionCompareFunctionTypeSpecifyingExtension (src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php): Handles the 3-arg form version_compare(PHP_VERSION, 'x.y', op). Translates the operator and version string into a synthetic PHP_VERSION_ID op versionId comparison expression and delegates to TypeSpecifier::specifyTypesInCondition().

  • New VersionCompareHelper (src/Type/Php/VersionCompareHelper.php): Shared utility with:

    • parseVersionCompareFuncCall() — detects version_compare calls with PHP_VERSION as an argument
    • versionStringToId() — converts version strings like '8.4' to PHP_VERSION_ID integers (80400)
    • operatorToComparisonClass() — maps operator strings to AST comparison node classes
    • resultSetToPhpVersionIdComparison() — maps a subset of {-1, 0, 1} to equivalent PHP_VERSION_ID comparison expressions (used by the 2-arg form)
    • VALID_OPERATORS constant — moved from VersionCompareFunctionDynamicReturnTypeExtension
  • TypeSpecifier changes (src/Analyser/TypeSpecifier.php):

    • Added version_compare(PHP_VERSION, 'x.y') === N handling in resolveNormalizedIdentical for N in {-1, 0, 1}
    • Added specifyTypesForVersionCompare2Arg() private method for handling comparisons like version_compare(PHP_VERSION, 'x.y') < 0 and version_compare(PHP_VERSION, 'x.y') >= 0 in the Smaller/SmallerOrEqual branch
  • RemoveUnusedCodeByPhpVersionIdVisitor (src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php): Extended to evaluate version_compare(PHP_VERSION, 'x.y', op) conditions at parse time and remove dead branches, matching existing behavior for PHP_VERSION_ID comparisons.

  • Removed dead VALID_OPERATORS from VersionCompareFunctionDynamicReturnTypeExtension (moved to helper)

Root cause

The existing PHP version narrowing logic in TypeSpecifier worked by narrowing the PHP_VERSION_ID constant expression when encountering direct integer comparisons (e.g., PHP_VERSION_ID >= 80000). The MutatingScope::getPhpVersion() method then reads the narrowed PHP_VERSION_ID type to determine the PHP version in scope. However, version_compare() calls were never translated into equivalent PHP_VERSION_ID narrowing, so the scope's PHP version remained unaffected.

The fix bridges this gap by translating version_compare(PHP_VERSION, ...) calls into synthetic PHP_VERSION_ID comparison expressions that feed into the existing narrowing machinery.

Test

  • tests/PHPStan/Analyser/nsrt/version-compare-php-version-scope.php: Comprehensive NSRT test covering:

    • 3-arg form: >=, <, >, <=, == operators
    • 3-arg form with swapped arguments (version_compare('8.0', PHP_VERSION, '<='))
    • 3-arg form with patch version ('8.0.1')
    • 3-arg form with operator aliases (ge, lt)
    • 3-arg form with major-only version ('8')
    • 2-arg form: === 1, === -1, === 0, >= 0, < 0, > 0, <= 0
    • 2-arg form with swapped arguments
    • Playground reproducer: variadic parameter type narrowed by PHP version
  • tests/PHPStan/Parser/CleaningParserTest.php: Tests for RemoveUnusedCodeByPhpVersionIdVisitor with version_compare conditions (PHP 8.1 removes else branch, PHP 7.4 removes if branch)

Fixes phpstan/phpstan#13904

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants