Skip to content

Commit

Permalink
Fix for inferring closure parameter type from callable union
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 21, 2024
1 parent ffa7686 commit c10476d
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 6 deletions.
71 changes: 67 additions & 4 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Native\NativeMethodReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
Expand Down Expand Up @@ -3457,8 +3458,39 @@ private function processClosureNode(
}

$acceptors = $passedToType->getCallableParametersAcceptors($scope);
if (count($acceptors) === 1) {
$callableParameters = $acceptors[0]->getParameters();
if (count($acceptors) > 0) {
foreach ($acceptors as $acceptor) {
if ($callableParameters === null) {
$callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
$callableParameter->getName(),
$callableParameter->isOptional(),
$callableParameter->getType(),
$callableParameter->passedByReference(),
$callableParameter->isVariadic(),
$callableParameter->getDefaultValue(),
), $acceptor->getParameters());
continue;
}

$newParameters = [];
foreach ($acceptor->getParameters() as $i => $callableParameter) {
if (!array_key_exists($i, $callableParameters)) {
$newParameters[] = $callableParameter;
continue;
}

$newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection(
$callableParameter->getName(),
$callableParameter->isOptional(),
$callableParameter->getType(),
$callableParameter->passedByReference(),
$callableParameter->isVariadic(),
$callableParameter->getDefaultValue(),
));
}

$callableParameters = $newParameters;
}
}
}

Expand Down Expand Up @@ -3639,8 +3671,39 @@ private function processArrowFunctionNode(
}

$acceptors = $passedToType->getCallableParametersAcceptors($scope);
if (count($acceptors) === 1) {
$callableParameters = $acceptors[0]->getParameters();
if (count($acceptors) > 0) {
foreach ($acceptors as $acceptor) {
if ($callableParameters === null) {
$callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
$callableParameter->getName(),
$callableParameter->isOptional(),
$callableParameter->getType(),
$callableParameter->passedByReference(),
$callableParameter->isVariadic(),
$callableParameter->getDefaultValue(),
), $acceptor->getParameters());
continue;
}

$newParameters = [];
foreach ($acceptor->getParameters() as $i => $callableParameter) {
if (!array_key_exists($i, $callableParameters)) {
$newParameters[] = $callableParameter;
continue;
}

$newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection(
$callableParameter->getName(),
$callableParameter->isOptional(),
$callableParameter->getType(),
$callableParameter->passedByReference(),
$callableParameter->isVariadic(),
$callableParameter->getDefaultValue(),
));
}

$callableParameters = $newParameters;
}
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/Reflection/Native/NativeParameterReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class NativeParameterReflection implements ParameterReflection
{
Expand Down Expand Up @@ -50,6 +51,18 @@ public function getDefaultValue(): ?Type
return $this->defaultValue;
}

public function union(self $other): self
{
return new self(
$this->name,
$this->optional && $other->optional,
TypeCombinator::union($this->type, $other->type),
$this->passedByReference->combine($other->passedByReference),
$this->variadic && $other->variadic,
$this->optional && $other->optional ? $this->defaultValue : null,
);
}

/**
* @param mixed[] $properties
*/
Expand Down
4 changes: 3 additions & 1 deletion tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,9 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6672.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6687.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php');
if (PHP_VERSION_ID >= 70400) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php');
}

if (PHP_VERSION_ID < 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php7.php');
Expand Down
17 changes: 16 additions & 1 deletion tests/PHPStan/Analyser/data/callable-in-union.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php
<?php // lint >= 7.4

namespace CallableInUnion;

Expand All @@ -15,3 +15,18 @@ function acceptArrayOrCallable($_)
assertType('array<string, mixed>', $parameter);
return $parameter;
});

/**
* @param (callable(string): void)|callable(int): void $a
* @return void
*/
function acceptCallableOrCallableLikeArray($a): void
{

}

acceptCallableOrCallableLikeArray(function ($p) {
assertType('int|string', $p);
});

acceptCallableOrCallableLikeArray(fn ($p) => assertType('int|string', $p));

0 comments on commit c10476d

Please sign in to comment.