Skip to content

Commit e30d1e5

Browse files
committed
Downgrade $this to static when calling methods on static type
- In StaticType::transformStaticType(), when the caller is a StaticType (not ThisType) and the method return type is ThisType, downgrade it to StaticType since $this cannot be guaranteed on a static caller - New regression test in tests/PHPStan/Rules/Methods/data/bug-5946.php - The root cause was that ThisType::changeBaseClass() returns a new ThisType, preserving $this semantics even when the caller is only static Fixes phpstan/phpstan#5946
1 parent 5d52bba commit e30d1e5

File tree

4 files changed

+54
-0
lines changed

4 files changed

+54
-0
lines changed

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ The `tryRemove()` method handles removing a `ConstantStringType` (e.g., `'Car'`)
299299

300300
This affects match expression exhaustiveness: `class-string<FinalA|FinalB>` matched against `FinalA::class` and `FinalB::class` is exhaustive only because both classes are final.
301301

302+
### StaticType::transformStaticType and ThisType downgrading
303+
304+
`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`.
305+
302306
### PHPDoc inheritance
303307

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

src/Type/StaticType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop
356356
$isFinal = $classReflection->isFinal();
357357
}
358358
$type = $type->changeBaseClass($classReflection);
359+
360+
// When calling a method on a `static` type (not `$this`),
361+
// `$this` return type should be downgraded to `static`
362+
// because we can't guarantee the exact instance.
363+
if ($type instanceof ThisType && !$this instanceof ThisType) {
364+
$type = new self($type->getClassReflection(), $type->getSubtractedType());
365+
}
366+
359367
if (!$isFinal || $type instanceof ThisType) {
360368
return $traverse($type);
361369
}

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,4 +1291,18 @@ public function testBug10771(): void
12911291
$this->analyse([__DIR__ . '/data/bug-10771.php'], []);
12921292
}
12931293

1294+
public function testBug5946(): void
1295+
{
1296+
$this->analyse([__DIR__ . '/data/bug-5946.php'], [
1297+
[
1298+
'Method Bug5946\Model::getModel() should return $this(Bug5946\Model) but returns static(Bug5946\Model).',
1299+
21,
1300+
],
1301+
[
1302+
'Method Bug5946\Model::getModel() should return $this(Bug5946\Model) but returns static(Bug5946\Model).',
1303+
23,
1304+
],
1305+
]);
1306+
}
1307+
12941308
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug5946;
4+
5+
class Model
6+
{
7+
/**
8+
* @return static
9+
*/
10+
public function getParent()
11+
{
12+
return new static();
13+
}
14+
15+
/**
16+
* @return $this
17+
*/
18+
public function getModel(bool $useParent)
19+
{
20+
if ($useParent) {
21+
return $this->getParent()->getModel(false); // error - returns static not $this
22+
} elseif (mt_rand() === 0) {
23+
return $this->getParent(); // error - returns static not $this
24+
}
25+
26+
return $this;
27+
}
28+
}

0 commit comments

Comments
 (0)