Skip to content
11 changes: 9 additions & 2 deletions src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\Callables\SimpleThrowPoint;
use PHPStan\Reflection\ExtendedParameterReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Reflection\Php\DummyParameter;
Expand Down Expand Up @@ -97,29 +98,35 @@ public function getClosureType(
}

$callableParameters = null;
$nativeCallableParameters = null;
$arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
$immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME);
if ($arrayMapArgs !== null) {
$callableParameters = [];
$nativeCallableParameters = [];
foreach ($arrayMapArgs as $funcCallArg) {
$callableParameters[] = new DummyParameter('item', $scope->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}
} elseif ($immediatelyInvokedArgs !== null) {
foreach ($immediatelyInvokedArgs as $immediatelyInvokedArg) {
$callableParameters[] = new DummyParameter('item', $scope->getType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}
} else {
$inFunctionCallsStackCount = count($scope->inFunctionCallsStack);
if ($inFunctionCallsStackCount > 0) {
[, $inParameter] = $scope->inFunctionCallsStack[$inFunctionCallsStackCount - 1];
if ($inParameter !== null) {
$callableParameters = $this->nodeScopeResolver->createCallableParameters($scope, $expr, null, $inParameter->getType());
$nativeType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType();
$nativeCallableParameters = $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, null, $nativeType);
}
}
}

if ($expr instanceof ArrowFunction) {
$arrowScope = $scope->enterArrowFunctionWithoutReflection($expr, $callableParameters);
$arrowScope = $scope->enterArrowFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters);

if ($expr->expr instanceof Yield_ || $expr->expr instanceof YieldFrom) {
$yieldNode = $expr->expr;
Expand Down Expand Up @@ -232,7 +239,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu

self::$resolveClosureTypeDepth++;

$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters);
$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters);
$closureReturnStatements = [];
$closureYieldStatements = [];
$onlyNeverExecutionEnds = null;
Expand Down
31 changes: 21 additions & 10 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1970,18 +1970,20 @@
/**
* @api
* @param ParameterReflection[]|null $callableParameters
* @param ParameterReflection[]|null $nativeCallableParameters
*/
public function enterAnonymousFunction(
Comment thread
VincentLanglet marked this conversation as resolved.
Expr\Closure $closure,
?array $callableParameters,
?array $nativeCallableParameters = null,
): self
{
$anonymousFunctionReflection = $this->resolveType('__phpstanClosure', $closure);
if (!$anonymousFunctionReflection instanceof ClosureType) {
throw new ShouldNotHappenException();
}

$scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters);
$scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters, $nativeCallableParameters);

return $this->scopeFactory->create(
$scope->context,
Expand All @@ -2005,10 +2007,12 @@

/**
* @param ParameterReflection[]|null $callableParameters
* @param ParameterReflection[]|null $nativeCallableParameters
*/
public function enterAnonymousFunctionWithoutReflection(
Expr\Closure $closure,
?array $callableParameters,
?array $nativeCallableParameters,
): self
{
$expressionTypes = [];
Expand All @@ -2019,13 +2023,15 @@
}
$paramExprString = sprintf('$%s', $parameter->var->name);
$isNullable = $this->isParameterValueNullable($parameter);
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
$nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
if ($callableParameters !== null) {
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
}
$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
$expressionTypes[$paramExprString] = $holder;
$nativeTypes[$paramExprString] = $holder;
if ($nativeCallableParameters !== null) {
$nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
}
$expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
$nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $nativeParameterType);
}

$nonRefVariableNames = [];
Expand Down Expand Up @@ -2181,15 +2187,16 @@
/**
* @api
* @param ParameterReflection[]|null $callableParameters
* @param ParameterReflection[]|null $nativeCallableParameters
Comment thread
VincentLanglet marked this conversation as resolved.
*/
public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters = null): self
{
$anonymousFunctionReflection = $this->resolveType('__phpStanArrowFn', $arrowFunction);
if (!$anonymousFunctionReflection instanceof ClosureType) {
throw new ShouldNotHappenException();
}

$scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters);
$scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters, $nativeCallableParameters);

return $this->scopeFactory->create(
$scope->context,
Expand All @@ -2213,21 +2220,25 @@

/**
* @param ParameterReflection[]|null $callableParameters
* @param ParameterReflection[]|null $nativeCallableParameters
*/
public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters): self
{
$arrowFunctionScope = $this;
foreach ($arrowFunction->params as $i => $parameter) {
$isNullable = $this->isParameterValueNullable($parameter);
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
$nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
if ($callableParameters !== null) {
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
}
if ($nativeCallableParameters !== null) {
$nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
}

if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
throw new ShouldNotHappenException();
}
$arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes());
$arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $nativeParameterType, TrinaryLogic::createYes());
}

