Skip to content

Commit b4e9e9e

Browse files
committed
Check closure/function/method returning nullsafe by ref
1 parent 38daf37 commit b4e9e9e

File tree

8 files changed

+176
-3
lines changed

8 files changed

+176
-3
lines changed

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ rules:
3737
- PHPStan\Rules\Functions\ExistingClassesInTypehintsRule
3838
- PHPStan\Rules\Functions\InnerFunctionRule
3939
- PHPStan\Rules\Functions\PrintfParametersRule
40+
- PHPStan\Rules\Functions\ReturnNullsafeByRefRule
4041
- PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule
4142
- PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
4243
- PHPStan\Rules\Methods\MissingMethodImplementationRule

src/Node/ClosureReturnStatementsNode.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use PhpParser\NodeAbstract;
77
use PHPStan\Analyser\StatementResult;
88

9-
class ClosureReturnStatementsNode extends NodeAbstract implements VirtualNode
9+
class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode
1010
{
1111

1212
private \PhpParser\Node\Expr\Closure $closureExpr;
@@ -51,6 +51,11 @@ public function getStatementResult(): StatementResult
5151
return $this->statementResult;
5252
}
5353

54+
public function returnsByRef(): bool
55+
{
56+
return $this->closureExpr->byRef;
57+
}
58+
5459
public function getType(): string
5560
{
5661
return 'PHPStan_Node_ClosureReturnStatementsNode';

src/Node/FunctionReturnStatementsNode.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use PhpParser\NodeAbstract;
77
use PHPStan\Analyser\StatementResult;
88

9-
class FunctionReturnStatementsNode extends NodeAbstract implements VirtualNode
9+
class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode
1010
{
1111

12+
private Function_ $function;
13+
1214
/** @var \PHPStan\Node\ReturnStatement[] */
1315
private array $returnStatements;
1416

@@ -26,6 +28,7 @@ public function __construct(
2628
)
2729
{
2830
parent::__construct($function->getAttributes());
31+
$this->function = $function;
2932
$this->returnStatements = $returnStatements;
3033
$this->statementResult = $statementResult;
3134
}
@@ -43,6 +46,11 @@ public function getStatementResult(): StatementResult
4346
return $this->statementResult;
4447
}
4548

49+
public function returnsByRef(): bool
50+
{
51+
return $this->function->byRef;
52+
}
53+
4654
public function getType(): string
4755
{
4856
return 'PHPStan_Node_FunctionReturnStatementsNode';

src/Node/MethodReturnStatementsNode.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use PhpParser\NodeAbstract;
77
use PHPStan\Analyser\StatementResult;
88

9-
class MethodReturnStatementsNode extends NodeAbstract implements VirtualNode
9+
class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode
1010
{
1111

12+
private ClassMethod $classMethod;
13+
1214
/** @var \PHPStan\Node\ReturnStatement[] */
1315
private array $returnStatements;
1416

@@ -26,6 +28,7 @@ public function __construct(
2628
)
2729
{
2830
parent::__construct($method->getAttributes());
31+
$this->classMethod = $method;
2932
$this->returnStatements = $returnStatements;
3033
$this->statementResult = $statementResult;
3134
}
@@ -43,6 +46,11 @@ public function getStatementResult(): StatementResult
4346
return $this->statementResult;
4447
}
4548

49+
public function returnsByRef(): bool
50+
{
51+
return $this->classMethod->byRef;
52+
}
53+
4654
public function getType(): string
4755
{
4856
return 'PHPStan_Node_FunctionReturnStatementsNode';

src/Node/ReturnStatementsNode.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PHPStan\Analyser\StatementResult;
6+
7+
interface ReturnStatementsNode extends VirtualNode
8+
{
9+
10+
/**
11+
* @return \PHPStan\Node\ReturnStatement[]
12+
*/
13+
public function getReturnStatements(): array;
14+
15+
public function getStatementResult(): StatementResult;
16+
17+
public function returnsByRef(): bool;
18+
19+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ReturnStatementsNode;
8+
use PHPStan\Rules\NullsafeCheck;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
12+
/**
13+
* @implements Rule<ReturnStatementsNode>
14+
*/
15+
class ReturnNullsafeByRefRule implements Rule
16+
{
17+
18+
private NullsafeCheck $nullsafeCheck;
19+
20+
public function __construct(NullsafeCheck $nullsafeCheck)
21+
{
22+
$this->nullsafeCheck = $nullsafeCheck;
23+
}
24+
25+
public function getNodeType(): string
26+
{
27+
return ReturnStatementsNode::class;
28+
}
29+
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
if (!$node->returnsByRef()) {
33+
return [];
34+
}
35+
36+
$errors = [];
37+
foreach ($node->getReturnStatements() as $returnStatement) {
38+
$returnNode = $returnStatement->getReturnNode();
39+
if ($returnNode->expr === null) {
40+
continue;
41+
}
42+
43+
if (!$this->nullsafeCheck->containsNullSafe($returnNode->expr)) {
44+
continue;
45+
}
46+
47+
$errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->line($returnNode->getLine())->nonIgnorable()->build();
48+
}
49+
50+
return $errors;
51+
}
52+
53+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\NullsafeCheck;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<ReturnNullsafeByRefRule>
11+
*/
12+
class ReturnNullsafeByRefRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ReturnNullsafeByRefRule(new NullsafeCheck());
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (!self::$useStaticReflectionProvider) {
23+
$this->markTestSkipped('Test requires static reflection.');
24+
}
25+
26+
$this->analyse([__DIR__ . '/data/return-null-safe-by-ref.php'], [
27+
[
28+
'Nullsafe cannot be returned by reference.',
29+
15,
30+
],
31+
[
32+
'Nullsafe cannot be returned by reference.',
33+
25,
34+
],
35+
[
36+
'Nullsafe cannot be returned by reference.',
37+
36,
38+
],
39+
]);
40+
}
41+
42+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace ReturnNullsafeByRef;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(?\stdClass $foo)
9+
{
10+
function &() use ($foo) {
11+
if (rand(0, 1)) {
12+
return $foo;
13+
}
14+
15+
return $foo?->bar->foo;
16+
};
17+
}
18+
19+
public function &doBar()
20+
{
21+
if (rand(0, 1)) {
22+
return $foo;
23+
}
24+
25+
return $foo?->bar->foo;
26+
}
27+
28+
}
29+
30+
function &foo(): void
31+
{
32+
if (rand(0, 1)) {
33+
return $foo;
34+
}
35+
36+
return $foo?->bar->foo;
37+
}

0 commit comments

Comments
 (0)