Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,11 @@ services:
tags:
- phpstan.parser.richParserNodeVisitor

-
class: PHPStan\Parser\ImmediatelyInvokedClosureVisitor
tags:
- phpstan.parser.richParserNodeVisitor

-
class: PHPStan\Parallel\ParallelAnalyser
arguments:
Expand Down
6 changes: 5 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Parser\ArrayMapArgVisitor;
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
use PHPStan\Parser\NewAssignedToPropertyVisitor;
use PHPStan\Parser\Parser;
use PHPStan\Php\PhpVersion;
Expand Down Expand Up @@ -4759,6 +4760,7 @@ private function processFinallyScopeVariableTypeHolders(
* @param Node\ClosureUse[] $byRefUses
*/
public function processClosureScope(
Expr\Closure $expr,
self $closureScope,
?self $prevScope,
array $byRefUses,
Expand Down Expand Up @@ -4791,7 +4793,9 @@ public function processClosureScope(
$prevVariableType = $prevScope->getVariableType($variableName);
if (!$variableType->equals($prevVariableType)) {
$variableType = TypeCombinator::union($variableType, $prevVariableType);
$variableType = self::generalizeType($variableType, $prevVariableType, 0);
if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true) {
$variableType = self::generalizeType($variableType, $prevVariableType, 0);
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4228,7 +4228,7 @@ private function processClosureNode(
}

$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
$closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
$closureScope = $closureScope->processClosureScope($expr, $scope, null, $byRefUses);
$closureType = $closureScope->getAnonymousFunctionReflection();
if (!$closureType instanceof ClosureType) {
throw new ShouldNotHappenException();
Expand Down Expand Up @@ -4298,7 +4298,7 @@ private function processClosureNode(
$intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
}
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
$closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
$closureScope = $closureScope->processClosureScope($expr, $intermediaryClosureScope, $prevScope, $byRefUses);
if ($closureScope->equals($prevScope)) {
break;
}
Expand All @@ -4318,7 +4318,7 @@ private function processClosureNode(
array_merge($statementResult->getImpurePoints(), $closureImpurePoints),
), $closureScope);

return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions);
return new ProcessClosureResult($scope->processClosureScope($expr, $closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions);
}

/**
Expand Down
46 changes: 46 additions & 0 deletions src/Parser/ImmediatelyInvokedClosureVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

final class ImmediatelyInvokedClosureVisitor extends NodeVisitorAbstract
{

public const ATTRIBUTE_NAME = 'isImmediatelyInvokedClosure';

private bool $inFuncCall = false;

public function beforeTraverse(array $nodes): ?array
{
$this->inFuncCall = false;
return null;
}

public function enterNode(Node $node): ?Node
{
if ($node instanceof Node\Expr\FuncCall) {
$this->inFuncCall = true;
}

if (
$this->inFuncCall
&& $node instanceof Node\Expr\Closure
) {
$node->setAttribute(self::ATTRIBUTE_NAME, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would lead to a lot of false positives. You only want to tag this if in $node->name, but right now the visitor is tagging args too.

Just check $node->name inside $node instanceof Node\Expr\FuncCall.

}

return null;
}

public function leaveNode(Node $node): ?Node
{
if ($node instanceof Node\Expr\FuncCall) {
$this->inFuncCall = false;
}

return null;
}

}
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11561.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.0

namespace Bug11561;

use function PHPStan\Testing\assertType;
use DateTime;

/** @param array{date: DateTime} $c */
function main(mixed $c): void{
assertType('array{date: DateTime}', $c);
$c['id']=1;
assertType('array{date: DateTime, id: 1}', $c);

$x = (function() use (&$c) {
assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is wrong, see phpstan/phpstan#11945

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning is, that since this function is invoked immediately, it should be treated as the same code as normally, without the function.

$c['name'] = 'ruud';
assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c);
return 'x';
})();

assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is wrong, see phpstan/phpstan#11945

}


/** @param array{date: DateTime} $c */
function main2(mixed $c): void{
assertType('array{date: DateTime}', $c);
$c['id']=1;
$c['name'] = 'staabm';
assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c);

$x = (function() use (&$c) {
assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c);
$c['name'] = 'ruud';
assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c);
return 'x';
})();

assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c);
}
Loading