Skip to content

Commit 9a7e53f

Browse files
committed
Fix phpstan/phpstan#12063: Call to function is_callable() with array{...} will always evaluate to true (phpstan#5409)
1 parent f0c230a commit 9a7e53f

File tree

9 files changed

+127
-2
lines changed

9 files changed

+127
-2
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@ public function equals(Type $type): bool
494494

495495
public function isCallable(): TrinaryLogic
496496
{
497-
$typeAndMethods = $this->findTypeAndMethodNames();
497+
$hasNonExistentMethod = false;
498+
$typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod);
498499
if ($typeAndMethods === []) {
499500
return TrinaryLogic::createNo();
500501
}
@@ -504,7 +505,13 @@ public function isCallable(): TrinaryLogic
504505
$typeAndMethods,
505506
);
506507

507-
return TrinaryLogic::createYes()->and(...$results);
508+
$result = TrinaryLogic::createYes()->and(...$results);
509+
510+
if ($hasNonExistentMethod) {
511+
$result = $result->and(TrinaryLogic::createMaybe());
512+
}
513+
514+
return $result;
508515
}
509516

510517
public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
@@ -537,6 +544,12 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
537544

538545
/** @return ConstantArrayTypeAndMethod[] */
539546
public function findTypeAndMethodNames(): array
547+
{
548+
return $this->doFindTypeAndMethodNames();
549+
}
550+
551+
/** @return ConstantArrayTypeAndMethod[] */
552+
private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod = false): array
540553
{
541554
if (count($this->keyTypes) !== 2) {
542555
return [];
@@ -578,6 +591,7 @@ public function findTypeAndMethodNames(): array
578591
foreach ($methods->getConstantStrings() as $methodName) {
579592
$has = $type->hasMethod($methodName->getValue());
580593
if ($has->no()) {
594+
$hasNonExistentMethod = true;
581595
continue;
582596
}
583597

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,4 +1214,10 @@ public function testBug13799(): void
12141214
]);
12151215
}
12161216

1217+
public function testBug12063(): void
1218+
{
1219+
$this->treatPhpDocTypesAsCertain = true;
1220+
$this->analyse([__DIR__ . '/data/bug-12063.php'], []);
1221+
}
1222+
12171223
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug12063;
4+
5+
use BadFunctionCallException;
6+
7+
final class View
8+
{
9+
public function existingMethod(): void
10+
{
11+
}
12+
}
13+
14+
final class TwigExtension
15+
{
16+
private View $viewFunctions;
17+
18+
public function __construct(View $viewFunctions)
19+
{
20+
$this->viewFunctions = $viewFunctions;
21+
}
22+
23+
public function iterateFunctions(): void
24+
{
25+
$functionMappings = [
26+
'i_exist' => 'existingMethod',
27+
'i_dont_exist' => 'nonExistingMethod'
28+
];
29+
30+
$functions = [];
31+
foreach ($functionMappings as $nameFrom => $nameTo) {
32+
$callable = [$this->viewFunctions, $nameTo];
33+
if (!is_callable($callable)) {
34+
throw new BadFunctionCallException("Function $nameTo does not exist in view functions");
35+
}
36+
}
37+
}
38+
}

tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,16 @@ public function testPipeOperator(): void
364364
]);
365365
}
366366

367+
public function testBug4608(): void
368+
{
369+
$this->analyse([__DIR__ . '/data/bug-4608-callables.php'], [
370+
[
371+
"Trying to invoke array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-callables.php:5, 'abc'|'not_abc'} but it might not be a callable.",
372+
11,
373+
],
374+
]);
375+
}
376+
367377
public function testMaybeNotCallable(): void
368378
{
369379
$errors = [];

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2817,4 +2817,15 @@ public function testBug14312b(): void
28172817
$this->analyse([__DIR__ . '/data/bug-14312b.php'], []);
28182818
}
28192819

2820+
public function testBug4608(): void
2821+
{
2822+
$paramName = PHP_VERSION_ID >= 80000 ? 'callback' : 'function';
2823+
$this->analyse([__DIR__ . '/data/bug-4608-call-user-func.php'], [
2824+
[
2825+
sprintf("Parameter #1 \$%s of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", $paramName),
2826+
11,
2827+
],
2828+
]);
2829+
}
2830+
28202831
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug4608CallUserFunc;
4+
5+
$c = new class {
6+
public function abc(): void {}
7+
};
8+
9+
$s = rand(0, 1) ? 'abc' : 'not_abc';
10+
11+
call_user_func([$c, $s]);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug4608Callables;
4+
5+
$c = new class {
6+
public function abc(): void {}
7+
};
8+
9+
$s = rand(0, 1) ? 'abc' : 'not_abc';
10+
11+
[$c, $s]();

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3962,4 +3962,17 @@ public function testBug11463(): void
39623962
]);
39633963
}
39643964

3965+
public function testBug4608(): void
3966+
{
3967+
$this->checkThisOnly = false;
3968+
$this->checkNullables = true;
3969+
$this->checkUnionTypes = true;
3970+
$this->analyse([__DIR__ . '/data/bug-4608.php'], [
3971+
[
3972+
'Call to an undefined method class@anonymous/tests/PHPStan/Rules/Methods/data/bug-4608.php:5::not_abc().',
3973+
11,
3974+
],
3975+
]);
3976+
}
3977+
39653978
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug4608;
4+
5+
$c = new class {
6+
public function abc(): void {}
7+
};
8+
9+
$s = rand(0, 1) ? 'abc' : 'not_abc';
10+
11+
$c->{$s}();

0 commit comments

Comments
 (0)