Skip to content

Commit 325e624

Browse files
committed
ShortGetPropertyHookReturnTypeRule - level 3
1 parent 8dfb6d3 commit 325e624

File tree

5 files changed

+201
-1
lines changed

5 files changed

+201
-1
lines changed

conf/config.level3.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ rules:
2020
- PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule
2121
- PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule
2222
- PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRefRule
23+
- PHPStan\Rules\Properties\ShortGetPropertyHookReturnTypeRule
2324
- PHPStan\Rules\Properties\TypesAssignedToPropertiesRule
2425
- PHPStan\Rules\Variables\ParameterOutAssignedTypeRule
2526
- PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule

src/Node/InPropertyHookNode.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function getClassReflection(): ClassReflection
2727
return $this->classReflection;
2828
}
2929

30-
public function getMethodReflection(): PhpMethodFromParserNodeReflection
30+
public function getHookReflection(): PhpMethodFromParserNodeReflection
3131
{
3232
return $this->hookReflection;
3333
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InPropertyHookNode;
8+
use PHPStan\Rules\FunctionReturnTypeCheck;
9+
use PHPStan\Rules\Rule;
10+
11+
/**
12+
* @implements Rule<InPropertyHookNode>
13+
*/
14+
final class ShortGetPropertyHookReturnTypeRule implements Rule
15+
{
16+
17+
public function __construct(private FunctionReturnTypeCheck $returnTypeCheck)
18+
{
19+
}
20+
21+
public function getNodeType(): string
22+
{
23+
return InPropertyHookNode::class;
24+
}
25+
26+
public function processNode(Node $node, Scope $scope): array
27+
{
28+
// return statements in long property hook bodies are checked by Methods\ReturnTypeRule
29+
// short set property hook type is checked by TypesAssignedToPropertiesRule
30+
$hookReflection = $node->getHookReflection();
31+
if ($hookReflection->getPropertyHookName() !== 'get') {
32+
return [];
33+
}
34+
35+
$originalHookNode = $node->getOriginalNode();
36+
$hookBody = $originalHookNode->body;
37+
if (!$hookBody instanceof Node\Expr) {
38+
return [];
39+
}
40+
41+
$methodDescription = sprintf(
42+
'Get hook for property %s::$%s',
43+
$hookReflection->getDeclaringClass()->getDisplayName(),
44+
$hookReflection->getHookedPropertyName(),
45+
);
46+
47+
$returnType = $hookReflection->getReturnType();
48+
49+
return $this->returnTypeCheck->checkReturnType(
50+
$scope,
51+
$returnType,
52+
$hookBody,
53+
$node,
54+
sprintf(
55+
'%s should return %%s but empty return statement found.',
56+
$methodDescription,
57+
),
58+
sprintf(
59+
'%s with return type void returns %%s but should not return anything.',
60+
$methodDescription,
61+
),
62+
sprintf(
63+
'%s should return %%s but returns %%s.',
64+
$methodDescription,
65+
),
66+
sprintf(
67+
'%s should never return but return statement found.',
68+
$methodDescription,
69+
),
70+
$hookReflection->isGenerator(),
71+
);
72+
}
73+
74+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\FunctionReturnTypeCheck;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Rules\RuleLevelHelper;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<ShortGetPropertyHookReturnTypeRule>
12+
*/
13+
final class ShortGetPropertyHookReturnTypeRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new ShortGetPropertyHookReturnTypeRule(
19+
new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, false))
20+
);
21+
}
22+
23+
public function testRule(): void
24+
{
25+
if (PHP_VERSION_ID < 80400) {
26+
$this->markTestSkipped('Test requires PHP 8.4.');
27+
}
28+
29+
$this->analyse([__DIR__ . '/data/short-get-property-hook-return.php'], [
30+
[
31+
'Get hook for property ShortGetPropertyHookReturn\Foo::$i should return int but returns string.',
32+
9,
33+
],
34+
[
35+
'Get hook for property ShortGetPropertyHookReturn\Foo::$s should return non-empty-string but returns \'\'.',
36+
18,
37+
],
38+
[
39+
'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$a should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.',
40+
36,
41+
'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
42+
],
43+
[
44+
'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$b should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.',
45+
50,
46+
'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
47+
],
48+
[
49+
'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$c should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.',
50+
59,
51+
'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
52+
],
53+
]);
54+
}
55+
56+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php // lint >= 8.4
2+
3+
namespace ShortGetPropertyHookReturn;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
get => 'foo';
10+
}
11+
12+
public int $i2 {
13+
get => 1;
14+
}
15+
16+
/** @var non-empty-string */
17+
public string $s {
18+
get => '';
19+
}
20+
21+
/** @var non-empty-string */
22+
public string $s2 {
23+
get => 'foo';
24+
}
25+
26+
}
27+
28+
/**
29+
* @template T of Foo
30+
*/
31+
class GenericFoo
32+
{
33+
34+
/** @var T */
35+
public Foo $a {
36+
get => new Foo();
37+
}
38+
39+
/** @var T */
40+
public Foo $a2 {
41+
get => $this->a2;
42+
}
43+
44+
/**
45+
* @param T $c
46+
*/
47+
public function __construct(
48+
/** @var T */
49+
public Foo $b {
50+
get => new Foo();
51+
},
52+
53+
/** @var T */
54+
public Foo $b2 {
55+
get => $this->b2;
56+
},
57+
58+
public Foo $c {
59+
get => new Foo();
60+
},
61+
62+
public Foo $c2 {
63+
get => $this->c2;
64+
}
65+
)
66+
{
67+
}
68+
69+
}

0 commit comments

Comments
 (0)