Skip to content

Fix built-in function signatures overridden by vendor stubs#5440

Closed
bartech wants to merge 1 commit intophpstan:2.1.xfrom
bartech:fix/built-in-function-signature-override-2.1
Closed

Fix built-in function signatures overridden by vendor stubs#5440
bartech wants to merge 1 commit intophpstan:2.1.xfrom
bartech:fix/built-in-function-signature-override-2.1

Conversation

@bartech
Copy link
Copy Markdown

@bartech bartech commented Apr 10, 2026

Fixes phpstan/phpstan#14450

Problem

Since 2.1.29 (commit 326c6ec), NativeFunctionReflectionProvider skips signature map corrections for functions where isInternal() returns false. This was correct for userland classes/methods sharing names with PECL extensions (#13556, #12151, #11303, #9486).

However, when any vendor package ships incorrect stubs for core PHP functions (e.g. jetbrains/phpstorm-stubs with optional parameters lacking default values), BetterReflection resolves built-in functions like substr() and str_replace() from those vendor .php files. Since they come from files, isInternal() returns false, and PHPStan's correct signature corrections are skipped — causing false-positive arguments.count errors.

jetbrains/phpstorm-stubs is the most common case — it arrives as a transitive dependency via roave/better-reflection (12.6M downloads, 100+ dependents including roave/backward-compatibility-check, kcs/class-finder, php-tui/php-tui, lucasbustamante/stubz). Users never install it directly and have no reason to know it's in their vendor directory.

JetBrains/phpstorm-stubs#1863 fixes 199 incorrect signatures but is merged and not yet released (latest release: v2025.3, September 2025).

Fix

Added a function_exists() check: if the function exists in PHP's runtime, it is a core built-in that cannot be redeclared (function substr() {} is a fatal error), so signature corrections should always apply.

// Before:
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) {

// After:
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill') && !function_exists($functionName)) {

The isInternal() guard is preserved for functions from unloaded PECL extensions (e.g. swf_actiongotoframe, http_redirect), which CAN be redeclared — function_exists() correctly returns false for those.

Why function_exists() is safe

  • PhpStormStubsMap.php (loaded via Composer autoload_files) is a class with constant arrays — it does not define functions. function_exists() is unaffected by stub loading.
  • Core built-in functions always return function_exists() = true and cannot be redeclared.
  • Loaded PECL extension functions return function_exists() = true and isInternal() = true — already handled by the existing guard.
  • Unloaded PECL functions return function_exists() = false — guard correctly skips corrections.
  • Functions disabled via disable_functions are removed from PHP's function table — function_exists() = false — guard correctly skips corrections (they can be redeclared).

Test

Bug14450Test uses a scanFiles config to load a stub file with intentionally incorrect signatures (simulating any vendor package with wrong stubs). The test fails without the fix (reports str_replace and substr errors) and passes with the fix.

Reproduction repository: https://github.com/bartech/phpstan-14450-repro

When incorrect stubs for core PHP functions are present in vendor/ (e.g.
jetbrains/phpstorm-stubs installed transitively via roave/better-reflection),
BetterReflection resolves built-in PHP functions like substr() and
str_replace() from those stub files. Since these come from .php files,
isInternal() returns false, and the signature map corrections introduced
in 326c6ec are skipped.

This causes false positives when the vendor stubs have incorrect
signatures (e.g. optional parameters without default values).

The fix adds a function_exists() check: if the function exists in PHP's
runtime, it is a core built-in that cannot be redeclared, so signature
corrections should always apply. The isInternal() guard is preserved for
functions from unloaded PECL extensions, which CAN be redeclared by
userland code.

Fixes phpstan/phpstan#14450
@bartech
Copy link
Copy Markdown
Author

bartech commented Apr 10, 2026

@ondrejmirtes Sure thing but this throws away all symbol discovery from my dependencies.

./vendor/bin/phpstan analyse --configuration=.phpstan/local-config.neon --level=2
 Function wc_get_price_decimals not found.                                                            
          🪪  function.notFound                                                                                
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :810    Function wc_get_price_decimals not found.                                                            
          🪪  function.notFound                                                                                
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :821    Function wc_get_price_decimals not found.                                                            
          🪪  function.notFound                                                                                
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :826    Function wc_get_price_decimals not found.                                                            
          🪪  function.notFound                                                                                
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1377   Class WC_Product not found.                                                                          
          🪪  class.notFound                                                                                   
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1381   Call to method needs_shipping() on an unknown class WC_Product.                                      
          🪪  class.notFound                                                                                   
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1383   Call to method get_id() on an unknown class WC_Product.                                              
          🪪  class.notFound                                                                                   
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1393   Call to method get_meta() on an unknown class WC_Product.                                            
          🪪  class.notFound                                                                                   
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1394   Call to method get_price() on an unknown class WC_Product.                                           
          🪪  class.notFound                                                                                   
          💡  Learn more at https://phpstan.org/user-guide/discovering-symbols                                 
  :1412   Call to method get_meta() on an unknown class WC_Product.                                            
          🪪  class.notFound                                                      

What's wrong with the PR? I need symbol discovery. What I don't need is dependencies overwriting core php functions with wrong stubs definitions.

@ondrejmirtes
Copy link
Copy Markdown
Member

Make sure to use https://github.com/szepeviktor/phpstan-wordpress. Does it help?

@bartech
Copy link
Copy Markdown
Author

bartech commented Apr 10, 2026

I'm already using szepeviktor/phpstan-wordpress in the project. What I was missing was php-stubs/woocommerce-stubs instead of lucasbustamante/stubz that is using jetbrains/phpstorm-stubs. Nonetheless I still think this PR is solid and fixes issue for all projects that use jetbrains/phpstorm-stubs directly or indirectly. Why someone would need his own definitions for functions that can't be redeclared. You should seriously reconsider.

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