From fbba5d5369ad8b4ac20911375569c6db52d19f52 Mon Sep 17 00:00:00 2001 From: bartech Date: Fri, 10 Apr 2026 19:28:41 +0200 Subject: [PATCH] Fix built-in function signatures overridden by vendor stubs When `jetbrains/phpstorm-stubs` is present in vendor/ as a transitive dependency (e.g. 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 because the JetBrains stubs have incorrect signatures (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 --- .../NativeFunctionReflectionProvider.php | 3 +- .../PHPStan/Rules/Functions/Bug14450Test.php | 61 +++++++++++++++++++ tests/PHPStan/Rules/Functions/bug-14450.neon | 3 + .../Rules/Functions/data/bug-14450-stubs.php | 18 ++++++ .../Rules/Functions/data/bug-14450.php | 14 +++++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/Bug14450Test.php create mode 100644 tests/PHPStan/Rules/Functions/bug-14450.neon create mode 100644 tests/PHPStan/Rules/Functions/data/bug-14450-stubs.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-14450.php diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index efb3b508db3..7a788de4087 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -25,6 +25,7 @@ use PHPStan\Type\TypehintHelper; use function array_key_exists; use function array_map; +use function function_exists; use function str_contains; use function strtolower; @@ -78,7 +79,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $isDeprecated = $reflectionFunction->isDeprecated(); if ($reflectionFunction->getFileName() !== null) { $fileName = $reflectionFunction->getFileName(); - if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) { + if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill') && !function_exists($functionName)) { return null; } $docComment = $reflectionFunction->getDocComment(); diff --git a/tests/PHPStan/Rules/Functions/Bug14450Test.php b/tests/PHPStan/Rules/Functions/Bug14450Test.php new file mode 100644 index 00000000000..5589af01f86 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/Bug14450Test.php @@ -0,0 +1,61 @@ + + */ +class Bug14450Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new CallToFunctionParametersRule( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ), + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + $reflectionProvider, + checkArgumentTypes: true, + checkArgumentsPassedByReference: true, + checkExtraArguments: true, + checkMissingTypehints: true, + ), + ); + } + + public function testBug14450(): void + { + $this->analyse([__DIR__ . '/data/bug-14450.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/bug-14450.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/bug-14450.neon b/tests/PHPStan/Rules/Functions/bug-14450.neon new file mode 100644 index 00000000000..56055c70cdb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/bug-14450.neon @@ -0,0 +1,3 @@ +parameters: + scanFiles: + - data/bug-14450-stubs.php diff --git a/tests/PHPStan/Rules/Functions/data/bug-14450-stubs.php b/tests/PHPStan/Rules/Functions/data/bug-14450-stubs.php new file mode 100644 index 00000000000..7a5c1e3e432 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-14450-stubs.php @@ -0,0 +1,18 @@ + 1, 'b' => 2]); +$d = strtotime('now'); +$e = array_filter([0, 1, 2, '', null]); +$f = phpversion(); +$g = ['a' => 1, 'b' => 2]; +array_walk_recursive($g, function(&$v) { $v = ''; });