Skip to content

Commit ffa7686

Browse files
authored
Fix how union of callables is understood
1 parent 2ece1f8 commit ffa7686

File tree

11 files changed

+303
-4
lines changed

11 files changed

+303
-4
lines changed

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3939
$callableIsNull = $callableType->isNull()->yes();
4040

4141
if ($callableType->isCallable()->yes()) {
42-
$valueType = new NeverType();
42+
$valueTypes = [new NeverType()];
4343
foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) {
44-
$valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType());
44+
$valueTypes[] = $parametersAcceptor->getReturnType();
4545
}
46+
$valueType = TypeCombinator::union(...$valueTypes);
4647
} elseif ($callableIsNull) {
4748
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
4849
foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) {

src/Type/UnionType.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use PHPStan\Type\Generic\TemplateUnionType;
2929
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
3030
use function array_map;
31+
use function array_merge;
3132
use function array_unique;
3233
use function array_values;
3334
use function count;
@@ -708,15 +709,21 @@ public function isCallable(): TrinaryLogic
708709
*/
709710
public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
710711
{
712+
$acceptors = [];
713+
711714
foreach ($this->types as $type) {
712715
if ($type->isCallable()->no()) {
713716
continue;
714717
}
715718

716-
return $type->getCallableParametersAcceptors($scope);
719+
$acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
720+
}
721+
722+
if (count($acceptors) === 0) {
723+
throw new ShouldNotHappenException();
717724
}
718725

719-
throw new ShouldNotHappenException();
726+
return $acceptors;
720727
}
721728

722729
public function isCloneable(): TrinaryLogic

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public function dataFileAsserts(): iterable
6161
yield from $this->gatherAssertTypes(__DIR__ . '/data/strtotime-return-type-extensions.php');
6262
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php');
6363
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php');
64+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6633.php');
65+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10283.php');
66+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10442.php');
6467
yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9972.php');
6568
yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php');
6669
yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php');
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug10283;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class JsExpressionable {}
8+
9+
class Cl
10+
{
11+
/**
12+
* @param \Closure(): JsExpressionable|\Closure(): int $fx
13+
*/
14+
public function test($fx): ?JsExpressionable
15+
{
16+
$res = $fx();
17+
assertType('Bug10283\JsExpressionable|int', $res);
18+
19+
if (is_int($res)) {
20+
return null;
21+
}
22+
23+
return $res;
24+
}
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Bug10442;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param callable(mixed): string|callable(mixed): int $callable
9+
*/
10+
function test(callable $callable): void
11+
{
12+
$val = array_map($callable, ['val', 'val2']);
13+
14+
assertType('array{int|string, int|string}', $val);
15+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6633;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class CreateServiceSolrData
8+
{
9+
public ?string $name;
10+
public ?string $version;
11+
}
12+
13+
class CreateServiceRedisData
14+
{
15+
public ?string $name;
16+
public ?string $version;
17+
public ?bool $persistent;
18+
19+
}
20+
21+
class ServiceSolr
22+
{
23+
public function __construct(
24+
private string $name,
25+
private string $version,
26+
) {}
27+
28+
public function touchAll() : string{
29+
return $this->name . $this->version;
30+
}
31+
}
32+
33+
class ServiceRedis
34+
{
35+
public function __construct(
36+
private string $name,
37+
private string $version,
38+
private bool $persistent,
39+
) {}
40+
41+
public function touchAll() : string{
42+
return $this->persistent ? $this->name : $this->version;
43+
}
44+
}
45+
46+
function test(?string $type = NULL) : void {
47+
$types = [
48+
'solr' => [
49+
'label' => 'SOLR Search',
50+
'data_class' => CreateServiceSolrData::class,
51+
'to_entity' => function (CreateServiceSolrData $data) {
52+
assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation");
53+
return new ServiceSolr($data->name, $data->version);
54+
},
55+
],
56+
'redis' => [
57+
'label' => 'Redis',
58+
'data_class' => CreateServiceRedisData::class,
59+
'to_entity' => function (CreateServiceRedisData $data) {
60+
assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation");
61+
return new ServiceRedis($data->name, $data->version, $data->persistent);
62+
},
63+
],
64+
];
65+
66+
if ($type === NULL || !isset($types[$type])) {
67+
throw new \RuntimeException("404 or choice form here");
68+
}
69+
70+
$data = new $types[$type]['data_class']();
71+
72+
$service = $types[$type]['to_entity']($data);
73+
74+
assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service);
75+
}

tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,24 @@ public function testBug6485(): void
286286
]);
287287
}
288288

