Skip to content

Commit a936524

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#12063: is_callable() false positive with union method names
- When ConstantArrayType::findTypeAndMethodNames() skips non-existent methods, isCallable() now accounts for those skipped entries - Added doFindTypeAndMethodNames() private helper with out parameter tracking whether any method names were skipped due to not existing - New regression test in tests/PHPStan/Rules/Comparison/data/bug-12063.php
1 parent 06ea1e1 commit a936524

File tree

3 files changed

+61
-2
lines changed

3 files changed

+61
-2
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 17 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,13 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
537544

538545
/** @return ConstantArrayTypeAndMethod[] */
539546
public function findTypeAndMethodNames(): array
547+
{
548+
$hasNonExistentMethod = false;
549+
return $this->doFindTypeAndMethodNames($hasNonExistentMethod);
550+
}
551+
552+
/** @return ConstantArrayTypeAndMethod[] */
553+
private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod): array
540554
{
541555
if (count($this->keyTypes) !== 2) {
542556
return [];
@@ -578,6 +592,7 @@ public function findTypeAndMethodNames(): array
578592
foreach ($methods->getConstantStrings() as $methodName) {
579593
$has = $type->hasMethod($methodName->getValue());
580594
if ($has->no()) {
595+
$hasNonExistentMethod = true;
581596
continue;
582597
}
583598

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+
}

0 commit comments

Comments
 (0)