Skip to content

Commit d69b80b

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 0e505f4 + d5c05eb commit d69b80b

4 files changed

Lines changed: 202 additions & 2 deletions

File tree

src/Analyser/ExprHandler/ArrayHandler.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace PHPStan\Analyser\ExprHandler;
44

5+
use PhpParser\Node\Arg;
56
use PhpParser\Node\Expr;
67
use PhpParser\Node\Expr\Array_;
8+
use PhpParser\Node\Expr\FuncCall;
9+
use PhpParser\Node\Name\FullyQualified;
710
use PhpParser\Node\Stmt;
811
use PHPStan\Analyser\ExpressionContext;
912
use PHPStan\Analyser\ExpressionResult;
@@ -15,8 +18,11 @@
1518
use PHPStan\Node\LiteralArrayItem;
1619
use PHPStan\Node\LiteralArrayNode;
1720
use PHPStan\Reflection\InitializerExprTypeResolver;
21+
use PHPStan\Type\CallableType;
1822
use PHPStan\Type\Type;
23+
use PHPStan\Type\TypeCombinator;
1924
use function array_merge;
25+
use function count;
2026

2127
/**
2228
* @implements ExprHandler<Array_>
@@ -38,7 +44,26 @@ public function supports(Expr $expr): bool
3844

3945
public function resolveType(MutatingScope $scope, Expr $expr): Type
4046
{
41-
return $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr));
47+
$type = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr));
48+
49+
if (
50+
count($expr->items) === 2
51+
&& isset($expr->items[0], $expr->items[1])
52+
&& $type->isCallable()->maybe()
53+
) {
54+
$isCallableCall = new FuncCall(
55+
new FullyQualified('is_callable'),
56+
[new Arg($expr)],
57+
);
58+
if (
59+
$scope->hasExpressionType($isCallableCall)->yes()
60+
&& $scope->getType($isCallableCall)->isTrue()->yes()
61+
) {
62+
$type = TypeCombinator::intersect($type, new CallableType());
63+
}
64+
}
65+
66+
return $type;
4267
}
4368

4469
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult

src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpParser\Node\Expr\Array_;
77
use PhpParser\Node\Expr\FuncCall;
88
use PhpParser\Node\Name;
9+
use PhpParser\Node\Name\FullyQualified;
910
use PHPStan\Analyser\Scope;
1011
use PHPStan\Analyser\SpecifiedTypes;
1112
use PHPStan\Analyser\TypeSpecifier;
@@ -15,6 +16,7 @@
1516
use PHPStan\Reflection\FunctionReflection;
1617
use PHPStan\ShouldNotHappenException;
1718
use PHPStan\Type\CallableType;
19+
use PHPStan\Type\Constant\ConstantBooleanType;
1820
use PHPStan\Type\FunctionTypeSpecifyingExtension;
1921
use function count;
2022
use function strtolower;
@@ -57,7 +59,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
5759
new Arg($value->items[0]->value),
5860
new Arg($value->items[1]->value),
5961
]);
60-
return $this->methodExistsExtension->specifyTypes($functionReflection, $functionCall, $scope, $context);
62+
$methodExistsTypes = $this->methodExistsExtension->specifyTypes($functionReflection, $functionCall, $scope, $context);
63+
64+
return $methodExistsTypes->unionWith($this->typeSpecifier->create(
65+
new FuncCall(new FullyQualified('is_callable'), [
66+
new Arg($value),
67+
]),
68+
new ConstantBooleanType(true),
69+
$context,
70+
$scope,
71+
));
6172
}
6273

6374
return $this->typeSpecifier->create($value, new CallableType(), $context, $scope);
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug4510;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
public function existingMethod(): void {}
10+
11+
public function doSomething(string $method): void {
12+
if (!method_exists($this, $method)) {
13+
return;
14+
}
15+
16+
[$this, $method](); // error - method_exists doesn't imply callable
17+
}
18+
}
19+
20+
function testMethodExists(string $method): void {
21+
$instance = new HelloWorld();
22+
if (!method_exists($instance, $method)) {
23+
return;
24+
}
25+
26+
assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]);
27+
[$instance, $method](); // error - method_exists doesn't imply callable
28+
}
29+
30+
function testIsCallableInlineArray(string $method): void {
31+
$instance = new HelloWorld();
32+
if (!is_callable([$instance, $method])) {
33+
return;
34+
}
35+
36+
assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [$instance, $method]);
37+
[$instance, $method](); // ok - is_callable verifies callability
38+
}
39+
40+
function testMethodExistsWithClassString(string $method): void {
41+
if (!method_exists(HelloWorld::class, $method)) {
42+
return;
43+
}
44+
45+
assertType("array{'Bug4510\\\\HelloWorld', string}", [HelloWorld::class, $method]);
46+
[HelloWorld::class, $method](); // error - method_exists doesn't imply callable
47+
}
48+
49+
function testIsCallableWithClassString(string $method): void {
50+
if (!is_callable([HelloWorld::class, $method])) {
51+
return;
52+
}
53+
54+
assertType("list{'Bug4510\\\\HelloWorld', string}&callable(): mixed", [HelloWorld::class, $method]);
55+
[HelloWorld::class, $method](); // ok - is_callable verifies callability
56+
}
57+
58+
function testIsCallableExplicitKeys(string $method): void {
59+
$instance = new HelloWorld();
60+
if (!is_callable([0 => $instance, 1 => $method])) {
61+
return;
62+
}
63+
64+
assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [0 => $instance, 1 => $method]);
65+
[0 => $instance, 1 => $method](); // ok - is_callable verifies callability
66+
}
67+
68+
function testIsCallableExplicitKeysWithClassString(string $method): void {
69+
if (!is_callable([0 => HelloWorld::class, 1 => $method])) {
70+
return;
71+
}
72+
73+
assertType("list{'Bug4510\\\\HelloWorld', string}&callable(): mixed", [0 => HelloWorld::class, 1 => $method]);
74+
[0 => HelloWorld::class, 1 => $method](); // ok - is_callable verifies callability
75+
}
76+
77+
function testWithDynamicMethodExistsAndVariable(string $method): void {
78+
$instance = new HelloWorld();
79+
$callable = [$instance, $method];
80+
if (!is_callable($callable)) {
81+
return;
82+
}
83+
84+
$callable(); // ok - is_callable on variable already worked
85+
}
86+
87+
function testMethodExistsInElseBranch(string $method): void {
88+
$instance = new HelloWorld();
89+
if (method_exists($instance, $method)) {
90+
[$instance, $method](); // error - method_exists doesn't imply callable
91+
}
92+
}
93+
94+
function testIsCallableInElseBranch(string $method): void {
95+
$instance = new HelloWorld();
96+
if (is_callable([$instance, $method])) {
97+
[$instance, $method](); // ok - is_callable verifies callability
98+
}
99+
}
100+
101+
function testIsCallableNamedArg(string $method): void {
102+
$instance = new HelloWorld();
103+
if (!is_callable(value: [$instance, $method])) {
104+
return;
105+
}
106+
107+
assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [$instance, $method]);
108+
[$instance, $method](); // ok - is_callable verifies callability
109+
}
110+
111+
function testMethodExistsNamedArgs(string $method): void {
112+
$instance = new HelloWorld();
113+
if (!method_exists(object_or_class: $instance, method: $method)) {
114+
return;
115+
}
116+
117+
assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]);
118+
[$instance, $method](); // error - method_exists doesn't imply callable
119+
}
120+
121+
function testNoMethodExists(string $method): void {
122+
$instance = new HelloWorld();
123+
assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]);
124+
}
125+
126+
function testIsCallableFalseBranch(string $method): void {
127+
$instance = new HelloWorld();
128+
if (is_callable([$instance, $method])) {
129+
return;
130+
}
131+
132+
assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]);
133+
[$instance, $method](); // error - is_callable was false
134+
}

tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,36 @@ public function testBug4608(): void
386386
]);
387387
}
388388

389+
public function testBug4510(): void
390+
{
391+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4510.php'], [
392+
[
393+
'Trying to invoke array{$this(Bug4510\HelloWorld), string} but it might not be a callable.',
394+
16,
395+
],
396+
[
397+
'Trying to invoke array{Bug4510\HelloWorld, string} but it might not be a callable.',
398+
27,
399+
],
400+
[
401+
"Trying to invoke array{'Bug4510\\\HelloWorld', string} but it might not be a callable.",
402+
46,
403+
],
404+
[
405+
'Trying to invoke array{Bug4510\HelloWorld, string} but it might not be a callable.',
406+
90,
407+
],
408+
[
409+
'Trying to invoke array{Bug4510\HelloWorld, string} but it might not be a callable.',
410+
118,
411+
],
412+
[
413+
'Trying to invoke array{Bug4510\HelloWorld, string} but it might not be a callable.',
414+
133,
415+
],
416+
]);
417+
}
418+
389419
public function testMaybeNotCallable(): void
390420
{
391421
$errors = [];

0 commit comments

Comments
 (0)