Skip to content

Commit da3e8c8

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix static::CONST in PHPDoc resolving as self::CONST
- Added ClassConstantAccessType, a late-resolvable type that defers constant resolution until the actual class is known via static type transformation - Modified TypeNodeResolver::resolveConstTypeNode() to distinguish static from self when resolving class constant PHPDoc types - When static::CONST appears in a PHPDoc (e.g. @return static::FOO_BAR) on a non-final class, the constant is now resolved on the actual called-on class rather than always using the declaring class - New regression test in tests/PHPStan/Analyser/nsrt/bug-13828.php Closes phpstan/phpstan#13828
1 parent ba4fe14 commit da3e8c8

File tree

3 files changed

+142
-0
lines changed

3 files changed

+142
-0
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 9 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;
@@ -1188,9 +1189,13 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
11881189
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
11891190
}
11901191

1192+
$isStaticKeyword = false;
11911193
if ($nameScope->getClassName() !== null) {
11921194
switch (strtolower($constExpr->className)) {
11931195
case 'static':
1196+
$isStaticKeyword = true;
1197+
$className = $nameScope->getClassName();
1198+
break;
11941199
case 'self':
11951200
$className = $nameScope->getClassName();
11961201
break;
@@ -1263,6 +1268,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12631268
return new EnumCaseObjectType($classReflection->getName(), $constantName);
12641269
}
12651270

1271+
if ($isStaticKeyword && !$classReflection->isFinal()) {
1272+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1273+
}
1274+
12661275
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
12671276
if ($reflectionConstant === false) {
12681277
return new ErrorType();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 $classType,
20+
private string $constantName,
21+
)
22+
{
23+
}
24+
25+
public function getReferencedClasses(): array
26+
{
27+
return $this->classType->getReferencedClasses();
28+
}
29+
30+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
31+
{
32+
return $this->classType->getReferencedTemplateTypes($positionVariance);
33+
}
34+
35+
public function equals(Type $type): bool
36+
{
37+
return $type instanceof self
38+
&& $this->classType->equals($type->classType)
39+
&& $this->constantName === $type->constantName;
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 !($this->classType instanceof StaticType);
50+
}
51+
52+
protected function getResult(): Type
53+
{
54+
foreach ($this->classType->getObjectClassReflections() as $classReflection) {
55+
if (!$classReflection->hasConstant($this->constantName)) {
56+
continue;
57+
}
58+
59+
return $classReflection->getConstant($this->constantName)->getValueType();
60+
}
61+
62+
return new MixedType();
63+
}
64+
65+
public function traverse(callable $cb): Type
66+
{
67+
$newClassType = $cb($this->classType);
68+
if ($newClassType !== $this->classType) {
69+
return new self($newClassType, $this->constantName);
70+
}
71+
72+
return $this;
73+
}
74+
75+
public function traverseSimultaneously(Type $right, callable $cb): Type
76+
{
77+
if (!$right instanceof self) {
78+
return $this;
79+
}
80+
81+
$newClassType = $cb($this->classType, $right->classType);
82+
if ($newClassType !== $this->classType) {
83+
return new self($newClassType, $this->constantName);
84+
}
85+
86+
return $this;
87+
}
88+
89+
public function toPhpDocNode(): TypeNode
90+
{
91+
return new ConstTypeNode(new ConstFetchNode('static', $this->constantName));
92+
}
93+
94+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13828;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
abstract class FooBar
10+
{
11+
const FOO_BAR = 'foo';
12+
13+
/** @return static::FOO_BAR */
14+
public function test(): string
15+
{
16+
return static::FOO_BAR;
17+
}
18+
}
19+
20+
class Foo extends FooBar
21+
{
22+
const FOO_BAR = 'foo';
23+
}
24+
25+
class Bar extends FooBar
26+
{
27+
const FOO_BAR = 'bar';
28+
}
29+
30+
class Baz extends FooBar
31+
{
32+
// Does not override FOO_BAR, inherits 'foo'
33+
}
34+
35+
function () {
36+
assertType("'foo'", (new Foo())->test()); // Foo::FOO_BAR = 'foo'
37+
assertType("'bar'", (new Bar())->test()); // Bar::FOO_BAR = 'bar'
38+
assertType("'foo'", (new Baz())->test()); // Baz inherits FOO_BAR = 'foo'
39+
};

0 commit comments

Comments
 (0)