Skip to content

Commit dc8a226

Browse files
calebdwondrejmirtes
authored andcommitted
test: add failing test case for bug #11857
1 parent d9b383f commit dc8a226

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Analyser\Analyser;
6+
use PHPStan\Analyser\Error;
7+
use PHPStan\File\FileHelper;
8+
use PHPStan\Testing\PHPStanTestCase;
9+
use Throwable;
10+
11+
class Bug11857Test extends PHPStanTestCase
12+
{
13+
14+
public function dataIntegrationTests(): iterable
15+
{
16+
yield [__DIR__ . '/data/bug-11857.php'];
17+
}
18+
19+
/** @dataProvider dataIntegrationTests */
20+
public function testIntegration(string $file): void
21+
{
22+
$this->assertNoErrors($this->runAnalyse($file));
23+
}
24+
25+
/** @return Error[] */
26+
private function runAnalyse(string $file): array
27+
{
28+
$file = $this->getFileHelper()->normalizePath($file);
29+
30+
$analyser = self::getContainer()->getByType(Analyser::class);
31+
$fileHelper = self::getContainer()->getByType(FileHelper::class);
32+
33+
$errors = $analyser->analyse([$file], null, null, true, null)->getErrors();
34+
35+
foreach ($errors as $error) {
36+
$this->assertSame($fileHelper->normalizePath($file), $error->getFilePath());
37+
}
38+
39+
return $errors;
40+
}
41+
42+
public static function getAdditionalConfigFiles(): array
43+
{
44+
return [__DIR__ . '/bug-11857.neon'];
45+
}
46+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
-
3+
class: Bug11857\RelationDynamicMethodReturnTypeExtension
4+
tags:
5+
- phpstan.broker.dynamicMethodReturnTypeExtension
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
use function PHPStan\Testing\assertType;
14+
15+
class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
16+
{
17+
public function getClass(): string
18+
{
19+
return Model::class;
20+
}
21+
22+
public function isMethodSupported(MethodReflection $methodReflection): bool
23+
{
24+
return $methodReflection->getName() === 'belongsTo';
25+
}
26+
27+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type {
28+
$returnType = $methodReflection->getVariants()[0]->getReturnType();
29+
$argType = $scope->getType($methodCall->getArgs()[0]->value);
30+
$modelClass = $argType->getClassStringObjectType()->getObjectClassNames()[0];
31+
32+
return new GenericObjectType($returnType->getObjectClassNames()[0], [
33+
new ObjectType($modelClass),
34+
$scope->getType($methodCall->var),
35+
]);
36+
}
37+
}
38+
39+
abstract class Model
40+
{
41+
/** @return BelongsTo<*, *> */
42+
public function belongsTo(string $related): BelongsTo
43+
{
44+
return new BelongsTo();
45+
}
46+
}
47+
48+
/**
49+
* @template TRelatedModel of Model
50+
* @template TDeclaringModel of Model
51+
*/
52+
class BelongsTo {}
53+
54+
class User extends Model {}
55+
56+
class Post extends Model
57+
{
58+
/** @return BelongsTo<User, $this> */
59+
public function user(): BelongsTo
60+
{
61+
return $this->belongsTo(User::class);
62+
}
63+
64+
/** @return BelongsTo<User, self> */
65+
public function userSelf(): BelongsTo
66+
{
67+
/** @phpstan-ignore return.type */
68+
return $this->belongsTo(User::class);
69+
}
70+
}
71+
72+
class ChildPost extends Post {}
73+
74+
final class Comment extends Model
75+
{
76+
// This model is final, so either of these
77+
// two methods would work. It seems that
78+
// PHPStan is automatically converting the
79+
// `$this` to a `self` type in the user docblock,
80+
// but it is not doing so likewise for the `$this`
81+
// that is returned by the dynamic return extension.
82+
83+
/** @return BelongsTo<User, $this> */
84+
public function user(): BelongsTo
85+
{
86+
return $this->belongsTo(User::class);
87+
}
88+
89+
/** @return BelongsTo<User, self> */
90+
public function user2(): BelongsTo
91+
{
92+
return $this->belongsTo(User::class);
93+
}
94+
}
95+
96+
function test(ChildPost $child): void
97+
{
98+
assertType('Bug11857\BelongsTo<Bug11857\User, Bug11857\ChildPost>', $child->user());
99+
// This demonstrates why `$this` is needed in non-final models
100+
assertType('Bug11857\BelongsTo<Bug11857\User, Bug11857\ChildPost>', $child->userSelf());
101+
}

0 commit comments

Comments
 (0)