Skip to content

Commit

Permalink
Fix how union of callables is understood
Browse files Browse the repository at this point in the history
  • Loading branch information
mad-briller authored Feb 20, 2024
1 parent 2ece1f8 commit ffa7686
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 4 deletions.
5 changes: 3 additions & 2 deletions src/Type/Php/ArrayMapFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
$callableIsNull = $callableType->isNull()->yes();

if ($callableType->isCallable()->yes()) {
$valueType = new NeverType();
$valueTypes = [new NeverType()];
foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) {
$valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType());
$valueTypes[] = $parametersAcceptor->getReturnType();
}
$valueType = TypeCombinator::union(...$valueTypes);
} elseif ($callableIsNull) {
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) {
Expand Down
11 changes: 9 additions & 2 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use PHPStan\Type\Generic\TemplateUnionType;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
Expand Down Expand Up @@ -708,15 +709,21 @@ public function isCallable(): TrinaryLogic
*/
public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
{
$acceptors = [];

foreach ($this->types as $type) {
if ($type->isCallable()->no()) {
continue;
}

return $type->getCallableParametersAcceptors($scope);
$acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
}

if (count($acceptors) === 0) {
throw new ShouldNotHappenException();
}

throw new ShouldNotHappenException();
return $acceptors;
}

public function isCloneable(): TrinaryLogic
Expand Down
3 changes: 3 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/strtotime-return-type-extensions.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6633.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10283.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10442.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9972.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php');
Expand Down
25 changes: 25 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10283.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace Bug10283;

use function PHPStan\Testing\assertType;

class JsExpressionable {}

class Cl
{
/**
* @param \Closure(): JsExpressionable|\Closure(): int $fx
*/
public function test($fx): ?JsExpressionable
{
$res = $fx();
assertType('Bug10283\JsExpressionable|int', $res);

if (is_int($res)) {
return null;
}

return $res;
}
}
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10442.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Bug10442;

use function PHPStan\Testing\assertType;

/**
* @param callable(mixed): string|callable(mixed): int $callable
*/
function test(callable $callable): void
{
$val = array_map($callable, ['val', 'val2']);

assertType('array{int|string, int|string}', $val);
}
75 changes: 75 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6633.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);

namespace Bug6633;

use function PHPStan\Testing\assertType;

class CreateServiceSolrData
{
public ?string $name;
public ?string $version;
}

class CreateServiceRedisData
{
public ?string $name;
public ?string $version;
public ?bool $persistent;

}

class ServiceSolr
{
public function __construct(
private string $name,
private string $version,
) {}

public function touchAll() : string{
return $this->name . $this->version;
}
}

class ServiceRedis
{
public function __construct(
private string $name,
private string $version,
private bool $persistent,
) {}

public function touchAll() : string{
return $this->persistent ? $this->name : $this->version;
}
}

function test(?string $type = NULL) : void {
$types = [
'solr' => [
'label' => 'SOLR Search',
'data_class' => CreateServiceSolrData::class,
'to_entity' => function (CreateServiceSolrData $data) {
assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation");
return new ServiceSolr($data->name, $data->version);
},
],
'redis' => [
'label' => 'Redis',
'data_class' => CreateServiceRedisData::class,
'to_entity' => function (CreateServiceRedisData $data) {
assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation");
return new ServiceRedis($data->name, $data->version, $data->persistent);
},
],
];

if ($type === NULL || !isset($types[$type])) {
throw new \RuntimeException("404 or choice form here");
}

$data = new $types[$type]['data_class']();

$service = $types[$type]['to_entity']($data);

assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service);
}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,24 @@ public function testBug6485(): void
]);
}

public function testBug6633(): void
{
$this->analyse([__DIR__ . '/data/bug-6633.php'], []);
}

public function testBug3818b(): void
{
$this->analyse([__DIR__ . '/data/bug-3818b.php'], []);
}

public function testBug9594(): void
{
$this->analyse([__DIR__ . '/data/bug-9594.php'], []);
}

public function testBug9614(): void
{
$this->analyse([__DIR__ . '/data/bug-9614.php'], []);
}

}
29 changes: 29 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-3818b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace Bug3818b;

class A
{
}

class B
{
}

class Foo
{
public function handle(A|B $obj): void
{
$method = $obj instanceof A ? $this->handleA(...) : $this->handleB(...);

$method($obj);
}

private function handleA(A $a): void
{
}

private function handleB(B $b): void
{
}
}
71 changes: 71 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-6633.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace Bug6633\Rule;

class CreateServiceSolrData
{
public ?string $name;
public ?string $version;
}

class CreateServiceRedisData
{
public ?string $name;
public ?string $version;
public ?bool $persistent;

}

class ServiceSolr
{
public function __construct(
private string $name,
private string $version,
) {}

public function touchAll() : string{
return $this->name . $this->version;
}
}

class ServiceRedis
{
public function __construct(
private string $name,
private string $version,
private bool $persistent,
) {}

public function touchAll() : string{
return $this->persistent ? $this->name : $this->version;
}
}

function test(?string $type = NULL) : void {
$types = [
'solr' => [
'label' => 'SOLR Search',
'data_class' => CreateServiceSolrData::class,
'to_entity' => function (CreateServiceSolrData $data) {
assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation");
return new ServiceSolr($data->name, $data->version);
},
],
'redis' => [
'label' => 'Redis',
'data_class' => CreateServiceRedisData::class,
'to_entity' => function (CreateServiceRedisData $data) {
assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation");
return new ServiceRedis($data->name, $data->version, $data->persistent);
},
],
];

if ($type === NULL || !isset($types[$type])) {
throw new \RuntimeException("404 or choice form here");
}

$data = new $types[$type]['data_class']();

$service = $types[$type]['to_entity']($data);
}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-9594.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace Bug9594;

class HelloWorld
{
public function sayHello(): void
{
$data = [
[
'elements' => [1, 2, 3],
'greet' => fn (int $value) => 'I am '.$value,
],
[
'elements' => ['hello', 'world'],
'greet' => fn (string $value) => 'I am '.$value,
],
];

foreach ($data as $entry) {
foreach ($entry['elements'] as $element) {
$entry['greet']($element);
}
}
}
}
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-9614.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Bug9614;

class HelloWorld
{
public function sayHello(string $key, ?string $a = null, ?string $b = null): string
{
$funcs = [
'test' => function() {
return 'test';
},
'foo' => function($a) {
return 'foo';
},
'bar' => function($a, $b) {
return 'bar';
}
];

if (!isset($funcs[$key])) {
return '';
}

return $funcs[$key]($a, $b);
}
}

0 comments on commit ffa7686

Please sign in to comment.