Skip to content

Commit 8dfb6d3

Browse files
committed
Adjust method's ReturnTypeRule for property hooks
1 parent b216d06 commit 8dfb6d3

File tree

4 files changed

+172
-12
lines changed

4 files changed

+172
-12
lines changed

src/Reflection/Php/PhpMethodFromParserNodeReflection.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,38 @@ public function getName(): string
164164
return sprintf('$%s::%s', $this->hookForProperty, $function->name->toString());
165165
}
166166

167+
/**
168+
* @phpstan-assert-if-true !null $this->getHookedPropertyName()
169+
* @phpstan-assert-if-true !null $this->getPropertyHookName()
170+
*/
171+
public function isPropertyHook(): bool
172+
{
173+
return $this->hookForProperty !== null;
174+
}
175+
176+
public function getHookedPropertyName(): ?string
177+
{
178+
return $this->hookForProperty;
179+
}
180+
181+
/**
182+
* @return 'get'|'set'|null
183+
*/
184+
public function getPropertyHookName(): ?string
185+
{
186+
$function = $this->getFunctionLike();
187+
if (!$function instanceof Node\PropertyHook) {
188+
return null;
189+
}
190+
191+
$name = $function->name->toLowerString();
192+
if (!in_array($name, ['get', 'set'], true)) {
193+
throw new ShouldNotHappenException(sprintf('Unknown property hook: %s', $name));
194+
}
195+
196+
return $name;
197+
}
198+
167199
public function isStatic(): bool
168200
{
169201
$method = $this->getClassMethod();

src/Rules/Methods/ReturnTypeRule.php

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use function count;
2222
use function sprintf;
2323
use function strtolower;
24+
use function ucfirst;
2425

2526
/**
2627
* @implements Rule<Node\Stmt\Return_>
@@ -52,31 +53,38 @@ public function processNode(Node $node, Scope $scope): array
5253
return [];
5354
}
5455

56+
if ($method->isPropertyHook()) {
57+
$methodDescription = sprintf(
58+
'%s hook for property %s::$%s',
59+
ucfirst($method->getPropertyHookName()),
60+
$method->getDeclaringClass()->getDisplayName(),
61+
$method->getHookedPropertyName(),
62+
);
63+
} else {
64+
$methodDescription = sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName());
65+
}
66+
5567
$returnType = $method->getReturnType();
5668
$errors = $this->returnTypeCheck->checkReturnType(
5769
$scope,
5870
$returnType,
5971
$node->expr,
6072
$node,
6173
sprintf(
62-
'Method %s::%s() should return %%s but empty return statement found.',
63-
$method->getDeclaringClass()->getDisplayName(),
64-
$method->getName(),
74+
'%s should return %%s but empty return statement found.',
75+
$methodDescription,
6576
),
6677
sprintf(
67-
'Method %s::%s() with return type void returns %%s but should not return anything.',
68-
$method->getDeclaringClass()->getDisplayName(),
69-
$method->getName(),
78+
'%s with return type void returns %%s but should not return anything.',
79+
$methodDescription,
7080
),
7181
sprintf(
72-
'Method %s::%s() should return %%s but returns %%s.',
73-
$method->getDeclaringClass()->getDisplayName(),
74-
$method->getName(),
82+
'%s should return %%s but returns %%s.',
83+
$methodDescription,
7584
),
7685
sprintf(
77-
'Method %s::%s() should never return but return statement found.',
78-
$method->getDeclaringClass()->getDisplayName(),
79-
$method->getName(),
86+
'%s should never return but return statement found.',
87+
$methodDescription,
8088
),
8189
$method->isGenerator(),
8290
);

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,4 +1101,41 @@ public function testBug12223(): void
11011101
$this->analyse([__DIR__ . '/data/bug-12223.php'], []);
11021102
}
11031103

1104+
public function testPropertyHooks(): void
1105+
{
1106+
if (PHP_VERSION_ID < 80400) {
1107+
self::markTestSkipped('Test requires PHP 8.4.');
1108+
}
1109+
1110+
$this->analyse([__DIR__ . '/data/property-hooks-return.php'], [
1111+
[
1112+
'Get hook for property PropertyHooksReturn\Foo::$i should return int but returns string.',
1113+
11,
1114+
],
1115+
[
1116+
'Set hook for property PropertyHooksReturn\Foo::$i with return type void returns int but should not return anything.',
1117+
21,
1118+
],
1119+
[
1120+
'Get hook for property PropertyHooksReturn\Foo::$s should return non-empty-string but returns \'\'.',
1121+
29,
1122+
],
1123+
[
1124+
'Get hook for property PropertyHooksReturn\GenericFoo::$a should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.',
1125+
48,
1126+
'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
1127+
],
1128+
[
1129+
'Get hook for property PropertyHooksReturn\GenericFoo::$b should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.',
1130+
63,
1131+
'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
1132+
],
1133+
[
1134+
'Get hook for property PropertyHooksReturn\GenericFoo::$c should return T of PropertyHooksReturn\Foo but returns PropertyHooksReturn\Foo.',
1135+
73,
1136+
'Type PropertyHooksReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
1137+
],
1138+
]);
1139+
}
1140+
11041141
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php // lint >= 8.4
2+
3+
namespace PropertyHooksReturn;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
get {
10+
if (rand(0, 1)) {
11+
return 'foo';
12+
}
13+
14+
return 1;
15+
}
16+
set {
17+
if (rand(0, 1)) {
18+
return;
19+
}
20+
21+
return 1;
22+
}
23+
}
24+
25+
/** @var non-empty-string */
26+
public string $s {
27+
get {
28+
if (rand(0, 1)) {
29+
return '';
30+
}
31+
32+
return 'foo';
33+
}
34+
}
35+
36+
}
37+
38+
/**
39+
* @template T of Foo
40+
*/
41+
class GenericFoo
42+
{
43+
44+
/** @var T */
45+
public Foo $a {
46+
get {
47+
if (rand(0, 1)) {
48+
return new Foo();
49+
}
50+
51+
return $this->a;
52+
}
53+
}
54+
55+
/**
56+
* @param T $c
57+
*/
58+
public function __construct(
59+
/** @var T */
60+
public Foo $b {
61+
get {
62+
if (rand(0, 1)) {
63+
return new Foo();
64+
}
65+
66+
return $this->b;
67+
}
68+
},
69+
70+
public Foo $c {
71+
get {
72+
if (rand(0, 1)) {
73+
return new Foo();
74+
}
75+
76+
return $this->c;
77+
}
78+
}
79+
)
80+
{
81+
}
82+
83+
}

0 commit comments

Comments
 (0)