Skip to content

Commit 5d47f7e

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 1b641bd + 5c0e026 commit 5d47f7e

3 files changed

Lines changed: 299 additions & 0 deletions

File tree

src/PhpDoc/TypeNodeResolver.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
use PHPStan\Type\BenevolentUnionType;
6262
use PHPStan\Type\BooleanType;
6363
use PHPStan\Type\CallableType;
64+
use PHPStan\Type\ClassConstantAccessType;
6465
use PHPStan\Type\ClassStringType;
6566
use PHPStan\Type\ClosureType;
6667
use PHPStan\Type\ConditionalType;
@@ -1138,9 +1139,14 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
11381139
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
11391140
}
11401141

1142+
$isStatic = false;
11411143
if ($nameScope->getClassName() !== null) {
11421144
switch (strtolower($constExpr->className)) {
11431145
case 'static':
1146+
$className = $nameScope->getClassName();
1147+
$isStatic = true;
1148+
break;
1149+
11441150
case 'self':
11451151
$className = $nameScope->getClassName();
11461152
break;
@@ -1168,11 +1174,19 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
11681174
}
11691175
$classReflection = $this->getReflectionProvider()->getClass($className);
11701176

1177+
if ($isStatic && $classReflection->isFinal()) {
1178+
$isStatic = false;
1179+
}
1180+
11711181
$constantName = $constExpr->name;
11721182
if (!$classReflection->hasConstant($constantName)) {
11731183
return new ErrorType();
11741184
}
11751185

1186+
if ($isStatic) {
1187+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1188+
}
1189+
11761190
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
11771191
if ($reflectionConstant === false) {
11781192
return new ErrorType();
@@ -1228,9 +1242,14 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12281242
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
12291243
}
12301244

1245+
$isStatic = false;
12311246
if ($nameScope->getClassName() !== null) {
12321247
switch (strtolower($constExpr->className)) {
12331248
case 'static':
1249+
$className = $nameScope->getClassName();
1250+
$isStatic = true;
1251+
break;
1252+
12341253
case 'self':
12351254
$className = $nameScope->getClassName();
12361255
break;
@@ -1259,6 +1278,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12591278

12601279
$classReflection = $this->getReflectionProvider()->getClass($className);
12611280

1281+
if ($isStatic && $classReflection->isFinal()) {
1282+
$isStatic = false;
1283+
}
1284+
12621285
$constantName = $constExpr->name;
12631286
if (Strings::contains($constantName, '*')) {
12641287
// convert * into .*? and escape everything else so the constants can be matched against the pattern
@@ -1303,6 +1326,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
13031326
return new EnumCaseObjectType($classReflection->getName(), $constantName);
13041327
}
13051328

1329+
if ($isStatic) {
1330+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1331+
}
1332+
13061333
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
13071334
if ($reflectionConstant === false) {
13081335
return new ErrorType();
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
6+
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use PHPStan\Type\Generic\TemplateTypeVariance;
9+
use PHPStan\Type\Traits\LateResolvableTypeTrait;
10+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
11+
12+
final class ClassConstantAccessType implements CompoundType, LateResolvableType
13+
{
14+
15+
use LateResolvableTypeTrait;
16+
use NonGeneralizableTypeTrait;
17+
18+
public function __construct(
19+
private Type $type,
20+
private string $constantName,
21+
)
22+
{
23+
}
24+
25+
public function getReferencedClasses(): array
26+
{
27+
return $this->type->getReferencedClasses();
28+
}
29+
30+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
31+
{
32+
return $this->type->getReferencedTemplateTypes($positionVariance);
33+
}
34+
35+
public function equals(Type $type): bool
36+
{
37+
return $type instanceof self
38+
&& $this->constantName === $type->constantName
39+
&& $this->type->equals($type->type);
40+
}
41+
42+
public function describe(VerbosityLevel $level): string
43+
{
44+
return $this->resolve()->describe($level);
45+
}
46+
47+
public function isResolvable(): bool
48+
{
49+
return !TypeUtils::containsTemplateType($this->type);
50+
}
51+
52+
protected function getResult(): Type
53+
{
54+
if ($this->type->hasConstant($this->constantName)->yes()) {
55+
return $this->type->getConstant($this->constantName)->getValueType();
56+
}
57+
58+
return new ErrorType();
59+
}
60+
61+
/**
62+
* @param callable(Type): Type $cb
63+
*/
64+
public function traverse(callable $cb): Type
65+
{
66+
$type = $cb($this->type);
67+
68+
if ($this->type === $type) {
69+
return $this;
70+
}
71+
72+
return new self($type, $this->constantName);
73+
}
74+
75+
public function traverseSimultaneously(Type $right, callable $cb): Type
76+
{
77+
if (!$right instanceof self) {
78+
return $this;
79+
}
80+
81+
$type = $cb($this->type, $right->type);
82+
83+
if ($this->type === $type) {
84+
return $this;
85+
}
86+
87+
return new self($type, $this->constantName);
88+
}
89+
90+
public function toPhpDocNode(): TypeNode
91+
{
92+
return new ConstTypeNode(new ConstFetchNode('static', $this->constantName));
93+
}
94+
95+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
namespace Bug13828;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class FooBar
8+
{
9+
const FOO_BAR = 'foo';
10+
11+
/** @return static::FOO_BAR */
12+
public function test(): string
13+
{
14+
return static::FOO_BAR;
15+
}
16+
}
17+
18+
class BarBaz extends FooBar
19+
{
20+
const FOO_BAR = 'bar';
21+
}
22+
23+
function test(FooBar $foo, BarBaz $bar): void
24+
{
25+
assertType("'foo'", $foo->test());
26+
assertType("'bar'", $bar->test());
27+
}
28+
29+
final class FinalFoo
30+
{
31+
const FOO_BAR = 'foo';
32+
33+
/** @return static::FOO_BAR */
34+
public function test(): string
35+
{
36+
return static::FOO_BAR;
37+
}
38+
}
39+
40+
function testFinal(FinalFoo $foo): void
41+
{
42+
assertType("'foo'", $foo->test());
43+
}
44+
45+
class WithNativeType
46+
{
47+
const string FOO_BAR = 'foo';
48+
49+
/** @return static::FOO_BAR */
50+
public function test(): string
51+
{
52+
return static::FOO_BAR;
53+
}
54+
}
55+
56+
class WithNativeTypeChild extends WithNativeType
57+
{
58+
const string FOO_BAR = 'bar';
59+
}
60+
61+
function testNativeType(WithNativeType $foo, WithNativeTypeChild $bar): void
62+
{
63+
assertType('string', $foo->test());
64+
assertType('string', $bar->test());
65+
}
66+
67+
class WithPhpDocType
68+
{
69+
/** @var non-empty-string */
70+
const FOO_BAR = 'foo';
71+
72+
/** @return static::FOO_BAR */
73+
public function test(): string
74+
{
75+
return static::FOO_BAR;
76+
}
77+
}
78+
79+
class WithPhpDocTypeChild extends WithPhpDocType
80+
{
81+
/** @var non-empty-string */
82+
const FOO_BAR = 'bar';
83+
}
84+
85+
function testPhpDocType(WithPhpDocType $foo, WithPhpDocTypeChild $bar): void
86+
{
87+
assertType('non-empty-string', $foo->test());
88+
assertType('non-empty-string', $bar->test());
89+
}
90+
91+
class WithBothTypes
92+
{
93+
/** @var non-empty-string */
94+
const string FOO_BAR = 'foo';
95+
96+
/** @return static::FOO_BAR */
97+
public function test(): string
98+
{
99+
return static::FOO_BAR;
100+
}
101+
}
102+
103+
class WithBothTypesChild extends WithBothTypes
104+
{
105+
/** @var non-empty-string */
106+
const string FOO_BAR = 'bar';
107+
}
108+
109+
function testBothTypes(WithBothTypes $foo, WithBothTypesChild $bar): void
110+
{
111+
assertType('non-empty-string', $foo->test());
112+
assertType('non-empty-string', $bar->test());
113+
}
114+
115+
class WithFinalConstant
116+
{
117+
final const FOO_BAR = 'foo';
118+
119+
/** @return static::FOO_BAR */
120+
public function test(): string
121+
{
122+
return static::FOO_BAR;
123+
}
124+
}
125+
126+
class WithFinalConstantChild extends WithFinalConstant
127+
{
128+
}
129+
130+
function testFinalConstant(WithFinalConstant $foo, WithFinalConstantChild $bar): void
131+
{
132+
assertType("'foo'", $foo->test());
133+
assertType("'foo'", $bar->test());
134+
}
135+
136+
class WithUntypedConstant
137+
{
138+
const FOO_BAR = 'foo';
139+
140+
/** @return static::FOO_BAR */
141+
public function test(): string
142+
{
143+
return static::FOO_BAR;
144+
}
145+
}
146+
147+
function testUntypedConstant(WithUntypedConstant $foo): void
148+
{
149+
assertType("'foo'", $foo->test());
150+
}
151+
152+
final class FinalChild extends FooBar
153+
{
154+
const FOO_BAR = 'baz';
155+
}
156+
157+
function testFinalChild(FinalChild $foo): void
158+
{
159+
assertType("'baz'", $foo->test());
160+
}
161+
162+
class WithFinalTypedConstant
163+
{
164+
/** @var non-empty-string */
165+
final const string FOO_BAR = 'foo';
166+
167+
/** @return static::FOO_BAR */
168+
public function test(): string
169+
{
170+
return static::FOO_BAR;
171+
}
172+
}
173+
174+
function testFinalTypedConstant(WithFinalTypedConstant $foo): void
175+
{
176+
assertType('non-empty-string', $foo->test());
177+
}

0 commit comments

Comments
 (0)