Skip to content

Commit 67066af

Browse files
committed
@var in promoted parameter's PHPDoc sets the parameter type
1 parent 08172a2 commit 67066af

10 files changed

+218
-11
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
use PHPStan\Php\PhpVersion;
7272
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
7373
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
74-
use PHPStan\PhpDoc\Tag\ParamTag;
7574
use PHPStan\Reflection\ClassReflection;
7675
use PHPStan\Reflection\FunctionReflection;
7776
use PHPStan\Reflection\ParametersAcceptor;
@@ -2743,6 +2742,44 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array
27432742
$functionLike->name->name,
27442743
$positionalParameterNames
27452744
);
2745+
2746+
if ($functionLike->name->toLowerString() === '__construct') {
2747+
foreach ($functionLike->params as $param) {
2748+
if ($param->flags === 0) {
2749+
continue;
2750+
}
2751+
2752+
if ($param->getDocComment() === null) {
2753+
continue;
2754+
}
2755+
2756+
if (
2757+
!$param->var instanceof Variable
2758+
|| !is_string($param->var->name)
2759+
) {
2760+
throw new \PHPStan\ShouldNotHappenException();
2761+
}
2762+
2763+
$paramPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty(
2764+
$param->getDocComment()->getText(),
2765+
$scope->getClassReflection(),
2766+
$file,
2767+
$trait,
2768+
$param->var->name,
2769+
'__construct'
2770+
);
2771+
$varTags = $paramPhpDoc->getVarTags();
2772+
if (isset($varTags[0]) && count($varTags) === 1) {
2773+
$phpDocType = $varTags[0]->getType();
2774+
} elseif (isset($varTags[$param->var->name])) {
2775+
$phpDocType = $varTags[$param->var->name]->getType();
2776+
} else {
2777+
continue;
2778+
}
2779+
2780+
$phpDocParameterTypes[$param->var->name] = $phpDocType;
2781+
}
2782+
}
27462783
} elseif ($functionLike instanceof Node\Stmt\Function_) {
27472784
$functionName = trim($scope->getNamespace() . '\\' . $functionLike->name->name, '\\');
27482785
}
@@ -2759,9 +2796,12 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array
27592796

27602797
if ($resolvedPhpDoc !== null) {
27612798
$templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
2762-
$phpDocParameterTypes = array_map(static function (ParamTag $tag): Type {
2763-
return $tag->getType();
2764-
}, $resolvedPhpDoc->getParamTags());
2799+
foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
2800+
if (array_key_exists($paramName, $phpDocParameterTypes)) {
2801+
continue;
2802+
}
2803+
$phpDocParameterTypes[$paramName] = $paramTag->getType();
2804+
}
27652805
$nativeReturnType = $scope->getFunctionType($functionLike->getReturnType(), false, false);
27662806
$phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
27672807
$phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;

src/Reflection/Php/BuiltinMethodReflection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public function isPrivate(): bool;
3636

3737
public function isPublic(): bool;
3838

39+
public function isConstructor(): bool;
40+
3941
public function getPrototype(): self;
4042

4143
public function isDeprecated(): TrinaryLogic;

src/Reflection/Php/FakeBuiltinMethodReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public function isPublic(): bool
7979
return true;
8080
}
8181

82+
public function isConstructor(): bool
83+
{
84+
return false;
85+
}
86+
8287
public function getPrototype(): BuiltinMethodReflection
8388
{
8489
throw new \ReflectionException();

src/Reflection/Php/NativeBuiltinMethodReflection.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ public function isPublic(): bool
7878
return $this->reflection->isPublic();
7979
}
8080

