Skip to content

Commit d225a68

Browse files
committed
Fixed handling of static in parameter type in implemented interfaces
1 parent 00a8ba9 commit d225a68

File tree

6 files changed

+243
-6
lines changed

6 files changed

+243
-6
lines changed

src/Rules/Methods/MethodSignatureRule.php

+28-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
use PHPStan\TrinaryLogic;
1414
use PHPStan\Type\Generic\TemplateTypeHelper;
1515
use PHPStan\Type\MixedType;
16+
use PHPStan\Type\StaticType;
1617
use PHPStan\Type\Type;
1718
use PHPStan\Type\TypehintHelper;
19+
use PHPStan\Type\TypeTraverser;
1820
use PHPStan\Type\VerbosityLevel;
1921
use PHPStan\Type\VoidType;
2022

@@ -62,6 +64,7 @@ public function processNode(Node $node, Scope $scope): array
6264
$parameters = ParametersAcceptorSelector::selectSingle($method->getVariants());
6365

6466
$errors = [];
67+
$declaringClass = $method->getDeclaringClass();
6568
foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) {
6669
$parentVariants = $parentMethod->getVariants();
6770
if (count($parentVariants) !== 1) {
@@ -72,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array
7275
continue;
7376
}
7477

75-
[$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($parameters, $parentParameters);
78+
[$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $parameters, $parentParameters);
7679
if ($returnTypeCompatibility->no() || (!$returnTypeCompatibility->yes() && $this->reportMaybes)) {
7780
$errors[] = RuleErrorBuilder::message(sprintf(
7881
'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()',
@@ -86,7 +89,7 @@ public function processNode(Node $node, Scope $scope): array
8689
))->build();
8790
}
8891

89-
$parameterResults = $this->checkParameterTypeCompatibility($parameters->getParameters(), $parentParameters->getParameters());
92+
$parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $parameters->getParameters(), $parentParameters->getParameters());
9093
foreach ($parameterResults as $parameterIndex => [$parameterResult, $parameterType, $parentParameterType]) {
9194
if ($parameterResult->yes()) {
9295
continue;
@@ -149,6 +152,7 @@ private function collectParentMethods(string $methodName, ClassReflection $class
149152
* @return array{TrinaryLogic, Type, Type}
150153
*/
151154
private function checkReturnTypeCompatibility(
155+
ClassReflection $declaringClass,
152156
ParametersAcceptorWithPhpDocs $currentVariant,
153157
ParametersAcceptorWithPhpDocs $parentVariant
154158
): array
@@ -157,10 +161,11 @@ private function checkReturnTypeCompatibility(
157161
$currentVariant->getNativeReturnType(),
158162
TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType())
159163
);
160-
$parentReturnType = TypehintHelper::decideType(
164+
$originalParentReturnType = TypehintHelper::decideType(
161165
$parentVariant->getNativeReturnType(),
162166
TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType())
163167
);
168+
$parentReturnType = $this->transformStaticType($declaringClass, $originalParentReturnType);
164169
// Allow adding `void` return type hints when the parent defines no return type
165170
if ($returnType instanceof VoidType && $parentReturnType instanceof MixedType) {
166171
return [TrinaryLogic::createYes(), $returnType, $parentReturnType];
@@ -174,7 +179,7 @@ private function checkReturnTypeCompatibility(
174179
return [$parentReturnType->isSuperTypeOf($returnType), TypehintHelper::decideType(
175180
$currentVariant->getNativeReturnType(),
176181
$currentVariant->getPhpDocReturnType()
177-
), $parentReturnType];
182+
), $originalParentReturnType];
178183
}
179184

180185
/**
@@ -183,6 +188,7 @@ private function checkReturnTypeCompatibility(
183188
* @return array<int, array{TrinaryLogic, Type, Type}>
184189
*/
185190
private function checkParameterTypeCompatibility(
191+
ClassReflection $declaringClass,
186192
array $parameters,
187193
array $parentParameters
188194
): array
@@ -198,18 +204,34 @@ private function checkParameterTypeCompatibility(
198204
$parameter->getNativeType(),
199205
TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType())
200206
);
201-
$parentParameterType = TypehintHelper::decideType(
207+
$originalParameterType = TypehintHelper::decideType(
202208
$parentParameter->getNativeType(),
203209
TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType())
204210
);
211+
$parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType);
205212

206213
$parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType), TypehintHelper::decideType(
207214
$parameter->getNativeType(),
208215
$parameter->getPhpDocType()
209-
), $parentParameterType];
216+
), $originalParameterType];
210217
}
211218

212219
return $parameterResults;
213220
}
214221

222+
private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
223+
{
224+
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
225+
if ($type instanceof StaticType) {
226+
$changedType = $type->changeBaseClass($declaringClass);
227+
if ($declaringClass->isFinal()) {
228+
$changedType = $changedType->getStaticObjectType();
229+
}
230+
return $traverse($changedType);
231+
}
232+
233+
return $traverse($type);
234+
});
235+
}
236+
215237
}

tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php

+31
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,35 @@ public function testBug3523(): void
302302
]);
303303
}
304304