289+
public function testBug6633(): void
290+
{
291+
$this->analyse([__DIR__ . '/data/bug-6633.php'], []);
292+
}
293+
294+
public function testBug3818b(): void
295+
{
296+
$this->analyse([__DIR__ . '/data/bug-3818b.php'], []);
297+
}
298+
299+
public function testBug9594(): void
300+
{
301+
$this->analyse([__DIR__ . '/data/bug-9594.php'], []);
302+
}
303+
304+
public function testBug9614(): void
305+
{
306+
$this->analyse([__DIR__ . '/data/bug-9614.php'], []);
307+
}
308+
289309
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3818b;
4+
5+
class A
6+
{
7+
}
8+
9+
class B
10+
{
11+
}
12+
13+
class Foo
14+
{
15+
public function handle(A|B $obj): void
16+
{
17+
$method = $obj instanceof A ? $this->handleA(...) : $this->handleB(...);
18+
19+
$method($obj);
20+
}
21+
22+
private function handleA(A $a): void
23+
{
24+
}
25+
26+
private function handleB(B $b): void
27+
{
28+
}
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6633\Rule;
4+
5+
class CreateServiceSolrData
6+
{
7+
public ?string $name;
8+
public ?string $version;
9+
}
10+
11+
class CreateServiceRedisData
12+
{
13+
public ?string $name;
14+
public ?string $version;
15+
public ?bool $persistent;
16+
17+
}
18+
19+
class ServiceSolr
20+
{
21+
public function __construct(
22+
private string $name,
23+
private string $version,
24+
) {}
25+
26+
public function touchAll() : string{
27+
return $this->name . $this->version;
28+
}
29+
}
30+
31+
class ServiceRedis
32+
{
33+
public function __construct(
34+
private string $name,
35+
private string $version,
36+
private bool $persistent,
37+
) {}
38+
39+
public function touchAll() : string{
40+
return $this->persistent ? $this->name : $this->version;
41+
}
42+
}
43+
44+
function test(?string $type = NULL) : void {
45+
$types = [
46+
'solr' => [
47+
'label' => 'SOLR Search',
48+
'data_class' => CreateServiceSolrData::class,
49+
'to_entity' => function (CreateServiceSolrData $data) {
50+
assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation");
51+
return new ServiceSolr($data->name, $data->version);
52+
},
53+
],
54+
'redis' => [
55+
'label' => 'Redis',
56+
'data_class' => CreateServiceRedisData::class,
57+
'to_entity' => function (CreateServiceRedisData $data) {
58+
assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation");
59+
return new ServiceRedis($data->name, $data->version, $data->persistent);
60+
},
61+
],
62+
];
63+
64+
if ($type === NULL || !isset($types[$type])) {
65+
throw new \RuntimeException("404 or choice form here");
66+
}
67+
68+
$data = new $types[$type]['data_class']();
69+
70+
$service = $types[$type]['to_entity']($data);
71+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9594;
4+
5+
class HelloWorld
6+
{
7+
public function sayHello(): void
8+
{
9+
$data = [
10+
[
11+
'elements' => [1, 2, 3],
12+
'greet' => fn (int $value) => 'I am '.$value,
13+
],
14+
[
15+
'elements' => ['hello', 'world'],
16+
'greet' => fn (string $value) => 'I am '.$value,
17+
],
18+
];
19+
20+
foreach ($data as $entry) {
21+
foreach ($entry['elements'] as $element) {
22+
$entry['greet']($element);
23+
}
24+
}
25+
}
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9614;
4+
5+
class HelloWorld
6+
{
7+
public function sayHello(string $key, ?string $a = null, ?string $b = null): string
8+
{
9+
$funcs = [
10+
'test' => function() {
11+
return 'test';
12+
},
13+
'foo' => function($a) {
14+
return 'foo';
15+
},
16+
'bar' => function($a, $b) {
17+
return 'bar';
18+
}
19+
];
20+
21+
if (!isset($funcs[$key])) {
22+
return '';
23+
}
24+
25+
return $funcs[$key]($a, $b);
26+
}
27+
}

0 commit comments

Comments
 (0)