81+
public function isConstructor(): bool
82+
{
83+
return $this->reflection->isConstructor();
84+
}
85+
8186
public function getPrototype(): BuiltinMethodReflection
8287
{
8388
return new self($this->reflection->getPrototype());

src/Reflection/Php/PhpClassReflectionExtension.php

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
1515
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
1616
use PHPStan\PhpDoc\StubPhpDocProvider;
17-
use PHPStan\PhpDoc\Tag\ParamTag;
1817
use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension;
1918
use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension;
2019
use PHPStan\Reflection\ClassReflection;
@@ -562,14 +561,60 @@ private function createMethod(
562561
$isDeprecated = false;
563562
$isInternal = false;
564563
$isFinal = false;
564+
if (
565+
$methodReflection->isConstructor()
566+
&& $declaringClass->getFileName() !== false
567+
) {
568+
foreach ($methodReflection->getParameters() as $parameter) {
569+
if (!method_exists($parameter, 'isPromoted') || !$parameter->isPromoted()) {
570+
continue;
571+
}
572+
573+
if (!$methodReflection->getDeclaringClass()->hasProperty($parameter->getName())) {
574+
continue;
575+
}
576+
577+
$parameterProperty = $methodReflection->getDeclaringClass()->getProperty($parameter->getName());
578+
if (!method_exists($parameterProperty, 'isPromoted') || !$parameterProperty->isPromoted()) {
579+
continue;
580+
}
581+
if ($parameterProperty->getDocComment() === false) {
582+
continue;
583+
}
584+
585+
$propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc(
586+
$declaringClass->getFileName(),
587+
$declaringClassName,
588+
$declaringTraitName,
589+
$methodReflection->getName(),
590+
$parameterProperty->getDocComment()
591+
);
592+
$varTags = $propertyDocblock->getVarTags();
593+
if (isset($varTags[0]) && count($varTags) === 1) {
594+
$phpDocType = $varTags[0]->getType();
595+
} elseif (isset($varTags[$parameter->getName()])) {
596+
$phpDocType = $varTags[$parameter->getName()]->getType();
597+
} else {
598+
continue;
599+
}
600+
601+
$phpDocParameterTypes[$parameter->getName()] = $phpDocType;
602+
}
603+
}
565604
if ($resolvedPhpDoc !== null) {
566605
$templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
567-
$phpDocParameterTypes = array_map(static function (ParamTag $tag) use ($phpDocBlockClassReflection): Type {
568-
return TemplateTypeHelper::resolveTemplateTypes(
569-
$tag->getType(),
606+
foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
607+
if (array_key_exists($paramName, $phpDocParameterTypes)) {
608+
continue;
609+
}
610+
$phpDocParameterTypes[$paramName] = $paramTag->getType();
611+
}
612+
foreach ($phpDocParameterTypes as $paramName => $paramType) {
613+
$phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes(
614+
$paramType,
570615
$phpDocBlockClassReflection->getActiveTemplateTypeMap()
571616
);
572-
}, $resolvedPhpDoc->getParamTags());
617+
}
573618
$nativeReturnType = TypehintHelper::decideTypeFromReflection(
574619
$methodReflection->getReturnType(),
575620
null,

tests/PHPStan/Analyser/data/promoted-properties-types.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PromotedPropertiesTypes;
44

5+
use function PHPStan\Analyser\assertNativeType;
56
use function PHPStan\Analyser\assertType;
67

78
/**
@@ -14,6 +15,7 @@ class Foo
1415
* @param array<int, string> $anotherPhpDocArray
1516
* @param T $anotherTemplateProperty
1617
* @param string $bothProperty
18+
* @param array<string> $anotherBothProperty
1719
*/
1820
public function __construct(
1921
public $noType,
@@ -23,8 +25,18 @@ public function __construct(
2325
/** @var array<int, string> */ public array $yetAnotherPhpDocArray,
2426
/** @var T */ public $templateProperty,
2527
public $anotherTemplateProperty,
26-
/** @var int */ public $bothProperty
27-
) { }
28+
/** @var int */ public $bothProperty,
29+
/** @var array<int> */ public $anotherBothProperty
30+
) {
31+
assertType('array<int, string>', $phpDocArray);
32+
assertNativeType('mixed', $phpDocArray);
33+
assertType('array<int, string>', $anotherPhpDocArray);
34+
assertNativeType('mixed', $anotherPhpDocArray);
35+
assertType('array<int, string>', $yetAnotherPhpDocArray);
36+
assertNativeType('array', $yetAnotherPhpDocArray);
37+
assertType('int', $bothProperty);
38+
assertType('array<int>', $anotherBothProperty);
39+
}
2840

2941
}
3042

@@ -35,6 +47,7 @@ function (Foo $foo): void {
3547
assertType('array<int, string>', $foo->anotherPhpDocArray);
3648
assertType('array<int, string>', $foo->yetAnotherPhpDocArray);
3749
assertType('int', $foo->bothProperty);
50+
assertType('array<int>', $foo->anotherBothProperty);
3851
};
3952

4053
/**

tests/PHPStan/Rules/Classes/InstantiationRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,22 @@ public function testBug4030(): void
269269
$this->analyse([__DIR__ . '/data/bug-4030.php'], []);
270270
}
271271

272+
public function testPromotedProperties(): void
273+
{
274+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
275+
$this->markTestSkipped('Test requires PHP 8.0.');
276+
}
277+
278+
$this->analyse([__DIR__ . '/data/instantiation-promoted-properties.php'], [
279+
[
280+
'Parameter #2 $bar of class InstantiationPromotedProperties\Foo constructor expects array<string>, array<int, int> given.',
281+
30,
282+
],
283+
[
284+
'Parameter #2 $bar of class InstantiationPromotedProperties\Bar constructor expects array<string>, array<int, int> given.',
285+
33,
286+
],
287+
]);
288+
}
289+
272290
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php // lint >= 8.0
2+
3+
namespace InstantiationPromotedProperties;
4+
5+
class Foo
6+
{
7+
8+
public function __construct(
9+
private array $foo,
10+
/** @var array<string> */private array $bar
11+
) { }
12+
13+
}
14+
15+
class Bar
16+
{
17+
18+
/**
19+
* @param array<string> $bar
20+
*/
21+
public function __construct(
22+
private array $foo,
23+
private array $bar
24+
) { }
25+
26+
}
27+
28+
function () {
29+
new Foo([], ['foo']);
30+
new Foo([], [1]);
31+
32+
new Bar([], ['foo']);
33+
new Bar([], [1]);
34+
};

tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,23 @@ public function testRule(): void
7474
$this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors);
7575
}
7676

