Skip to content

Commit 447d581

Browse files
committed
reproduce a weird bug with closures
1 parent c586014 commit 447d581

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

Diff for: tests/PHPStan/Analyser/WeirdBugTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class WeirdBugTest extends TypeInferenceTestCase
8+
{
9+
10+
public function dataFileAsserts(): iterable
11+
{
12+
yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-bug.php');
13+
}
14+
15+
/**
16+
* @dataProvider dataFileAsserts
17+
* @param mixed ...$args
18+
*/
19+
public function testFileAsserts(
20+
string $assertType,
21+
string $file,
22+
...$args,
23+
): void
24+
{
25+
$this->assertFileAsserts($assertType, $file, ...$args);
26+
}
27+
28+
public static function getAdditionalConfigFiles(): array
29+
{
30+
return [
31+
__DIR__ . '/weird-bug-config.neon',
32+
];
33+
}
34+
35+
}

Diff for: tests/PHPStan/Analyser/data/weird-bug-extensions.php

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
namespace WeirdBugExtensions;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\OutOfClassScope;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\ClassMemberReflection;
9+
use PHPStan\Reflection\ClassReflection;
10+
use PHPStan\Reflection\FunctionVariant;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Reflection\MethodsClassReflectionExtension;
13+
use PHPStan\Reflection\Native\NativeParameterReflection;
14+
use PHPStan\Reflection\ParameterReflection;
15+
use PHPStan\Reflection\PassedByReference;
16+
use PHPStan\Reflection\ReflectionProvider;
17+
use PHPStan\TrinaryLogic;
18+
use PHPStan\Type\CallableType;
19+
use PHPStan\Type\Generic\GenericObjectType;
20+
use PHPStan\Type\Generic\TemplateTypeMap;
21+
use PHPStan\Type\ObjectType;
22+
use PHPStan\Type\Type;
23+
24+
use function PHPStan\Testing\assertType;
25+
26+
class MethodParameterClosureTypeExtension implements \PHPStan\Type\MethodParameterClosureTypeExtension
27+
{
28+
29+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
30+
{
31+
return $methodReflection->getDeclaringClass()->getName() === \WeirdBug\Builder::class &&
32+
$parameter->getName() === 'callback' &&
33+
$methodReflection->getName() === 'methodWithCallback';
34+
}
35+
36+
public function getTypeFromMethodCall(
37+
MethodReflection $methodReflection,
38+
MethodCall $methodCall,
39+
ParameterReflection $parameter,
40+
Scope $scope
41+
): ?Type {
42+
return new CallableType(
43+
[
44+
new NativeParameterReflection('test', false, new GenericObjectType(\WeirdBug\Builder::class, [new ObjectType(\WeirdBug\SubModel::class)]), PassedByReference::createNo(), false, null), // @phpstan-ignore-line
45+
]
46+
);
47+
}
48+
}
49+
50+
class BuilderForwardsCallsToAnotherBuilderExtensions implements MethodsClassReflectionExtension
51+
{
52+
53+
public function __construct(private ReflectionProvider $reflectionProvider)
54+
{
55+
}
56+
57+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
58+
{
59+
return $classReflection->getName() === \WeirdBug\Builder::class && $this->reflectionProvider->getClass(\WeirdBug\AnotherBuilder::class)->hasNativeMethod($methodName);
60+
}
61+
62+
public function getMethod(
63+
ClassReflection $classReflection,
64+
string $methodName
65+
): MethodReflection {
66+
$ref = $this->reflectionProvider->getClass(\WeirdBug\AnotherBuilder::class)->getNativeMethod($methodName);
67+
68+
/** @var ObjectType $model */
69+
$model = $classReflection->getActiveTemplateTypeMap()->getType('T');
70+
71+
return new BuilderMethodReflection(
72+
$methodName,
73+
$classReflection,
74+
$ref->getVariants()[0]->getParameters(),
75+
$model->getMethod('getBuilder', new OutOfClassScope())->getVariants()[0]->getReturnType(),
76+
$ref->getVariants()[0]->isVariadic()
77+
);
78+
}
79+
}
80+
81+
final class BuilderMethodReflection implements MethodReflection
82+
{
83+
private Type $returnType;
84+
85+
/** @param ParameterReflection[] $parameters */
86+
public function __construct(private string $methodName, private ClassReflection $classReflection, private array $parameters, Type|null $returnType = null, private bool $isVariadic = false)
87+
{
88+
$this->returnType = $returnType ?? new ObjectType(\WeirdBug\Builder::class);
89+
}
90+
91+
public function getDeclaringClass(): ClassReflection
92+
{
93+
return $this->classReflection;
94+
}
95+
96+
public function isStatic(): bool
97+
{
98+
return true;
99+
}
100+
101+
public function isPrivate(): bool
102+
{
103+
return false;
104+
}
105+
106+
public function isPublic(): bool
107+
{
108+
return true;
109+
}
110+
111+
public function getName(): string
112+
{
113+
return $this->methodName;
114+
}
115+
116+
public function getPrototype(): ClassMemberReflection
117+
{
118+
return $this;
119+
}
120+
121+
/**
122+
* {@inheritDoc}
123+
*/
124+
public function getVariants(): array
125+
{
126+
return [
127+
new FunctionVariant(
128+
TemplateTypeMap::createEmpty(),
129+
null,
130+
$this->parameters,
131+
$this->isVariadic,
132+
$this->returnType,
133+
),
134+
];
135+
}
136+
137+
public function getDocComment(): string|null
138+
{
139+
return null;
140+
}
141+
142+
public function isDeprecated(): TrinaryLogic
143+
{
144+
return TrinaryLogic::createNo();
145+
}
146+
147+
public function getDeprecatedDescription(): string|null
148+
{
149+
return null;
150+
}
151+
152+
public function isFinal(): TrinaryLogic
153+
{
154+
return TrinaryLogic::createNo();
155+
}
156+
157+
public function isInternal(): TrinaryLogic
158+
{
159+
return TrinaryLogic::createNo();
160+
}
161+
162+
public function getThrowType(): Type|null
163+
{
164+
return null;
165+
}
166+
167+
public function hasSideEffects(): TrinaryLogic
168+
{
169+
return TrinaryLogic::createMaybe();
170+
}
171+
}

