Skip to content
4 changes: 4 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,10 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name
}

foreach ($typeNode->items as $itemNode) {
if ($itemNode->valueType instanceof CallableTypeNode) {
$builder->disableClosureDegradation();
}

$offsetType = $this->resolveArrayShapeOffsetType($itemNode, $nameScope);
$builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional);
}
Expand Down
13 changes: 9 additions & 4 deletions src/Type/Constant/ConstantArrayTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ final class ConstantArrayTypeBuilder
{

public const ARRAY_COUNT_LIMIT = 256;
private const CLOSURES_COUNT_LIMIT = 16;
private const CLOSURES_COUNT_LIMIT = 32;

private bool $degradeToGeneralArray = false;

private bool $degradeClosures = false;
private ?bool $degradeClosures = null;

private bool $oversized = false;

Expand Down Expand Up @@ -84,7 +84,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
}

if (!$this->degradeToGeneralArray) {
if ($valueType instanceof ClosureType) {
if ($valueType instanceof ClosureType && $this->degradeClosures !== false) {
$numClosures = 1;
foreach ($this->valueTypes as $innerType) {
if (!($innerType instanceof ClosureType)) {
Expand Down Expand Up @@ -300,6 +300,11 @@ public function degradeToGeneralArray(bool $oversized = false): void
$this->oversized = $this->oversized || $oversized;
}

public function disableClosureDegradation(): void
{
$this->degradeClosures = false;
}

public function getArray(): Type
{
$keyTypesCount = count($this->keyTypes);
Expand All @@ -313,7 +318,7 @@ public function getArray(): Type
return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
}

if ($this->degradeClosures) {
if ($this->degradeClosures === true) {
$itemTypes = [];
$itemTypes[] = new CallableType();
foreach ($this->valueTypes as $valueType) {
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 @@ -247,6 +247,7 @@ private static function findTestFiles(): iterable
yield __DIR__ . '/../Rules/Methods/data/bug-4801.php';
yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php';
yield __DIR__ . '/../Rules/Methods/data/bug-12927.php';
yield __DIR__ . '/../Rules/Properties/data/bug-14012.php';
}

/**
Expand Down
18 changes: 17 additions & 1 deletion tests/PHPStan/Analyser/nsrt/degrade-closures.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
assertType('array{Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void}', $arr);
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
$arr[] = static function () {};
assertType('array{Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void}', $arr);

$arr[] = static function () {};
assertType('non-empty-list<callable(): mixed>&oversized-array', $arr);
Original file line number Diff line number Diff line change
Expand Up @@ -2591,4 +2591,11 @@ public function testBug8936(): void
$this->analyse([__DIR__ . '/data/bug-8936.php'], []);
}

public function testBug14012(): void
{
$this->checkExplicitMixed = true;
$this->checkImplicitMixed = false;
$this->analyse([__DIR__ . '/../Properties/data/bug-14012.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,9 @@ public function testPromotedProperties(): void
$this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], []);
}

public function testBug14012(): void
{
$this->analyse([__DIR__ . '/data/bug-14012.php'], []);
}

}
55 changes: 55 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-14012.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);

namespace Bug14012;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use function PHPStan\Testing\assertType;

final class ExpectationMethodResolver
{
/**
* @var array{
* hasMethod: \Closure(Scope, Node\Arg, Node\Arg): Node\Expr,
* hasProperty: \Closure(Scope, Node\Arg, Node\Arg): Node\Expr,
* isArray: \Closure(Scope, Node\Arg): Node\Expr,
* isBool: \Closure(Scope, Node\Arg): Node\Expr,
* isCallable: \Closure(Scope, Node\Arg): Node\Expr,
* isCountable: \Closure(Scope, Node\Arg): Node\Expr,
* isFalse: \Closure(Scope, Node\Arg): Node\Expr,
* isFloat: \Closure(Scope, Node\Arg): Node\Expr,
* isInstanceOf: \Closure(Scope, Node\Arg, Node\Arg): Node\Expr,
* isInt: \Closure(Scope, Node\Arg): Node\Expr,
* isIterable: \Closure(Scope, Node\Arg): Node\Expr,
* isList: \Closure(Scope, Node\Arg): Node\Expr,
* isMap: \Closure(Scope, Node\Arg): Node\Expr,
* isNaturalInt: \Closure(Scope, Node\Arg): Node\Expr,
* isNegativeInt: \Closure(Scope, Node\Arg): Node\Expr,
* isNonEmptyString: \Closure(Scope, Node\Arg): Node\Expr,
* isNull: \Closure(Scope, Node\Arg): Node\Expr,
* isNumeric: \Closure(Scope, Node\Arg): Node\Expr,
* isObject: \Closure(Scope, Node\Arg): Node\Expr,
* isPositiveInt: \Closure(Scope, Node\Arg): Node\Expr,
* isResource: \Closure(Scope, Node\Arg): Node\Expr,
* isSameAs: \Closure(Scope, Node\Arg, Node\Arg): Node\Expr,
* isScalar: \Closure(Scope, Node\Arg): Node\Expr,
* isString: \Closure(Scope, Node\Arg): Node\Expr,
* isTrue: \Closure(Scope, Node\Arg): Node\Expr,
* }
*/
public static array $resolvers = [];
}

assertType("array{hasMethod: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg, PhpParser\Node\Arg): PhpParser\Node\Expr, hasProperty: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg, PhpParser\Node\Arg): PhpParser\Node\Expr, isArray: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isBool: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isCallable: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isCountable: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isFalse: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isFloat: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isInstanceOf: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg, PhpParser\Node\Arg): PhpParser\Node\Expr, isInt: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isIterable: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isList: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isMap: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isNaturalInt: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isNegativeInt: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isNonEmptyString: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isNull: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isNumeric: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isObject: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isPositiveInt: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isResource: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isSameAs: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg, PhpParser\Node\Arg): PhpParser\Node\Expr, isScalar: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isString: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr, isTrue: Closure(PHPStan\Analyser\Scope, PhpParser\Node\Arg): PhpParser\Node\Expr}", ExpectationMethodResolver::$resolvers);

/**
* @param callable(Scope, Node\Arg):Node\Expr $callable
*/
function doFoo($callable):void {}
doFoo(ExpectationMethodResolver::$resolvers['isArray']);

/**
* @param \Closure(Scope, Node\Arg):Node\Expr $callable
*/
function doBar($callable):void {}
doBar(ExpectationMethodResolver::$resolvers['isArray']);
Loading