Skip to content

Commit 0955395

Browse files
committed
Fix phpstan/phpstan#14325: Apply @param PHPDoc types to closure parameters
- Added resolveClosurePhpDocParameterTypes() to extract @param types from PHPDoc comments on the statement containing a closure assignment - Applied resolved PHPDoc parameter types to the closure scope in processClosureNode() - Updated count-type test expectations to reflect correct generic types - New regression test in tests/PHPStan/Analyser/nsrt/bug-14325.php
1 parent 6bac0de commit 0955395

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2716,7 +2716,16 @@ public function processClosureNode(
27162716
$this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
27172717
}
27182718

2719+
$phpDocParameterTypes = $this->resolveClosurePhpDocParameterTypes($stmt, $scope, $expr);
2720+
27192721
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
2722+
foreach ($phpDocParameterTypes as $paramName => $paramType) {
2723+
if (!$closureScope->hasVariableType($paramName)->yes()) {
2724+
continue;
2725+
}
2726+
2727+
$closureScope = $closureScope->assignVariable($paramName, $paramType, $closureScope->getNativeType(new Variable($paramName)), TrinaryLogic::createYes());
2728+
}
27202729
$closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
27212730
$closureType = $closureScope->getAnonymousFunctionReflection();
27222731
if (!$closureType instanceof ClosureType) {
@@ -4175,6 +4184,43 @@ private function processNodesForCalledMethod($node, ExpressionResultStorage $sto
41754184
}
41764185
}
41774186

4187+
/**
4188+
* @return array<string, Type>
4189+
*/
4190+
private function resolveClosurePhpDocParameterTypes(Node\Stmt $stmt, MutatingScope $scope, Expr\Closure $closure): array
4191+
{
4192+
$phpDocParameterTypes = [];
4193+
4194+
foreach ($stmt->getComments() as $comment) {
4195+
if (!$comment instanceof Doc) {
4196+
continue;
4197+
}
4198+
4199+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4200+
$scope->getFile(),
4201+
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
4202+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
4203+
$scope->getFunction() !== null ? $scope->getFunction()->getName() : null,
4204+
$comment->getText(),
4205+
);
4206+
4207+
foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
4208+
foreach ($closure->params as $param) {
4209+
if (
4210+
$param->var instanceof Variable
4211+
&& is_string($param->var->name)
4212+
&& $param->var->name === $paramName
4213+
) {
4214+
$phpDocParameterTypes[$paramName] = $paramTag->getType();
4215+
break;
4216+
}
4217+
}
4218+
}
4219+
}
4220+
4221+
return $phpDocParameterTypes;
4222+
}
4223+
41784224
/**
41794225
* @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock}
41804226
*/
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14325;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param list<string> $array
9+
*/
10+
function func(array $array): void
11+
{
12+
assertType('list<string>', $array);
13+
$array[] = 'bar';
14+
assertType('non-empty-list<string>', $array);
15+
}
16+
17+
/**
18+
* @param list<string> $array
19+
*/
20+
$func = function(array $array): void
21+
{
22+
assertType('list<string>', $array);
23+
$array[] = 'bar';
24+
assertType('non-empty-list<string>', $array);
25+
};
26+
27+
class Foo
28+
{
29+
/**
30+
* @param list<string> $array
31+
*/
32+
public function method(array $array): void
33+
{
34+
assertType('list<string>', $array);
35+
36+
/**
37+
* @param list<string> $inner
38+
*/
39+
$closure = function (array $inner): void {
40+
assertType('list<string>', $inner);
41+
$inner[] = 'baz';
42+
assertType('non-empty-list<string>', $inner);
43+
};
44+
}
45+
}

tests/PHPStan/Analyser/nsrt/count-type.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@ public function constantArrayWhichCanBecomeList(string $h): void
104104
*/
105105
function(\ArrayObject $obj): void {
106106
if (count($obj) === 0) {
107-
assertType('ArrayObject', $obj);
107+
assertType('ArrayObject<int, mixed>', $obj);
108108
return;
109109
}
110110

111-
assertType('ArrayObject', $obj);
111+
assertType('ArrayObject<int, mixed>', $obj);
112112
};
113113

114114
function($mixed): void {

0 commit comments

Comments
 (0)