Skip to content

Commit 9e511e7

Browse files
rvanvelzenondrejmirtes
authored andcommitted
Add @phpstan-self-out support
1 parent ab151d4 commit 9e511e7

25 files changed

+268
-16
lines changed

composer.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"ondram/ci-detector": "^3.4.0",
2525
"ondrejmirtes/better-reflection": "6.1.0",
2626
"phpstan/php-8-stubs": "0.3.38",
27-
"phpstan/phpdoc-parser": "1.8.0",
27+
"phpstan/phpdoc-parser": "dev-self-out",
2828
"react/async": "^3",
2929
"react/child-process": "^0.6.4",
3030
"react/event-loop": "^1.2",
@@ -61,6 +61,12 @@
6161
"rector/rector": "^0.14.3",
6262
"vaimo/composer-patches": "^4.22"
6363
},
64+
"repositories": [
65+
{
66+
"type": "vcs",
67+
"url": "https://github.com/rvanvelzen/phpdoc-parser"
68+
}
69+
],
6470
"config": {
6571
"platform": {
6672
"php": "8.1.99"

composer.lock

+17-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ parameters:
166166
count: 1
167167
path: src/PhpDoc/Tag/ReturnTag.php
168168

169+
-
170+
message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\:\\: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/SelfOutTypeTag.php
173+
169174
-
170175
message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#"
171176
count: 1

src/Analyser/MutatingScope.php

+2
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,7 @@ public function enterClassMethod(
25322532
?bool $isPure = null,
25332533
bool $acceptsNamedArguments = true,
25342534
?Assertions $asserts = null,
2535+
?Type $selfOutType = null,
25352536
): self
25362537
{
25372538
if (!$this->isInClass()) {
@@ -2557,6 +2558,7 @@ public function enterClassMethod(
25572558
$isPure,
25582559
$acceptsNamedArguments,
25592560
$asserts ?? Assertions::createEmpty(),
2561+
$selfOutType,
25602562
),
25612563
!$classMethod->isStatic(),
25622564
);

src/Analyser/NodeScopeResolver.php

+13-3
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ private function processStmtNode(
468468
$hasYield = false;
469469
$throwPoints = [];
470470
$this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
471-
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts] = $this->getPhpDocs($scope, $stmt);
471+
[$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts, $selfOutType] = $this->getPhpDocs($scope, $stmt);
472472

473473
foreach ($stmt->params as $param) {
474474
$this->processParamNode($param, $scope, $nodeCallback);
@@ -491,6 +491,7 @@ private function processStmtNode(
491491
$isPure,
492492
$acceptsNamedArguments,
493493
$asserts,
494+
$selfOutType,
494495
);
495496

496497
if ($stmt->name->toLowerString() === '__construct') {
@@ -2057,6 +2058,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
20572058
$scope = $scope->invalidateExpression($arg->value, true);
20582059
}
20592060
}
2061+
2062+
if ($parametersAcceptor !== null) {
2063+
$selfOutType = $methodReflection->getSelfOutType();
2064+
if ($selfOutType !== null) {
2065+
$scope = $scope->assignExpression($expr->var, TemplateTypeHelper::resolveTemplateTypes($selfOutType, $parametersAcceptor->getResolvedTemplateTypeMap()));
2066+
}
2067+
}
20602068
} else {
20612069
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
20622070
}
@@ -3968,7 +3976,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection
39683976
}
39693977

39703978
/**
3971-
* @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions}
3979+
* @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type}
39723980
*/
39733981
public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
39743982
{
@@ -3984,6 +3992,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
39843992
$acceptsNamedArguments = true;
39853993
$isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
39863994
$asserts = Assertions::createEmpty();
3995+
$selfOutType = null;
39873996
$docComment = $node->getDocComment() !== null
39883997
? $node->getDocComment()->getText()
39893998
: null;
@@ -4093,9 +4102,10 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
40934102
$acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
40944103
$isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
40954104
$asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
4105+
$selfOutType = $resolvedPhpDoc->getThisOutTag() !== null ? $resolvedPhpDoc->getThisOutTag()->getType() : null;
40964106
}
40974107

4098-
return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts];
4108+
return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType];
40994109
}
41004110

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

src/PhpDoc/PhpDocNodeResolver.php

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\PhpDoc\Tag\ParamTag;
1515
use PHPStan\PhpDoc\Tag\PropertyTag;
1616
use PHPStan\PhpDoc\Tag\ReturnTag;
17+
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
1718
use PHPStan\PhpDoc\Tag\TemplateTag;
1819
use PHPStan\PhpDoc\Tag\ThrowsTag;
1920
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
@@ -451,6 +452,18 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco
451452
return $resolved;
452453
}
453454

