Skip to content

Commit 5baf33d

Browse files
committed
Fix MissingReturnRule for property hooks
1 parent 551aab5 commit 5baf33d

File tree

4 files changed

+113
-6
lines changed

4 files changed

+113
-6
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+
}

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+
'Method PropertyHooksMissingReturn\Foo::$i::get() should return int but return statement is missing.',
364+
10,
365+
],
366+
[
367+
'Method PropertyHooksMissingReturn\Foo::$j::get() 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)