if ($arrowFunction->static) {
Expand Down Expand Up @@ -2339,7 +2350,7 @@

$elementType = TypeCombinator::union(...$elementTypes);

if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {

Check warning on line 2353 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $elementType = TypeCombinator::union(...$elementTypes); - if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { + if ($this->getPhpVersion()->supportsNamedArguments()->yes()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $elementType); }

Check warning on line 2353 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $elementType = TypeCombinator::union(...$elementTypes); - if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { + if ($this->getPhpVersion()->supportsNamedArguments()->yes()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $elementType); }
return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $elementType);
}

Expand Down
57 changes: 36 additions & 21 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2704,6 +2704,7 @@ public function processClosureNode(
callable $nodeCallback,
ExpressionContext $context,
?Type $passedToType,
?Type $nativePassedToType = null,
): ProcessClosureResult
{
foreach ($expr->params as $param) {
Expand All @@ -2713,12 +2714,8 @@ public function processClosureNode(
$byRefUses = [];

$closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
$callableParameters = $this->createCallableParameters(
$scope,
$expr,
$closureCallArgs,
$passedToType,
);
$callableParameters = $this->createCallableParameters($scope, $expr, $closureCallArgs, $passedToType);
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType);

$useScope = $scope;
foreach ($expr->uses as $use) {
Expand Down Expand Up @@ -2769,7 +2766,7 @@ public function processClosureNode(
$this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
}

$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
$closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
$closureType = $closureScope->getAnonymousFunctionReflection();
if (!$closureType instanceof ClosureType) {
Expand Down Expand Up @@ -2851,7 +2848,7 @@ public function processClosureNode(
break;
}

$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
$closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);

if ($closureScope->equals($prevScope)) {
Expand Down Expand Up @@ -2916,6 +2913,7 @@ public function processArrowFunctionNode(
ExpressionResultStorage $storage,
callable $nodeCallback,
?Type $passedToType,
?Type $nativePassedToType = null,
): ExpressionResult
{
foreach ($expr->params as $param) {
Expand All @@ -2926,12 +2924,9 @@ public function processArrowFunctionNode(
}

$arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
$arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters(
$scope,
$expr,
$arrowFunctionCallArgs,
$passedToType,
));
$callableParameters = $this->createCallableParameters($scope, $expr, $arrowFunctionCallArgs, $passedToType);
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType);
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
$arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
if ($arrowFunctionType === null) {
throw new ShouldNotHappenException();
Expand All @@ -2943,14 +2938,33 @@ public function processArrowFunctionNode(
}

/**
* @param Node\Arg[] $args
* @param Node\Arg[]|null $args
* @return ParameterReflection[]|null
*/
public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
{
return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, static fn (Scope $s, Expr $e) => $s->getType($e));
}

/**
* @param Node\Arg[]|null $args
* @return ParameterReflection[]|null
*/
public function createNativeCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array
Comment thread
VincentLanglet marked this conversation as resolved.
{
return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e));
}

/**
* @param Node\Arg[]|null $args
* @param Closure(Scope, Expr): Type $typeGetter
* @return ParameterReflection[]|null
*/
private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array
{
$callableParameters = null;
if ($args !== null) {
$closureType = $scope->getType($closureExpr);
$closureType = $typeGetter($scope, $closureExpr);

if ($closureType->isCallable()->no()) {
return null;
Expand All @@ -2967,12 +2981,13 @@ public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array

if ($callableParameter->isVariadic()) {
$argTypes = [];
for ($j = $index; $j < count($args); $j++) {
$argTypes[] = $scope->getType($args[$j]->value);
$argNumber = count($args);
for ($j = $index; $j < $argNumber; $j++) {
$argTypes[] = $typeGetter($scope, $args[$j]->value);
}
$type = TypeCombinator::union(...$argTypes);
} else {
$type = $scope->getType($args[$index]->value);
$type = $typeGetter($scope, $args[$index]->value);
}
$callableParameters[$index] = new NativeParameterReflection(
$callableParameter->getName(),
Expand Down Expand Up @@ -3391,7 +3406,7 @@ public function processArgs(
}

$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null);
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null, $parameterNativeType);
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
$impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
Expand Down Expand Up @@ -3450,7 +3465,7 @@ public function processArgs(
}

$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null);
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null, $parameterNativeType);
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
$impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
Expand Down
Loading
Loading