455+
public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag
456+
{
457+
foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) {
458+
foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) {
459+
$type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope);
460+
return new SelfOutTypeTag($type);
461+
}
462+
}
463+
464+
return null;
465+
}
466+
454467
public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag
455468
{
456469
foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) {

src/PhpDoc/ResolvedPhpDocBlock.php

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\PhpDoc\Tag\ParamTag;
1313
use PHPStan\PhpDoc\Tag\PropertyTag;
1414
use PHPStan\PhpDoc\Tag\ReturnTag;
15+
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
1516
use PHPStan\PhpDoc\Tag\TemplateTag;
1617
use PHPStan\PhpDoc\Tag\ThrowsTag;
1718
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
@@ -90,6 +91,8 @@ class ResolvedPhpDocBlock
9091
/** @var array<AssertTag>|false */
9192
private array|false $assertTags = false;
9293

94+
private SelfOutTypeTag|false|null $selfOutTypeTag = false;
95+
9396
private DeprecatedTag|false|null $deprecatedTag = false;
9497

9598
private ?bool $isDeprecated = null;
@@ -164,6 +167,7 @@ public static function createEmpty(): self
164167
$self->typeAliasTags = [];
165168
$self->typeAliasImportTags = [];
166169
$self->assertTags = [];
170+
$self->selfOutTypeTag = null;
167171
$self->deprecatedTag = null;
168172
$self->isDeprecated = false;
169173
$self->isInternal = false;
@@ -216,6 +220,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
216220
$result->typeAliasTags = $this->getTypeAliasTags();
217221
$result->typeAliasImportTags = $this->getTypeAliasImportTags();
218222
$result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks);
223+
$result->selfOutTypeTag = $this->getThisOutTag();
219224
$result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents);
220225
$result->isDeprecated = $result->deprecatedTag !== null;
221226
$result->isInternal = $this->isInternal();
@@ -297,6 +302,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self
297302
$self->typeAliasTags = $this->typeAliasTags;
298303
$self->typeAliasImportTags = $this->typeAliasImportTags;
299304
$self->assertTags = $assertTags;
305+
$self->selfOutTypeTag = $this->getThisOutTag();
300306
$self->deprecatedTag = $this->deprecatedTag;
301307
$self->isDeprecated = $this->isDeprecated;
302308
$self->isInternal = $this->isInternal;
@@ -522,6 +528,18 @@ public function getAssertTags(): array
522528
return $this->assertTags;
523529
}
524530

531+
public function getThisOutTag(): ?SelfOutTypeTag
532+
{
533+
if ($this->selfOutTypeTag === false) {
534+
$this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag(
535+
$this->phpDocNode,
536+
$this->getNameScope(),
537+
);
538+
}
539+
540+
return $this->selfOutTypeTag;
541+
}
542+
525543
public function getDeprecatedTag(): ?DeprecatedTag
526544
{
527545
if (is_bool($this->deprecatedTag)) {

src/PhpDoc/Tag/SelfOutTypeTag.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc\Tag;
4+
5+
use PHPStan\Type\Type;
6+
7+
class SelfOutTypeTag implements TypedTag
8+
{
9+
10+
public function __construct(private Type $type)
11+
{
12+
}
13+
14+
public function getType(): Type
15+
{
16+
return $this->type;
17+
}
18+
19+
/**
20+
* @return self
21+
*/
22+
public function withType(Type $type): TypedTag
23+
{
24+
return new self($type);
25+
}
26+
27+
}

src/Reflection/Annotations/AnnotationMethodReflection.php

+5
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,9 @@ public function getAsserts(): Assertions
121121
return Assertions::createEmpty();
122122
}
123123

124+
public function getSelfOutType(): ?Type
125+
{
126+
return null;
127+
}
128+
124129
}

src/Reflection/Dummy/ChangedTypeMethodReflection.php

+5
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,9 @@ public function getAsserts(): Assertions
9595
return $this->reflection->getAsserts();
9696
}
9797

98+
public function getSelfOutType(): ?Type
99+
{
100+
return $this->reflection->getSelfOutType();
101+
}
102+
98103
}

src/Reflection/Dummy/DummyMethodReflection.php

+5
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,9 @@ public function getAsserts(): Assertions
102102
return Assertions::createEmpty();
103103
}
104104

105+
public function getSelfOutType(): ?Type
106+
{
107+
return null;
108+
}
109+
105110
}

src/Reflection/ExtendedMethodReflection.php

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PHPStan\Reflection;
44

5+
use PHPStan\Type\Type;
6+
57
/**
68
* The purpose of this interface is to be able to
79
* answer more questions about methods
@@ -18,4 +20,6 @@ interface ExtendedMethodReflection extends MethodReflection
1820

1921
public function getAsserts(): Assertions;
2022

23+
public function getSelfOutType(): ?Type;
24+
2125
}

src/Reflection/Native/NativeMethodReflection.php

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function __construct(
3131
private TrinaryLogic $hasSideEffects,
3232
private ?Type $throwType,
3333
private Assertions $assertions,
34+
private ?Type $selfOutType,
3435
)
3536
{
3637
}
@@ -160,4 +161,9 @@ public function getAsserts(): Assertions
160161
return $this->assertions;
161162
}
162163

164+
public function getSelfOutType(): ?Type
165+
{
166+
return $this->selfOutType;
167+
}
168+
163169
}

src/Reflection/Php/ClosureCallMethodReflection.php

+5
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,9 @@ public function getAsserts(): Assertions
124124
return $this->nativeMethodReflection->getAsserts();
125125
}
126126

127+
public function getSelfOutType(): ?Type
128+
{
129+
return $this->nativeMethodReflection->getSelfOutType();
130+
}
131+
127132
}

src/Reflection/Php/EnumCasesMethodReflection.php

+5
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,9 @@ public function getAsserts(): Assertions
111111
return Assertions::createEmpty();
112112
}
113113

114+
public function getSelfOutType(): ?Type
115+
{
116+
return null;
117+
}
118+
114119
}

0 commit comments

Comments
 (0)