Skip to content

Commit a2c60cd

Browse files
ondrejmirtesphpstan-bot
authored andcommitted
Fix static::CONST in PHPDoc treating static as self
- Added ClassConstantAccessType that wraps StaticType + constant name and implements LateResolvableType, deferring resolution until the caller type is known - Modified TypeNodeResolver::resolveConstTypeNode() and resolveArrayShapeOffsetType() to create ClassConstantAccessType when the keyword is 'static' instead of resolving eagerly like 'self' - The StaticType inside ClassConstantAccessType gets replaced with the concrete ObjectType during CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType(), then the constant is resolved on the correct class - New regression test in tests/PHPStan/Analyser/nsrt/bug-13828.php
1 parent 944077a commit a2c60cd

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ This affects match expression exhaustiveness: `class-string<FinalA|FinalB>` matc
307307

308308
`StaticType::transformStaticType()` is used when resolving method return types on a `StaticType` caller. It traverses the return type and transforms `StaticType`/`ThisType` instances via `changeBaseClass()`. Since `ThisType extends StaticType`, both are caught by the `$type instanceof StaticType` check. The critical invariant: when the **caller** is a `StaticType` (not `ThisType`) and the method's return type contains `ThisType`, the `ThisType` must be downgraded to a plain `StaticType`. This is because `$this` (the exact instance) cannot be guaranteed when calling on a `static` type (which could be any subclass instance). `ThisType::changeBaseClass()` returns a new `ThisType`, which preserves the `$this` semantics — so the downgrade must happen explicitly after `changeBaseClass()`. The `CallbackUnresolvedMethodPrototypeReflection` at line 91 also has special handling for `ThisType` return types intersected with `selfOutType`.
309309

310+
### PHPDoc `static::CONST` resolution and ClassConstantAccessType
311+
312+
PHPDoc types like `@return static::SOME_CONST` require late static binding semantics — the constant should resolve based on the caller's class, not the declaring class. `TypeNodeResolver::resolveConstTypeNode()` and `resolveArrayShapeOffsetType()` handle this by creating a `ClassConstantAccessType(StaticType, constantName)` when the keyword is `static` (not `self`). This type implements `LateResolvableType` and wraps a `StaticType` that gets replaced with the actual caller's `ObjectType` via `CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()` during method call resolution. The `getResult()` method then resolves the constant on the concrete type. For final classes, the `$isStatic` flag is cleared and the constant is resolved eagerly (same as `self`). The wildcard case (`static::CONST_*`) falls back to `getValueType()` from the class reflection.
313+
310314
### PHPDoc inheritance
311315

312316
PHPDoc types (`@return`, `@param`, `@throws`, `@property`) are inherited through class hierarchies. Bugs arise when:

src/PhpDoc/TypeNodeResolver.php

Lines changed: 37 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;
@@ -1098,9 +1099,14 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
10981099
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
10991100
}
11001101

1102+
$isStatic = false;
11011103
if ($nameScope->getClassName() !== null) {
11021104
switch (strtolower($constExpr->className)) {
11031105
case 'static':
1106+
$className = $nameScope->getClassName();
1107+
$isStatic = true;
1108+
break;
1109+
11041110
case 'self':
11051111
$className = $nameScope->getClassName();
11061112
break;
@@ -1128,11 +1134,19 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
11281134
}
11291135
$classReflection = $this->getReflectionProvider()->getClass($className);
11301136

1137+
if ($isStatic && $classReflection->isFinal()) {
1138+
$isStatic = false;
1139+
}
1140+
11311141
$constantName = $constExpr->name;
11321142
if (!$classReflection->hasConstant($constantName)) {
11331143
return new ErrorType();
11341144
}
11351145

1146+
if ($isStatic) {
1147+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1148+
}
1149+
11361150
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
11371151
if ($reflectionConstant === false) {
11381152
return new ErrorType();
@@ -1188,9 +1202,14 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
11881202
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
11891203
}
11901204

1205+
$isStatic = false;
11911206
if ($nameScope->getClassName() !== null) {
11921207
switch (strtolower($constExpr->className)) {
11931208
case 'static':
1209+
$className = $nameScope->getClassName();
1210+
$isStatic = true;
1211+
break;
1212+
11941213
case 'self':
11951214
$className = $nameScope->getClassName();
11961215
break;
@@ -1219,6 +1238,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12191238

12201239
$classReflection = $this->getReflectionProvider()->getClass($className);
12211240

1241+
if ($isStatic && $classReflection->isFinal()) {
1242+
$isStatic = false;
1243+
}
1244+
12221245
$constantName = $constExpr->name;
12231246
if (Strings::contains($constantName, '*')) {
12241247
// convert * into .*? and escape everything else so the constants can be matched against the pattern
@@ -1235,6 +1258,16 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12351258
continue;
12361259
}
12371260

1261+
if ($isStatic) {
1262+
$constantReflection = $classReflection->getConstant($classConstantName);
1263+
if (!$constantReflection->isFinal() && !$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType()) {
1264+
$constantTypes[] = new MixedType();
1265+
continue;
1266+
}
1267+
$constantTypes[] = $constantReflection->getValueType();
1268+
continue;
1269+
}
1270+
12381271
$declaringClassName = $reflectionConstant->getDeclaringClass()->getName();
12391272
if (!$this->getReflectionProvider()->hasClass($declaringClassName)) {
12401273
continue;
@@ -1263,6 +1296,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
12631296
return new EnumCaseObjectType($classReflection->getName(), $constantName);
12641297
}
12651298

1299+
if ($isStatic) {
1300+
return new ClassConstantAccessType(new StaticType($classReflection), $constantName);
1301+
}
1302+
12661303
$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
12671304
if ($reflectionConstant === false) {
12681305
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->type->equals($type->type)
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 !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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
}

0 commit comments

Comments
 (0)