Skip to content

Commit 6dcda72

Browse files
authored
Do not transform $this in return type even in final classes
1 parent d999117 commit 6dcda72

File tree

12 files changed

+201
-3
lines changed

12 files changed

+201
-3
lines changed

.github/workflows/e2e-tests.yml

+4
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ jobs:
255255
echo "$OUTPUT"
256256
../bashunit -a contains 'Child process error (exit code 255): PHP Fatal error' "$OUTPUT"
257257
../bashunit -a contains 'Result is incomplete because of severe errors.' "$OUTPUT"
258+
- script: |
259+
cd e2e/bug-11857
260+
composer install
261+
../../bin/phpstan
258262
259263
steps:
260264
- name: "Checkout"

e2e/bug-11857/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor

e2e/bug-11857/composer.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"autoload-dev": {
3+
"classmap": ["src/"]
4+
}
5+
}

e2e/bug-11857/composer.lock

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/bug-11857/phpstan.neon

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
parameters:
2+
level: 8
3+
paths:
4+
- src
5+
6+
services:
7+
-
8+
class: Bug11857\RelationDynamicMethodReturnTypeExtension
9+
tags:
10+
- phpstan.broker.dynamicMethodReturnTypeExtension

e2e/bug-11857/src/extension.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Bug11857;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\MethodReflection;
7+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
8+
use PHPStan\Type\Generic\GenericObjectType;
9+
use PHPStan\Type\ObjectType;
10+
use PHPStan\Type\Type;
11+
use PhpParser\Node\Expr\MethodCall;
12+
13+
class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
14+
{
15+
public function getClass(): string
16+
{
17+
return Model::class;
18+
}
19+
20+
public function isMethodSupported(MethodReflection $methodReflection): bool
21+
{
22+
return $methodReflection->getName() === 'belongsTo';
23+
}
24+
25+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type {
26+
$returnType = $methodReflection->getVariants()[0]->getReturnType();
27+
$argType = $scope->getType($methodCall->getArgs()[0]->value);
28+
$modelClass = $argType->getClassStringObjectType()->getObjectClassNames()[0];
29+
30+
return new GenericObjectType($returnType->getObjectClassNames()[0], [
31+
new ObjectType($modelClass),
32+
$scope->getType($methodCall->var),
33+
]);
34+
}
35+
}
36+

e2e/bug-11857/src/test.php

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Bug11857;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
abstract class Model
8+
{
9+
/** @return BelongsTo<*, *> */
10+
public function belongsTo(string $related): BelongsTo
11+
{
12+
return new BelongsTo();
13+
}
14+
}
15+
16+
/**
17+
* @template TRelatedModel of Model
18+
* @template TDeclaringModel of Model
19+
*/
20+
class BelongsTo {}
21+
22+
class User extends Model {}
23+
24+
class Post extends Model
25+
{
26+
/** @return BelongsTo<User, $this> */
27+
public function user(): BelongsTo
28+
{
29+
return $this->belongsTo(User::class);
30+
}
31+
32+
/** @return BelongsTo<User, self> */
33+
public function userSelf(): BelongsTo
34+
{
35+
/** @phpstan-ignore return.type */
36+
return $this->belongsTo(User::class);
37+
}
38+
}
39+
40+
class ChildPost extends Post {}
41+
42+
final class Comment extends Model
43+
{
44+
// This model is final, so either of these
45+
// two methods would work. It seems that
46+
// PHPStan is automatically converting the
47+
// `$this` to a `self` type in the user docblock,
48+
// but it is not doing so likewise for the `$this`
49+
// that is returned by the dynamic return extension.
50+
51+
/** @return BelongsTo<User, $this> */
52+
public function user(): BelongsTo
53+
{
54+
return $this->belongsTo(User::class);
55+
}
56+
57+
/** @return BelongsTo<User, self> */
58+
public function user2(): BelongsTo
59+
{
60+
/** @phpstan-ignore return.type */
61+
return $this->belongsTo(User::class);
62+
}
63+
}
64+
65+
function test(ChildPost $child): void
66+
{
67+
assertType('Bug11857\BelongsTo<Bug11857\User, Bug11857\ChildPost>', $child->user());
68+
// This demonstrates why `$this` is needed in non-final models
69+
assertType('Bug11857\BelongsTo<Bug11857\User, Bug11857\Post>', $child->userSelf()); // should be: Bug11857\BelongsTo<Bug11857\User, Bug11857\ChildPost>
70+
}

src/Analyser/MutatingScope.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -3015,7 +3015,7 @@ private function transformStaticType(Type $type): Type
30153015
if ($type instanceof StaticType) {
30163016
$classReflection = $this->getClassReflection();
30173017
$changedType = $type->changeBaseClass($classReflection);
3018-
if ($classReflection->isFinal()) {
3018+
if ($classReflection->isFinal() && !$type instanceof ThisType) {
30193019
$changedType = $changedType->getStaticObjectType();
30203020
}
30213021
return $traverse($changedType);

src/Analyser/NodeScopeResolver.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6159,7 +6159,7 @@ private function transformStaticType(ClassReflection $declaringClass, Type $type
61596159
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
61606160
if ($type instanceof StaticType) {
61616161
$changedType = $type->changeBaseClass($declaringClass);
6162-
if ($declaringClass->isFinal()) {
6162+
if ($declaringClass->isFinal() && !$type instanceof ThisType) {
61636163
$changedType = $changedType->getStaticObjectType();
61646164
}
61656165
return $traverse($changedType);

src/Type/StaticType.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop
299299
$isFinal = $classReflection->isFinal();
300300
}
301301
$type = $type->changeBaseClass($classReflection);
302-
if (!$isFinal) {
302+
if (!$isFinal || $type instanceof ThisType) {
303303
return $type;
304304
}
305305

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -1059,4 +1059,9 @@ public function testBug11663(): void
10591059
$this->analyse([__DIR__ . '/data/bug-11663.php'], []);
10601060
}
10611061

1062+
public function testBug11857(): void
1063+
{
1064+
$this->analyse([__DIR__ . '/data/bug-11857-builder.php'], []);
1065+
}
1066+
10621067
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug11857Builder;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param array<string, mixed> $attributes
10+
* @return $this
11+
*/
12+
public function filter(array $attributes): static
13+
{
14+
return $this;
15+
}
16+
17+
/**
18+
* @param array<string, mixed> $attributes
19+
* @return $this
20+
*/
21+
public function filterUsingRequest(array $attributes): static
22+
{
23+
return $this->filter($attributes);
24+
}
25+
26+
}
27+
28+
final class FinalFoo
29+
{
30+
31+
/**
32+
* @param array<string, mixed> $attributes
33+
* @return $this
34+
*/
35+
public function filter(array $attributes): static
36+
{
37+
return $this;
38+
}
39+
40+
/**
41+
* @param array<string, mixed> $attributes
42+
* @return $this
43+
*/
44+
public function filterUsingRequest(array $attributes): static
45+
{
46+
return $this->filter($attributes);
47+
}
48+
49+
}

0 commit comments

Comments
 (0)