Skip to content

Commit 73bb9eb

Browse files
authored
Add @param-out support
1 parent faf7a98 commit 73bb9eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1332
-124
lines changed

phpstan-baseline.neon

+10
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ parameters:
143143
count: 1
144144
path: src/PhpDoc/PhpDocBlock.php
145145

146+
-
147+
message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getParamOutTypeTagV…' will always evaluate to true\\.$#"
148+
count: 1
149+
path: src/PhpDoc/PhpDocNodeResolver.php
150+
146151
-
147152
message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getSelfOutTypeTagVa…' will always evaluate to true\\.$#"
148153
count: 1
@@ -161,6 +166,11 @@ parameters:
161166
count: 1
162167
path: src/PhpDoc/StubValidator.php
163168

169+
-
170+
message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#"
171+
count: 1
172+
path: src/PhpDoc/Tag/ParamOutTag.php
173+
164174
-
165175
message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#"
166176
count: 1

src/Analyser/MutatingScope.php

+6
Original file line numberDiff line numberDiff line change
@@ -2517,6 +2517,7 @@ public function enterTrait(ClassReflection $traitReflection): self
25172517
/**
25182518
* @api
25192519
* @param Type[] $phpDocParameterTypes
2520+
* @param Type[] $parameterOutTypes
25202521
*/
25212522
public function enterClassMethod(
25222523
Node\Stmt\ClassMethod $classMethod,
@@ -2533,6 +2534,7 @@ public function enterClassMethod(
25332534
?Assertions $asserts = null,
25342535
?Type $selfOutType = null,
25352536
?string $phpDocComment = null,
2537+
array $parameterOutTypes = [],
25362538
): self
25372539
{
25382540
if (!$this->isInClass()) {
@@ -2560,6 +2562,7 @@ public function enterClassMethod(
25602562
$asserts ?? Assertions::createEmpty(),
25612563
$selfOutType,
25622564
$phpDocComment,
2565+
array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes),
25632566
),
25642567
!$classMethod->isStatic(),
25652568
);
@@ -2626,6 +2629,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike):
26262629
/**
26272630
* @api
26282631
* @param Type[] $phpDocParameterTypes
2632+
* @param Type[] $parameterOutTypes
26292633
*/
26302634
public function enterFunction(
26312635
Node\Stmt\Function_ $function,
@@ -2641,6 +2645,7 @@ public function enterFunction(
26412645
bool $acceptsNamedArguments = true,
26422646
?Assertions $asserts = null,
26432647
?string $phpDocComment = null,
2648+
array $parameterOutTypes = [],
26442649
): self
26452650
{
26462651
return $this->enterFunctionLike(
@@ -2662,6 +2667,7 @@ public function enterFunction(
26622667
$acceptsNamedArguments,
26632668
$asserts ?? Assertions::createEmpty(),
26642669
$phpDocComment,
2670+
array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes),
26652671
),
26662672
false,
26672673
);

src/Analyser/NodeScopeResolver.php

+40-5
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
use PHPStan\Reflection\MethodReflection;
115115
use PHPStan\Reflection\Native\NativeMethodReflection;
116116
use PHPStan\Reflection\Native\NativeParameterReflection;
117+
use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
117118
use PHPStan\Reflection\ParametersAcceptor;
118119
use PHPStan\Reflection\ParametersAcceptorSelector;
119120
use PHPStan\Reflection\Php\PhpMethodReflection;
@@ -407,7 +408,7 @@ private function processStmtNode(
407408
$hasYield = false;
408409
$throwPoints = [];
409410
$this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
410-
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts] = $this->getPhpDocs($scope, $stmt);
411+
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
411412

412413
foreach ($stmt->params as $param) {
413414
$this->processParamNode($param, $scope, $nodeCallback);
@@ -431,6 +432,7 @@ private function processStmtNode(
431432
$acceptsNamedArguments,
432433
$asserts,
433434
$phpDocComment,
435+
$phpDocParameterOutTypes,
434436
);
435437
$functionReflection = $functionScope->getFunction();
436438
if (!$functionReflection instanceof FunctionReflection) {
@@ -470,7 +472,7 @@ private function processStmtNode(
470472
$hasYield = false;
471473
$throwPoints = [];
472474
$this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
473-
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType] = $this->getPhpDocs($scope, $stmt);
475+
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
474476

475477
foreach ($stmt->params as $param) {
476478
$this->processParamNode($param, $scope, $nodeCallback);
@@ -495,6 +497,7 @@ private function processStmtNode(
495497
$asserts,
496498
$selfOutType,
497499
$phpDocComment,
500+
$phpDocParameterOutTypes,
498501
);
499502

500503
if ($stmt->name->toLowerString() === '__construct') {
@@ -1806,6 +1809,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
18061809
$functionReflection->getVariants(),
18071810
);
18081811
}
1812+
18091813
if ($parametersAcceptor !== null) {
18101814
$expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
18111815
}
@@ -2043,11 +2047,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
20432047
}
20442048
}
20452049
}
2050+
20462051
if ($parametersAcceptor !== null) {
20472052
$expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
20482053
}
20492054
$result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
20502055
$scope = $result->getScope();
2056+
20512057
if ($methodReflection !== null) {
20522058
$hasSideEffects = $methodReflection->hasSideEffects();
20532059
if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') {
@@ -2174,12 +2180,14 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
21742180
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
21752181
}
21762182
}
2183+
21772184
if ($parametersAcceptor !== null) {
21782185
$expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
21792186
}
21802187
$result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null);
21812188
$scope = $result->getScope();
21822189
$scopeFunction = $scope->getFunction();
2190+
21832191
if (
21842192
$methodReflection !== null
21852193
&& !$methodReflection->isStatic()
@@ -2529,6 +2537,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
25292537
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
25302538
}
25312539
}
2540+
25322541
if ($parametersAcceptor !== null) {
25332542
$expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
25342543
}
@@ -3272,8 +3281,21 @@ private function processArgs(
32723281
?MutatingScope $closureBindScope = null,
32733282
): ExpressionResult
32743283
{
3284+
$paramOutTypes = [];
32753285
if ($parametersAcceptor !== null) {
32763286
$parameters = $parametersAcceptor->getParameters();
3287+
3288+
foreach ($parameters as $parameter) {
3289+
if (!$parameter instanceof ParameterReflectionWithPhpDocs) {
3290+
continue;
3291+
}
3292+
3293+
if ($parameter->getOutType() === null) {
3294+
continue;
3295+
}
3296+
3297+
$paramOutTypes[$parameter->getName()] = TemplateTypeHelper::resolveTemplateTypes($parameter->getOutType(), $parametersAcceptor->getResolvedTemplateTypeMap());
3298+
}
32773299
}
32783300

32793301
if ($calleeReflection !== null) {
@@ -3286,20 +3308,29 @@ private function processArgs(
32863308
$originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
32873309
$nodeCallback($originalArg, $scope);
32883310
if (isset($parameters) && $parametersAcceptor !== null) {
3311+
$byRefType = new MixedType();
32893312
$assignByReference = false;
32903313
if (isset($parameters[$i])) {
32913314
$assignByReference = $parameters[$i]->passedByReference()->createsNewVariable();
32923315
$parameterType = $parameters[$i]->getType();
3316+
3317+
if (isset($paramOutTypes[$parameters[$i]->getName()])) {
3318+
$byRefType = $paramOutTypes[$parameters[$i]->getName()];
3319+
}
32933320
} elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
32943321
$lastParameter = $parameters[count($parameters) - 1];
32953322
$assignByReference = $lastParameter->passedByReference()->createsNewVariable();
32963323
$parameterType = $lastParameter->getType();
3324+
3325+
if (isset($paramOutTypes[$lastParameter->getName()])) {
3326+
$byRefType = $paramOutTypes[$lastParameter->getName()];
3327+
}
32973328
}
32983329

32993330
if ($assignByReference) {
33003331
$argValue = $arg->value;
33013332
if ($argValue instanceof Variable && is_string($argValue->name)) {
3302-
$scope = $scope->assignVariable($argValue->name, new MixedType(), new MixedType());
3333+
$scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType());
33033334
}
33043335
}
33053336
}
@@ -4039,7 +4070,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection
40394070
}
40404071