Diff for: tests/PHPStan/Analyser/data/weird-bug.php

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace WeirdBug;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Model {
8+
/**
9+
* @return Builder<static>
10+
*/
11+
public static function getBuilder(): Builder
12+
{
13+
return new Builder(new static()); // @phpstan-ignore-line
14+
}
15+
}
16+
17+
class SubModel extends Model {}
18+
19+
/**
20+
* @template T of Model
21+
*/
22+
class Builder {
23+
24+
/**
25+
* @param T $model
26+
*/
27+
public function __construct(private Model $model) // @phpstan-ignore-line
28+
{
29+
}
30+
31+
public function methodWithCallback(\Closure $callback): void
32+
{
33+
$callback($this);
34+
}
35+
}
36+
37+
class AnotherBuilder {
38+
public function someMethod(): self
39+
{
40+
return $this;
41+
}
42+
}
43+
44+
function test(): void
45+
{
46+
SubModel::getBuilder()->methodWithCallback(function (Builder $builder, $value) {
47+
assertType('WeirdBug\Builder<WeirdBug\SubModel>', $builder);
48+
return $builder->someMethod();
49+
});
50+
}

Diff for: tests/PHPStan/Analyser/weird-bug-config.neon

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
-
3+
class: WeirdBugExtensions\MethodParameterClosureTypeExtension
4+
tags:
5+
- phpstan.methodParameterClosureTypeExtension
6+
7+
-
8+
class: WeirdBugExtensions\BuilderForwardsCallsToAnotherBuilderExtensions
9+
tags:
10+
- phpstan.broker.methodsClassReflectionExtension

0 commit comments

Comments
 (0)