Skip to content

Always skip Symfony polyfill packages in SkipPolyfillSourceLocator regardless of configured phpVersion#5633

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-iuvip4i
Closed

Always skip Symfony polyfill packages in SkipPolyfillSourceLocator regardless of configured phpVersion#5633
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-iuvip4i

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When phpVersion is configured to a lower PHP version (e.g., 70400 for PHP 7.4), functions introduced in later PHP versions like str_ends_with() (PHP 8.0) were not reported as "Function not found" if a Symfony polyfill package providing that function was present in the project's dependencies.

The SkipPolyfillSourceLocator now unconditionally skips all Symfony polyfill packages, so function/class/constant availability is determined solely by PHPStan's version-aware stubs and signature maps.

Changes

  • Removed phpVersion conditions from SkipPolyfillSourceLocator::locateIdentifier() — polyfill packages are now always skipped regardless of configured phpVersion (src/Reflection/BetterReflection/SourceLocator/SkipPolyfillSourceLocator.php)
  • Removed PhpVersion constructor parameter from SkipPolyfillSourceLocator
  • Updated BetterReflectionSourceLocatorFactory to no longer pass PhpVersion to SkipPolyfillSourceLocator, and removed the now-unused PhpVersion dependency (src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php)
  • Updated TestCaseSourceLocatorFactory to match (src/Testing/TestCaseSourceLocatorFactory.php)
  • All six polyfill versions (php80, php81, php82, php83, php84, php85) were already listed in SkipPolyfillSourceLocator — the fix applies to all of them

Root cause

SkipPolyfillSourceLocator was designed to skip polyfill source files only when the configured phpVersion was >= the polyfill's target (e.g., skip symfony/polyfill-php80 only when phpVersion >= 80000). The intent was to avoid conflicts when the native function already existed. However, this logic meant that when phpVersion was lower than the polyfill's target, the polyfill's function/class declarations were treated as legitimate symbols — the BetterReflection reflector found them through the composer source locator and returned them as valid functions, bypassing PHPStan's version-aware checks.

The fix: always skip polyfill packages. PHPStan already has comprehensive, version-aware function/class databases (PhpStorm stubs with @since annotations, function signature maps with version delta files). These are the authoritative sources for what exists in a given PHP version. Polyfill source files should never influence function availability determination.

Test

  • CallToNonExistentFunctionRulePhp74Test::testBug11810 — verifies str_ends_with, str_starts_with, and str_contains are reported as "not found" when phpVersion is 70400 (PHP 7.4)
  • CallToNonExistentFunctionRulePhp80Test::testBug11810 — verifies array_is_list and enum_exists (PHP 8.1 functions) are reported as "not found" when phpVersion is 80000 (PHP 8.0)
  • Manually verified that PHP 8.0 functions are correctly recognized when phpVersion is 80000, and that classes from polyfills (e.g., ValueError) are also correctly blocked for lower PHP versions

Fixes phpstan/phpstan#11810

…regardless of configured phpVersion

- `SkipPolyfillSourceLocator` previously only skipped polyfill packages when the
  configured phpVersion was >= the polyfill's target version (e.g., polyfill-php80
  was only skipped when phpVersion >= 80000). This meant polyfill-provided functions
  like `str_ends_with` were treated as available when phpVersion was < 80000, even
  though they don't exist in the target PHP version.
- Remove all phpVersion conditions from `SkipPolyfillSourceLocator` so polyfill
  packages are always skipped. Function/class availability is determined by PHPStan's
  version-aware PhpStorm stubs and function signature maps, not by polyfill source
  files.
- Remove now-unused `PhpVersion` dependency from `SkipPolyfillSourceLocator` and
  `BetterReflectionSourceLocatorFactory`.
- The fix covers all polyfill versions (php80-php85) and all symbol types (functions,
  classes, constants).
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we know if the polyfill are loaded because of PHPStan or if they are installed/loaded voluntary by the user on his project ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Tests pass and static analysis is clean.

Now regarding the reviewer's question — there are no code changes needed here. The architecture already handles this correctly:

The answer to VincentLanglet's question: It doesn't matter whether polyfills are installed because of PHPStan or voluntarily by the user. SkipPolyfillSourceLocator only wraps the Composer source locators (which scan vendor/ packages). PHPStan's version-aware stubs (PhpStorm stubs with @since annotations + function signature maps) are the authoritative source for what exists at a given PHP version.

If a user voluntarily installs a polyfill and wants PHPStan to recognize those functions at a lower phpVersion, they have escape hatches that bypass SkipPolyfillSourceLocator entirely:

  • Add function declarations via stubFiles in their config
  • Include the polyfill's bootstrap file in scanFiles
  • Use bootstrapFiles

These paths go through separate locator chains that are NOT wrapped by SkipPolyfillSourceLocator.

Since there are no code changes to make (the review is a design question answered by the existing architecture), there's nothing to commit or push. The appropriate response is to explain this rationale on the PR.

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