40414072
/**
4042-
* @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type}
4073+
* @return array{TemplateTypeMap, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>}
40434074
*/
40444075
public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
40454076
{
@@ -4065,6 +4096,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
40654096
$trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
40664097
$resolvedPhpDoc = null;
40674098
$functionName = null;
4099+
$phpDocParameterOutTypes = [];
40684100

40694101
if ($node instanceof Node\Stmt\ClassMethod) {
40704102
if (!$scope->isInClass()) {
@@ -4149,6 +4181,9 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
41494181
}
41504182
$phpDocParameterTypes[$paramName] = $paramType;
41514183
}
4184+
foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
4185+
$phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
4186+
}
41524187
if ($node instanceof Node\FunctionLike) {
41534188
$nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
41544189
$phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
@@ -4168,7 +4203,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
41684203
$selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
41694204
}
41704205

4171-
return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType];
4206+
return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes];
41724207
}
41734208

41744209
private function transformStaticType(ClassReflection $declaringClass, Type $type): Type

src/PhpDoc/PhpDocNodeResolver.php

+29
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\PhpDoc\Tag\MethodTag;
1212
use PHPStan\PhpDoc\Tag\MethodTagParameter;
1313
use PHPStan\PhpDoc\Tag\MixinTag;
14+
use PHPStan\PhpDoc\Tag\ParamOutTag;
1415
use PHPStan\PhpDoc\Tag\ParamTag;
1516
use PHPStan\PhpDoc\Tag\PropertyTag;
1617
use PHPStan\PhpDoc\Tag\ReturnTag;
@@ -318,6 +319,34 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope):
318319
return $resolved;
319320
}
320321

322+
/**
323+
* @return array<string, ParamOutTag>
324+
*/
325+
public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
326+
{
327+
if (!method_exists($phpDocNode, 'getParamOutTypeTagValues')) {
328+
return [];
329+
}
330+
331+
$resolved = [];
332+
333+
foreach (['@param-out', '@psalm-param-out', '@phpstan-param-out'] as $tagName) {
334+
foreach ($phpDocNode->getParamOutTypeTagValues($tagName) as $tagValue) {
335+
$parameterName = substr($tagValue->parameterName, 1);
336+
$parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);
337+
if ($this->shouldSkipType($tagName, $parameterType)) {
338+
continue;
339+
}
340+
341+
$resolved[$parameterName] = new ParamOutTag(
342+
$parameterType,
343+
);
344+
}
345+
}
346+
347+
return $resolved;
348+
}
349+
321350
public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag
322351
{
323352
$resolved = null;

0 commit comments

Comments
 (0)