From 13b9cb2470da48590be3acf31fd436393e6675c2 Mon Sep 17 00:00:00 2001 From: bartech Date: Fri, 10 Apr 2026 20:01:38 +0200 Subject: [PATCH] Fix built-in function signatures overridden by vendor stubs 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 --- .../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 a2f12f6daad..065bdb5d3a6 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -24,6 +24,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; @@ -76,7 +77,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..b2fca61c8b8 --- /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 = ''; });