Skip to content

Commit

Permalink
Resolving type of closure - get $passedToType from `inFunctionCalls…
Browse files Browse the repository at this point in the history
…Stack`
  • Loading branch information
ondrejmirtes committed May 17, 2024
1 parent 0159d34 commit ca41b7d
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public static function reorderNewArguments(
* @param Arg[] $callArgs
* @return ?array<int, Arg>
*/
private static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
{
if (count($callArgs) === 0) {
return [];
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/DirectInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function __construct(
* @param array<string, ExpressionTypeHolder> $expressionTypes
* @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
* @param list<array{FunctionReflection|MethodReflection, ParameterReflection|null}> $inFunctionCallsStack
* @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
* @param array<string, true> $currentlyAssignedExpressions
* @param array<string, true> $currentlyAllowedUndefinedExpressions
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/InternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface InternalScopeFactory
* @param list<string> $inClosureBindScopeClasses
* @param array<string, true> $currentlyAssignedExpressions
* @param array<string, true> $currentlyAllowedUndefinedExpressions
* @param list<array{MethodReflection|FunctionReflection, ParameterReflection|null}> $inFunctionCallsStack
* @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
*/
public function create(
ScopeContext $context,
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/LazyInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function __construct(
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
* @param array<string, true> $currentlyAssignedExpressions
* @param array<string, true> $currentlyAllowedUndefinedExpressions
* @param list<array{FunctionReflection|MethodReflection, ParameterReflection|null}> $inFunctionCallsStack
* @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
*/
public function create(
ScopeContext $context,
Expand Down
37 changes: 21 additions & 16 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
use stdClass;
use Throwable;
use function abs;
use function array_filter;
use function array_key_exists;
use function array_key_first;
use function array_keys;
Expand Down Expand Up @@ -186,7 +187,7 @@ class MutatingScope implements Scope
* @param array<string, true> $currentlyAssignedExpressions
* @param array<string, true> $currentlyAllowedUndefinedExpressions
* @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
* @param list<array{MethodReflection|FunctionReflection, ParameterReflection|null}> $inFunctionCallsStack
* @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
*/
public function __construct(
private InternalScopeFactory $scopeFactory,
Expand Down Expand Up @@ -1233,6 +1234,14 @@ private function resolveType(string $exprString, Expr $node): Type
foreach ($arrayMapArgs as $funcCallArg) {
$callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
}
} else {
$inFunctionCallsStackCount = count($this->inFunctionCallsStack);
if ($inFunctionCallsStackCount > 0) {
[, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1];
if ($inParameter !== null) {
$callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType());
}
}
}

if ($node instanceof Expr\ArrowFunction) {
Expand Down Expand Up @@ -2590,14 +2599,14 @@ public function hasExpressionType(Expr $node): TrinaryLogic
}

/**
* @param MethodReflection|FunctionReflection $reflection
* @param MethodReflection|FunctionReflection|null $reflection
*/
public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self
{
$stack = $this->inFunctionCallsStack;
$stack[] = [$reflection, $parameter];

$scope = $this->scopeFactory->create(
return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
Expand All @@ -2615,19 +2624,14 @@ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter)
$this->parentScope,
$this->nativeTypesPromoted,
);
$scope->resolvedTypes = $this->resolvedTypes;
$scope->truthyScopes = $this->truthyScopes;
$scope->falseyScopes = $this->falseyScopes;

return $scope;
}

public function popInFunctionCall(): self
{
$stack = $this->inFunctionCallsStack;
array_pop($stack);

$scope = $this->scopeFactory->create(
return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
Expand All @@ -2645,11 +2649,6 @@ public function popInFunctionCall(): self
$this->parentScope,
$this->nativeTypesPromoted,
);
$scope->resolvedTypes = $this->resolvedTypes;
$scope->truthyScopes = $this->truthyScopes;
$scope->falseyScopes = $this->falseyScopes;

return $scope;
}

/** @api */
Expand Down Expand Up @@ -2677,12 +2676,18 @@ public function isInClassExists(string $className): bool

public function getFunctionCallStack(): array
{
return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack);
return array_values(array_filter(
array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack),
static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null,
));
}

public function getFunctionCallStackWithParameters(): array
{
return $this->inFunctionCallsStack;
return array_values(array_filter(
$this->inFunctionCallsStack,
static fn ($item) => $item[0] !== null,
));
}

/** @api */
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4157,7 +4157,7 @@ private function processArrowFunctionNode(
* @param Node\Arg[] $args
* @return ParameterReflection[]|null
*/
private function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
{
$callableParameters = null;
if ($args !== null) {
Expand Down
40 changes: 35 additions & 5 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Closure;
use PhpParser\Node;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
use PHPStan\Parser\ArrayFilterArgVisitor;
Expand Down Expand Up @@ -336,16 +338,44 @@ public static function selectFromArgs(
}
}

$reorderedArgs = $args;
$parameters = null;
$singleParametersAcceptor = null;
if (count($parametersAcceptors) === 1) {
$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args);
$singleParametersAcceptor = $parametersAcceptors[0];
}

$hasName = false;
foreach ($args as $i => $arg) {
$type = $scope->getType($arg->value);
if ($arg->name !== null) {
$index = $arg->name->toString();
foreach ($reorderedArgs ?? $args as $i => $arg) {
$originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
$parameter = null;
if ($singleParametersAcceptor !== null) {
$parameters = $singleParametersAcceptor->getParameters();
if (isset($parameters[$i])) {
$parameter = $parameters[$i];
} elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) {
$parameter = $parameters[count($parameters) - 1];
}
}

if ($parameter !== null && $scope instanceof MutatingScope) {
$scope = $scope->pushInFunctionCall(null, $parameter);
}

$type = $scope->getType($originalArg->value);

if ($parameter !== null && $scope instanceof MutatingScope) {
$scope = $scope->popInFunctionCall();
}

if ($originalArg->name !== null) {
$index = $originalArg->name->toString();
$hasName = true;
} else {
$index = $i;
}
if ($arg->unpack) {
if ($originalArg->unpack) {
$unpack = true;
$types[$index] = $type->getIterableValueType();
} else {
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public function dataFileAsserts(): iterable
if (PHP_VERSION_ID >= 70400) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/reflection-type.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-passed-to-type.php');
}

if (PHP_VERSION_ID >= 80100) {
Expand Down
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/TestClosureTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\FunctionLike;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* @implements Rule<FunctionLike>
*/
class TestClosureTypeRule implements Rule
{

public function getNodeType(): string
{
return FunctionLike::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof Closure && !$node instanceof Node\Expr\ArrowFunction) {
return [];
}

$type = $scope->getType($node);

return [
RuleErrorBuilder::message(sprintf('Closure type: %s', $type->describe(VerbosityLevel::precise())))
->identifier('tests.closureType')
->build(),
];
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/TestClosureTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Rules\Rule as TRule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<TestClosureTypeRule>
*/
class TestClosureTypeRuleTest extends RuleTestCase
{

protected function getRule(): TRule
{
return new TestClosureTypeRule();
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/closure-passed-to-type.php'], [
[
'Closure type: Closure(mixed): (1|2|3)',
25,
],
[
'Closure type: Closure(mixed): (1|2|3)',
35,
],
]);
}

}
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/data/closure-passed-to-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace ClosurePassedToType;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @template T
* @template U
* @param array<T> $items
* @param callable(T): U $cb
* @return array<U>
*/
public function doFoo(array $items, callable $cb)
{

}

public function doBar()
{
$a = [1, 2, 3];
$b = $this->doFoo($a, function ($item) {
assertType('1|2|3', $item);
return $item;
});
assertType('array<1|2|3>', $b);
}

public function doBaz()
{
$a = [1, 2, 3];
$b = $this->doFoo($a, fn ($item) => $item);
assertType('array<1|2|3>', $b);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function testF($arrayOfInt, $callableOrNull)
assertType('array<string>', f($arrayOfInt, function ($a): string {
return (string)$a;
}));
assertType('array', f($arrayOfInt, function ($a) {
assertType('array<int>', f($arrayOfInt, function ($a) {
return $a;
}));
assertType('array<string>', f($arrayOfInt, $callableOrNull));
Expand Down

0 comments on commit ca41b7d

Please sign in to comment.