Skip to content

Commit fbba5d5

Browse files
committed
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
1 parent fa01947 commit fbba5d5

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
@@ -25,6 +25,7 @@
2525
use PHPStan\Type\TypehintHelper;
2626
use function array_key_exists;
2727
use function array_map;
28+
use function function_exists;
2829
use function str_contains;
2930
use function strtolower;
3031

@@ -78,7 +79,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
7879
$isDeprecated = $reflectionFunction->isDeprecated();
7980
if ($reflectionFunction->getFileName() !== null) {
8081
$fileName = $reflectionFunction->getFileName();
81-
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) {
82+
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill') && !function_exists($functionName)) {
8283
return null;
8384
}
8485
$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 vendor/jetbrains/phpstorm-stubs/standard/standard_1.php
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 jetbrains/phpstorm-stubs
5+
// is present in vendor/ with incorrect signatures (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)