Skip to content
Open
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
24 changes: 18 additions & 6 deletions src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@
final class ClosureTypeResolver implements PerFileAnalysisResettable
{

private static int $resolveClosureTypeDepth = 0;
/**
* Body-walk depth per closure node (spl_object_id()) - a self-by-ref
* closure re-walking itself is cut off, while different closures nested
* in each other's bodies are not.
*
* @var array<int, int>
*/
private static array $resolveClosureTypeDepth = [];

/**
* Per-context resolved closure types, keyed by the closure node. Node
Expand Down Expand Up @@ -155,7 +162,8 @@ public function getClosureType(
if (array_key_exists($cacheKey, $cachedTypes)) {
return $this->createClosureTypeFromCache($scope, $expr, $parameters, $isVariadic, $cachedTypes[$cacheKey], $contextFree);
}
if (self::$resolveClosureTypeDepth >= 2) {
$exprId = spl_object_id($expr);
if ((self::$resolveClosureTypeDepth[$exprId] ?? 0) >= 2) {
return new ClosureType(
$parameters,
$scope->getFunctionType($expr->returnType, false, false),
Expand All @@ -172,7 +180,7 @@ public function getClosureType(

$arrowFunctionImpurePoints = [];
$invalidateExpressions = [];
self::$resolveClosureTypeDepth++;
self::$resolveClosureTypeDepth[$exprId] = (self::$resolveClosureTypeDepth[$exprId] ?? 0) + 1;
$walkStorage = new ExpressionResultStorage();
$arrowScope->pushExpressionResultStorage($walkStorage);
try {
Expand Down Expand Up @@ -208,7 +216,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
);
} finally {
$arrowScope->popExpressionResultStorage();
self::$resolveClosureTypeDepth--;
if (--self::$resolveClosureTypeDepth[$exprId] === 0) {
unset(self::$resolveClosureTypeDepth[$exprId]);
}
}
$throwPoints = array_map(static fn ($throwPoint) => $throwPoint->toPublic(), $arrowFunctionExprResult->getThrowPoints());
$impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints());
Expand All @@ -220,7 +230,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
return $this->assembleClosureType($scope, $expr, $parameters, $isVariadic, $returnType, $throwPoints, $impurePoints, $invalidateExpressions, [], $cacheKey, $contextFree);
}

self::$resolveClosureTypeDepth++;
self::$resolveClosureTypeDepth[$exprId] = (self::$resolveClosureTypeDepth[$exprId] ?? 0) + 1;

$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters);
$closureReturnStatements = [];
Expand Down Expand Up @@ -271,7 +281,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
}, StatementContext::createTopLevel())->toPublic();
} finally {
$closureScope->popExpressionResultStorage();
self::$resolveClosureTypeDepth--;
if (--self::$resolveClosureTypeDepth[$exprId] === 0) {
unset(self::$resolveClosureTypeDepth[$exprId]);
}
}

$throwPoints = $closureStatementResult->getThrowPoints();
Expand Down
187 changes: 187 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15406.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15406;

use function PHPStan\Testing\assertType;

final class Node
{
/** @param list<self> $children */
public function __construct(public readonly array $children, public readonly int $value) {}
}

/**
* @template T
* @template U
*
* @param list<T> $list
* @param callable(T): iterable<U> $fn
*
* @return list<U>
*/
function collect(array $list, callable $fn): array
{
$result = [];
foreach ($list as $value) {
foreach ($fn($value) as $item) {
$result[] = $item;
}
}

return $result;
}

/**
* @param list<Node> $nodes
*/
function twoLevels(array $nodes): void
{
assertType('list<int>', collect($nodes, static fn (Node $node): array
=> collect($node->children, static fn (Node $child): array => [$child->value]),
));
}

/**
* @param list<Node> $nodes
*/
function threeLevels(array $nodes): void
{
assertType('list<int>', collect($nodes, static fn (Node $node): array
=> collect($node->children, static fn (Node $child): array
=> collect($child->children, static fn (Node $grandChild): array => [$grandChild->value]),
),
));
}

/**
* @param list<Node> $nodes
*/
function threeLevelsGenerators(array $nodes): void
{
assertType('list<int>', collect($nodes, static fn (Node $node): iterable
=> yield from collect($node->children, static fn (Node $child): iterable
=> yield from collect($child->children, static fn (Node $grandChild): iterable => yield $grandChild->value),
),
));
}

/**
* @param list<Node> $nodes
*/
function threeLevelsClosures(array $nodes): void
{
assertType('list<int>', collect($nodes, static function (Node $node): array {
return collect($node->children, static function (Node $child): array {
return collect($child->children, static function (Node $grandChild): array {
return [$grandChild->value];
});
});
}));
}

