Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make array_map understand union of callables. #2902

Merged
merged 2 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}