77+
public function testPromotedProperties(): void
78+
{
79+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
80+
$this->markTestSkipped('Test requires PHP 8.0.');
81+
}
82+
$this->analyse([__DIR__ . '/data/missing-typehint-promoted-properties.php'], [
83+
[
84+
'Method MissingTypehintPromotedProperties\Foo::__construct() has parameter $foo with no value type specified in iterable type array.',
85+
8,
86+
"Consider adding something like <fg=cyan>array<Foo></> to the PHPDoc.\nYou can turn off this check by setting <fg=cyan>checkMissingIterableValueType: false</> in your <fg=cyan>%configurationFile%</>.",
87+
],
88+
[
89+
'Method MissingTypehintPromotedProperties\Bar::__construct() has parameter $foo with no value type specified in iterable type array.',
90+
21,
91+
"Consider adding something like <fg=cyan>array<Foo></> to the PHPDoc.\nYou can turn off this check by setting <fg=cyan>checkMissingIterableValueType: false</> in your <fg=cyan>%configurationFile%</>.",
92+
],
93+
]);
94+
}
95+
7796
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.0
2+
3+
namespace MissingTypehintPromotedProperties;
4+
5+
class Foo
6+
{
7+
8+
public function __construct(
9+
private array $foo,
10+
/** @var array<string> */private array $bar
11+
) { }
12+
13+
}
14+
15+
class Bar
16+
{
17+
18+
/**
19+
* @param array<string> $bar
20+
*/
21+
public function __construct(
22+
private array $foo,
23+
private array $bar
24+
) { }
25+
26+
}

0 commit comments

Comments
 (0)