/**
* @param list<Node> $nodes
*/
function threeLevelsClosuresWithoutReturnTypes(array $nodes): void
{
assertType('list<int>', collect($nodes, static function (Node $node) {
return collect($node->children, static function (Node $child) {
return collect($child->children, static function (Node $grandChild) {
return [$grandChild->value];
});
});
}));
}

/**
* @param list<Node> $nodes
*/
function fiveLevels(array $nodes): void
{
assertType('list<int>', collect($nodes, static fn (Node $a): array
=> collect($a->children, static fn (Node $b): array
=> collect($b->children, static fn (Node $c): array
=> collect($c->children, static fn (Node $d): array
=> collect($d->children, static fn (Node $e): array => [$e->value]),
),
),
),
));
}

/**
* @template T
*/
final class Box
{
/** @param T $value */
public function __construct(public mixed $value)
{
}

/**
* @template U
* @param callable(T): U $callback
* @return Box<U>
*/
public function map(callable $callback): Box
{
return new Box($callback($this->value));
}

/**
* @template V
* @template U
* @param V $value
* @param callable(V): U $callback
* @return Box<U>
*/
public static function mapStatic(mixed $value, callable $callback): Box
{
return new Box($callback($value));
}
}

/**
* @template T
* @param callable(): T $callback
* @return T
*/
function run(callable $callback): mixed
{
return $callback();
}

function boxThreeLevels(): void
{
$box = new Box(1);
assertType('Bug15406\Box<Bug15406\Box<Bug15406\Box<array{k: 1}>>>', $box->map(fn (int $i): Box => (new Box($i))->map(fn (int $j): Box => (new Box($j))->map(fn (int $k): array => ['k' => $k]))));
}

function boxStaticThreeLevels(): void
{
assertType('Bug15406\Box<Bug15406\Box<Bug15406\Box<array{k: 1}>>>', Box::mapStatic(1, fn (int $i): Box => Box::mapStatic($i, fn (int $j): Box => Box::mapStatic($j, fn (int $k): array => ['k' => $k]))));
}

function boxTwoLevels(): void
{
/** @var Box<int> $box */
$box = new Box(1);
assertType('Bug15406\Box<Bug15406\Box<array{j: int}>>', $box->map(fn (int $i): Box => (new Box($i))->map(fn (int $j): array => ['j' => $j])));
}

run(function (): void {
$box = new Box(1);
assertType('Bug15406\Box<Bug15406\Box<array{j: 1}>>', $box->map(fn (int $i): Box => (new Box($i))->map(fn (int $j): array => ['j' => $j])));

/** @var Box<int> $box */
$box = new Box(1);
assertType('Bug15406\Box<Bug15406\Box<array{j: int}>>', $box->map(fn (int $i): Box => (new Box($i))->map(fn (int $j): array => ['j' => $j])));
});

function arrayMapFourLevels(): void
{
assertType('array{array{array{array{array{1}}}}}', array_map(fn ($x) => array_map(fn ($y) => array_map(fn ($z) => array_map(fn ($w) => [$w], [$z]), [$y]), [$x]), [1]));
}
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,14 @@ public function testBug15432(): void
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug15406(): void
{
$this->checkExplicitMixed = true;
$this->checkImplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-15406.php'], []);
}

public function testBug15487(): void
{
$this->checkExplicitMixed = true;
Expand Down
66 changes: 66 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-15406.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15406Rule;

final class Node
{
/** @param list<self> $children */
public function __construct(public readonly array $children, public readonly int $value) {}
}

/**
* @template T
* @template U
*
* @param list<T> $list
* @param callable(T): iterable<U> $fn
*
* @return list<U>
*/
function collect(array $list, callable $fn): array
{
$result = [];
foreach ($list as $value) {
foreach ($fn($value) as $item) {
$result[] = $item;
}
}

return $result;
}

/**
* @param list<Node> $nodes
*/
function twoLevels(array $nodes): void
{
$result = collect($nodes, static fn (Node $node): array
=> collect($node->children, static fn (Node $child): array => [$child->value]),
);
}

/**
* @param list<Node> $nodes
*/
function threeLevels(array $nodes): void
{
$result = collect($nodes, static fn (Node $node): array
=> collect($node->children, static fn (Node $child): array
=> collect($child->children, static fn (Node $grandChild): array => [$grandChild->value]),
),
);
}

/**
* @param list<Node> $nodes
*/
function threeLevelsGenerators(array $nodes): void
{
$result = collect($nodes, static fn (Node $node): iterable
=> yield from collect($node->children, static fn (Node $child): iterable
=> yield from collect($child->children, static fn (Node $grandChild): iterable => yield $grandChild->value),
),
);
}
Loading
Loading