Skip to content

Commit 5c0e026

Browse files
VincentLangletondrejmirtesgithub-actions[bot]staabmclaude
authored
Fix #13828: Reference to static const behaves as self const (#5510)
Co-authored-by: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com> Co-authored-by: Markus Staab <markus.staab@redaxo.de> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: phpstan-bot <ondrej+phpstanbot@mirtes.cz>
1 parent 91f823b commit 5c0e026

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
@@ -58,6 +58,7 @@
5858
use PHPStan\Type\BenevolentUnionType;
5959
use PHPStan\Type\BooleanType;
6060
use PHPStan\Type\CallableType;
61+
use PHPStan\Type\ClassConstantAccessType;
6162
use PHPStan\Type\ClassStringType;
6263
use PHPStan\Type\ClosureType;
6364
use PHPStan\Type\ConditionalType;
@@ -1103,9 +1104,14 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
11031104
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
11041105
}
11051106

1107+
$isStatic = false;
11061108
if ($nameScope->getClassName() !== null) {
11071109
switch (strtolower($constExpr->className)) {
11081110
case 'static':
1111+
$className = $nameScope->getClassName();
1112+
$isStatic = true;
1113+
break;
1114+
11091115
case 'self':
11101116
$className = $nameScope->getClassName();
11111117
break;
@@ -1133,11 +1139,19 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
11331139
}
11341140
$classReflection = $this->getReflectionProvider()->getClass($className);
11351141

1142+
if ($isStatic && $classReflection->isFinal()) {
1143+
$isStatic = false;
1144+
}
1145+
11361146
$constantName = $constExpr->name;
11371147
if (!$classReflection->hasConstant($constantName)) {
11381148
return new ErrorType();
11391149
}
11401150

1151+
if ($isStatic) {
1152+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1153+
}
1154+
11411155
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
11421156
if ($reflectionConstant === false) {
11431157
return new ErrorType();
@@ -1193,9 +1207,14 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
11931207
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
11941208
}
11951209

1210+
$isStatic = false;
11961211
if ($nameScope->getClassName() !== null) {
11971212
switch (strtolower($constExpr->className)) {
11981213
case 'static':
1214+
$className = $nameScope->getClassName();
1215+
$isStatic = true;
1216+
break;
1217+
11991218
case 'self':
12001219
$className = $nameScope->getClassName();
12011220
break;
@@ -1224,6 +1243,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12241243

12251244
$classReflection = $this->getReflectionProvider()->getClass($className);
12261245

1246+
if ($isStatic && $classReflection->isFinal()) {
1247+
$isStatic = false;
1248+
}
1249+
12271250
$constantName = $constExpr->name;
12281251
if (Strings::contains($constantName, '*')) {
12291252
// convert * into .*? and escape everything else so the constants can be matched against the pattern
@@ -1268,6 +1291,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12681291
return new EnumCaseObjectType($classReflection->getName(), $constantName);
12691292
}
12701293

1294+
if ($isStatic) {
1295+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1296+
}
1297+
12711298
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
12721299
if ($reflectionConstant === false) {
12731300
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)