Skip to content

Commit c91df5c

Browse files
committed
Fix MissingReturnRule for property hooks
1 parent 1b41ebf commit c91df5c

File tree

5 files changed

+121
-9
lines changed

5 files changed

+121
-9
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
use PHPStan\Node\MethodReturnStatementsNode;
115115
use PHPStan\Node\NoopExpressionNode;
116116
use PHPStan\Node\PropertyAssignNode;
117+
use PHPStan\Node\PropertyHookStatementNode;
117118
use PHPStan\Node\ReturnStatement;
118119
use PHPStan\Node\StaticMethodCallableNode;
119120
use PHPStan\Node\UnreachableStatementNode;
@@ -343,6 +344,7 @@ public function processStmtNodes(
343344
$stmtCount = count($stmts);
344345
$shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
345346
|| $parentNode instanceof Node\Stmt\ClassMethod
347+
|| $parentNode instanceof PropertyHookStatementNode
346348
|| $parentNode instanceof Expr\Closure;
347349
foreach ($stmts as $i => $stmt) {
348350
if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) {
@@ -360,7 +362,7 @@ public function processStmtNodes(
360362
$hasYield = $hasYield || $statementResult->hasYield();
361363

362364
if ($shouldCheckLastStatement && $isLast) {
363-
/** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
365+
/** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|PropertyHookStatementNode|Expr\Closure $parentNode */
364366
$parentNode = $parentNode;
365367

366368
$endStatements = $statementResult->getEndStatements();
@@ -377,7 +379,7 @@ public function processStmtNodes(
377379
$endStatementResult->getThrowPoints(),
378380
$endStatementResult->getImpurePoints(),
379381
),
380-
$parentNode->returnType !== null,
382+
$parentNode->getReturnType() !== null,
381383
), $endStatementResult->getScope());
382384
}
383385
} else {
@@ -391,7 +393,7 @@ public function processStmtNodes(
391393
$statementResult->getThrowPoints(),
392394
$statementResult->getImpurePoints(),
393395
),
394-
$parentNode->returnType !== null,
396+
$parentNode->getReturnType() !== null,
395397
), $scope);
396398
}
397399
}
@@ -414,9 +416,9 @@ public function processStmtNodes(
414416

415417
$statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
416418
if ($stmtCount === 0 && $shouldCheckLastStatement) {
417-
/** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
419+
/** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|PropertyHookStatementNode|Expr\Closure $parentNode */
418420
$parentNode = $parentNode;
419-
$returnTypeNode = $parentNode->returnType;
421+
$returnTypeNode = $parentNode->getReturnType();
420422
if ($parentNode instanceof Expr\Closure) {
421423
$parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes());
422424
}
@@ -4700,7 +4702,7 @@ private function processPropertyHooks(
47004702
$this->processExprNode($stmt, $hook->body, $hookScope, $nodeCallback, ExpressionContext::createTopLevel());
47014703
$nodeCallback(new PropertyAssignNode(new PropertyFetch(new Variable('this'), $propertyName, $hook->body->getAttributes()), $hook->body, false), $hookScope);
47024704
} elseif (is_array($hook->body)) {
4703-
$this->processStmtNodes($stmt, $hook->body, $hookScope, $nodeCallback, StatementContext::createTopLevel());
4705+
$this->processStmtNodes(new PropertyHookStatementNode($hook), $hook->body, $hookScope, $nodeCallback, StatementContext::createTopLevel());
47044706
}
47054707

47064708
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\Node\PropertyHook;
6+
use PhpParser\Node\Stmt;
7+
8+
/**
9+
* This class exists because PhpParser\Node\PropertyHook
10+
* is not a Stmt, but we need to pass it to
11+
* a few places that expect a Stmt.
12+
*
13+
* This is because PhpParser\Node\PropertyHook
14+
* is likely the one of two PhpParser nodes which contains Stmt[]
15+
* but itself is not a Stmt.
16+
*
17+
* The other one is Expr\Closure, but that one can
18+
* at least be wrapped in Stmt\Expression.
19+
*/
20+
final class PropertyHookStatementNode extends Stmt implements VirtualNode
21+
{
22+
23+
public function __construct(private PropertyHook $propertyHook)
24+
{
25+
parent::__construct($this->propertyHook->getAttributes());
26+
}
27+
28+
public function getPropertyHook(): PropertyHook
29+
{
30+
return $this->propertyHook;
31+
}
32+
33+
/**
34+
* @return null
35+
*/
36+
public function getReturnType()
37+
{
38+
return null;
39+
}
40+
41+
public function getType(): string
42+
{
43+
return 'PHPStan_Node_PropertyHookStatementNode';
44+
}
45+
46+
public function getSubNodeNames(): array
47+
{
48+
return [];
49+
}
50+
51+
52+
}

src/Rules/Missing/MissingReturnRule.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use PhpParser\Node;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\Node\ExecutionEndNode;
9-
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
1212
use PHPStan\ShouldNotHappenException;
@@ -19,6 +19,7 @@
1919
use PHPStan\Type\VerbosityLevel;
2020
use PHPStan\Type\VoidType;
2121
use function sprintf;
22+
use function ucfirst;
2223

2324
/**
2425
* @implements Rule<ExecutionEndNode>
@@ -55,8 +56,12 @@ public function processNode(Node $node, Scope $scope): array
5556
}
5657
} elseif ($scopeFunction !== null) {
5758
$returnType = $scopeFunction->getReturnType();
58-
if ($scopeFunction instanceof MethodReflection) {
59-
$description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName());
59+
if ($scopeFunction instanceof PhpMethodFromParserNodeReflection) {
60+
if (!$scopeFunction->isPropertyHook()) {
61+
$description = sprintf('Method %s::%s()', $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getName());
62+
} else {
63+
$description = sprintf('%s hook for property %s::$%s', ucfirst($scopeFunction->getPropertyHookName()), $scopeFunction->getDeclaringClass()->getDisplayName(), $scopeFunction->getHookedPropertyName());
64+
}
6065
} else {
6166
$description = sprintf('Function %s()', $scopeFunction->getName());
6267
}

tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,23 @@ public function testBug9374(): void
351351
$this->analyse([__DIR__ . '/data/bug-9374.php'], []);
352352
}
353353

354+
public function testPropertyHooks(): void
355+
{
356+
if (PHP_VERSION_ID < 80400) {
357+
$this->markTestSkipped('Test requires PHP 8.4.');
358+
}
359+
360+
$this->checkExplicitMixedMissingReturn = true;
361+
$this->analyse([__DIR__ . '/data/property-hooks-missing-return.php'], [
362+
[
363+
'Get hook for property PropertyHooksMissingReturn\Foo::$i should return int but return statement is missing.',
364+
10,
365+
],
366+
[
367+
'Get hook for property PropertyHooksMissingReturn\Foo::$j should return int but return statement is missing.',
368+
23,
369+
],
370+
]);
371+
}
372+
354373
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php // lint >= 8.4
2+
3+
namespace PropertyHooksMissingReturn;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
get {
10+
if (rand(0, 1)) {
11+
12+
} else {
13+
return 1;
14+
}
15+
}
16+
17+
set {
18+
// set hook returns void
19+
}
20+
}
21+
22+
public int $j {
23+
get {
24+
25+
}
26+
}
27+
28+
public int $ok {
29+
get {
30+
return $this->ok + 1;
31+
}
32+
}
33+
34+
}

0 commit comments

Comments
 (0)