305+
public function testBug4707(): void
306+
{
307+
$this->reportMaybes = true;
308+
$this->reportStatic = true;
309+
$this->analyse([__DIR__ . '/data/bug-4707.php'], [
310+
[
311+
'Return type (array<int, Bug4707\Row2>) of method Bug4707\Block2::getChildren() should be compatible with return type (array<int, Bug4707\ChildNodeInterface<static(Bug4707\ParentNodeInterface)>>) of method Bug4707\ParentNodeInterface::getChildren()',
312+
38,
313+
],
314+
]);
315+
}
316+
317+
public function testBug4707Covariant(): void
318+
{
319+
$this->reportMaybes = true;
320+
$this->reportStatic = true;
321+
$this->analyse([__DIR__ . '/data/bug-4707-covariant.php'], [
322+
[
323+
'Return type (array<int, Bug4707Covariant\Row2>) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (array<int, Bug4707Covariant\ChildNodeInterface<static(Bug4707Covariant\ParentNodeInterface)>>) of method Bug4707Covariant\ParentNodeInterface::getChildren()',
324+
38,
325+
],
326+
]);
327+
}
328+
329+
public function testBug4707Two(): void
330+
{
331+
$this->reportMaybes = true;
332+
$this->reportStatic = true;
333+
$this->analyse([__DIR__ . '/data/bug-4707-two.php'], []);
334+
}
335+
305336
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Bug4707Covariant;
4+
5+
/**
6+
* @template-covariant TParent of ParentNodeInterface
7+
*/
8+
interface ChildNodeInterface
9+
{
10+
/** @return TParent */
11+
public function getParent(): ParentNodeInterface;
12+
}
13+
14+
interface ParentNodeInterface
15+
{
16+
/** @return list<ChildNodeInterface<static>> */
17+
public function getChildren(): array;
18+
}
19+
20+
final class Block implements ParentNodeInterface
21+
{
22+
/** @var list<Row> */
23+
private $rows = [];
24+
25+
/** @return list<Row> */
26+
public function getChildren(): array
27+
{
28+
return $this->rows;
29+
}
30+
}
31+
32+
class Block2 implements ParentNodeInterface
33+
{
34+
/** @var list<Row2> */
35+
private $rows = [];
36+
37+
/** @return list<Row2> */
38+
public function getChildren(): array
39+
{
40+
return $this->rows;
41+
}
42+
}
43+
44+
/** @implements ChildNodeInterface<Block> */
45+
final class Row implements ChildNodeInterface
46+
{
47+
/** @var Block $parent */
48+
private $parent;
49+
50+
public function getParent(): Block
51+
{
52+
return $this->parent;
53+
}
54+
}
55+
56+
/** @implements ChildNodeInterface<Block2> */
57+
final class Row2 implements ChildNodeInterface
58+
{
59+
/** @var Block2 $parent */
60+
private $parent;
61+
62+
public function getParent(): Block2
63+
{
64+
return $this->parent;
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Bug4707Three;
4+
5+
/**
6+
* @template TParent of ParentNodeInterface
7+
*/
8+
interface ChildNodeInterface
9+
{}
10+
11+
interface ParentNodeInterface
12+
{
13+
/** @return ChildNodeInterface<Block> */
14+
public function getChildren();
15+
}
16+
17+
/** @implements ChildNodeInterface<Block> */
18+
final class Row implements ChildNodeInterface {}
19+
20+
class Block implements ParentNodeInterface
21+
{
22+
/** @return Row */
23+
public function getChildren() {
24+
return new Row();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Bug4707Two;
4+
5+
/**
6+
* @template TParent of ParentNodeInterface
7+
*/
8+
interface ChildNodeInterface
9+
{}
10+
11+
interface ParentNodeInterface
12+
{
13+
/** @return ChildNodeInterface<Block> */
14+
public function getChildren();
15+
}
16+
17+
/** @implements ChildNodeInterface<Block> */
18+
final class Row implements ChildNodeInterface {}
19+
20+
final class Block implements ParentNodeInterface
21+
{
22+
/** @return Row */
23+
public function getChildren() {
24+
return new Row();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Bug4707;
4+
5+
/**
6+
* @template TParent of ParentNodeInterface
7+
*/
8+
interface ChildNodeInterface
9+
{
10+
/** @return TParent */
11+
public function getParent(): ParentNodeInterface;
12+
}
13+
14+
interface ParentNodeInterface
15+
{
16+
/** @return list<ChildNodeInterface<static>> */
17+
public function getChildren(): array;
18+
}
19+
20+
final class Block implements ParentNodeInterface
21+
{
22+
/** @var list<Row> */
23+
private $rows = [];
24+
25+
/** @return list<Row> */
26+
public function getChildren(): array
27+
{
28+
return $this->rows;
29+
}
30+
}
31+
32+
class Block2 implements ParentNodeInterface
33+
{
34+
/** @var list<Row2> */
35+
private $rows = [];
36+
37+
/** @return list<Row2> */
38+
public function getChildren(): array
39+
{
40+
return $this->rows;
41+
}
42+
}
43+
44+
/** @implements ChildNodeInterface<Block> */
45+
final class Row implements ChildNodeInterface
46+
{
47+
/** @var Block $parent */
48+
private $parent;
49+
50+
public function getParent(): Block
51+
{
52+
return $this->parent;
53+
}
54+
}
55+
56+
/** @implements ChildNodeInterface<Block2> */
57+
final class Row2 implements ChildNodeInterface
58+
{
59+
/** @var Block2 $parent */
60+
private $parent;
61+
62+
public function getParent(): Block2
63+
{
64+
return $this->parent;
65+
}
66+
}

0 commit comments

Comments
 (0)