Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ The `tryRemove()` method handles removing a `ConstantStringType` (e.g., `'Car'`)

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

### StaticType::transformStaticType and ThisType downgrading

`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`.

### PHPDoc inheritance

PHPDoc types (`@return`, `@param`, `@throws`, `@property`) are inherited through class hierarchies. Bugs arise when:
Expand Down
8 changes: 8 additions & 0 deletions src/Type/StaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop
$isFinal = $classReflection->isFinal();
}
$type = $type->changeBaseClass($classReflection);

// When calling a method on a `static` type (not `$this`),
// `$this` return type should be downgraded to `static`
// because we can't guarantee the exact instance.
if ($type instanceof ThisType && !$this instanceof ThisType) {
$type = new self($type->getClassReflection(), $type->getSubtractedType());
}

if (!$isFinal || $type instanceof ThisType) {
return $traverse($type);
}
Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1291,4 +1291,18 @@ public function testBug10771(): void
$this->analyse([__DIR__ . '/data/bug-10771.php'], []);
}

public function testBug5946(): void
{
$this->analyse([__DIR__ . '/data/bug-5946.php'], [
[
'Method Bug5946\Model::getModel() should return $this(Bug5946\Model) but returns static(Bug5946\Model).',
21,
],
[
'Method Bug5946\Model::getModel() should return $this(Bug5946\Model) but returns static(Bug5946\Model).',
23,
],
]);
}

}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-5946.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace Bug5946;

class Model
{
/**
* @return static
*/
public function getParent()
{
return new static();
}

/**
* @return $this
*/
public function getModel(bool $useParent)
{
if ($useParent) {
return $this->getParent()->getModel(false); // error - returns static not $this
} elseif (mt_rand() === 0) {
return $this->getParent(); // error - returns static not $this
}

return $this;
}
}
Loading