Skip to content

Commit 13b9cb2

Browse files
committed
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
1 parent b12745b commit 13b9cb2

File tree

5 files changed

+98
-1
lines changed

5 files changed

+98
-1
lines changed

src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\Type\TypehintHelper;
2525
use function array_key_exists;
2626
use function array_map;
27+
use function function_exists;
2728
use function str_contains;
2829
use function strtolower;
2930

@@ -76,7 +77,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
7677
$isDeprecated = $reflectionFunction->isDeprecated();
7778
if ($reflectionFunction->getFileName() !== null) {
7879
$fileName = $reflectionFunction->getFileName();
79-
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) {
80+
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill') && !function_exists($functionName)) {
8081
return null;
8182
}
8283
$docComment = $reflectionFunction->getDocComment();
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\FunctionCallParametersCheck;
6+
use PHPStan\Rules\NullsafeCheck;
7+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
8+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleLevelHelper;
11+
use PHPStan\Testing\RuleTestCase;
12+
use function array_merge;
13+
14+
/**
15+
* @extends RuleTestCase<CallToFunctionParametersRule>
16+
*/
17+
class Bug14450Test extends RuleTestCase
18+
{
19+
20+
protected function getRule(): Rule
21+
{
22+
$reflectionProvider = self::createReflectionProvider();
23+
return new CallToFunctionParametersRule(
24+
$reflectionProvider,
25+
new FunctionCallParametersCheck(
26+
new RuleLevelHelper(
27+
$reflectionProvider,
28+
checkNullables: true,
29+
checkThisOnly: false,
30+
checkUnionTypes: true,
31+
checkExplicitMixed: false,
32+
checkImplicitMixed: false,
33+
checkBenevolentUnionTypes: false,
34+
discoveringSymbolsTip: true,
35+
),
36+
new NullsafeCheck(),
37+
new UnresolvableTypeHelper(),
38+
new PropertyReflectionFinder(),
39+
$reflectionProvider,
40+
checkArgumentTypes: true,
41+
checkArgumentsPassedByReference: true,
42+
checkExtraArguments: true,
43+
checkMissingTypehints: true,
44+
),
45+
);
46+
}
47+
48+
public function testBug14450(): void
49+
{
50+
$this->analyse([__DIR__ . '/data/bug-14450.php'], []);
51+
}
52+
53+
public static function getAdditionalConfigFiles(): array
54+
{
55+
return array_merge(
56+
parent::getAdditionalConfigFiles(),
57+
[__DIR__ . '/bug-14450.neon'],
58+
);
59+
}
60+
61+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
parameters:
2+
scanFiles:
3+
- data/bug-14450-stubs.php
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/**
4+
* Simulates incorrect vendor stubs (e.g. jetbrains/phpstorm-stubs)
5+
* where optional parameters lack default values.
6+
*
7+
* @param array|string $search
8+
* @param array|string $replace
9+
* @param array|string $subject
10+
* @param int $count
11+
* @return array|string
12+
*/
13+
function str_replace(array|string $search, array|string $replace, array|string $subject, &$count): array|string {}
14+
15+
/**
16+
* @return string
17+
*/
18+
function substr(string $string, int $offset, ?int $length): string {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// Built-in PHP functions called with optional parameters omitted.
4+
// These should not report errors even when incorrect vendor stubs
5+
// are present (e.g. jetbrains/phpstorm-stubs with missing defaults).
6+
7+
$a = str_replace('foo', 'bar', 'foobar');
8+
$b = substr('hello', 1);
9+
$c = array_keys(['a' => 1, 'b' => 2]);
10+
$d = strtotime('now');
11+
$e = array_filter([0, 1, 2, '', null]);
12+
$f = phpversion();
13+
$g = ['a' => 1, 'b' => 2];
14+
array_walk_recursive($g, function(&$v) { $v = ''; });

0 commit comments

Comments
 (0)