Skip to content

Commit fc33730

Browse files
committed
SetNonVirtualPropertyHookAssignRule - level 3
1 parent 7fe9bc2 commit fc33730

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
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\SetNonVirtualPropertyHookAssignRule
2324
- PHPStan\Rules\Properties\ShortGetPropertyHookReturnTypeRule
2425
- PHPStan\Rules\Properties\TypesAssignedToPropertiesRule
2526
- PHPStan\Rules\Variables\ParameterOutAssignedTypeRule
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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\Expr\PropertyInitializationExpr;
8+
use PHPStan\Node\PropertyHookReturnStatementsNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\ShouldNotHappenException;
12+
use PHPStan\Type\NeverType;
13+
use function sprintf;
14+
15+
/**
16+
* @implements Rule<PropertyHookReturnStatementsNode>
17+
*/
18+
final class SetNonVirtualPropertyHookAssignRule implements Rule
19+
{
20+
21+
public function getNodeType(): string
22+
{
23+
return PropertyHookReturnStatementsNode::class;
24+
}
25+
26+
public function processNode(Node $node, Scope $scope): array
27+
{
28+
$hookNode = $node->getPropertyHookNode();
29+
if ($hookNode->name->toLowerString() !== 'set') {
30+
return [];
31+
}
32+
33+
$hookReflection = $node->getHookReflection();
34+
if (!$hookReflection->isPropertyHook()) {
35+
throw new ShouldNotHappenException();
36+
}
37+
38+
$propertyName = $hookReflection->getHookedPropertyName();
39+
$classReflection = $node->getClassReflection();
40+
if (!$classReflection->hasNativeProperty($propertyName)) {
41+
throw new ShouldNotHappenException();
42+
}
43+
44+
$propertyReflection = $classReflection->getNativeProperty($propertyName);
45+
if ($propertyReflection->isVirtual()->yes()) {
46+
return [];
47+
}
48+
49+
$finalHookScope = null;
50+
foreach ($node->getExecutionEnds() as $executionEnd) {
51+
$statementResult = $executionEnd->getStatementResult();
52+
$endNode = $executionEnd->getNode();
53+
if ($statementResult->isAlwaysTerminating()) {
54+
if ($endNode instanceof Node\Stmt\Expression) {
55+
$exprType = $statementResult->getScope()->getType($endNode->expr);
56+
if ($exprType instanceof NeverType && $exprType->isExplicit()) {
57+
continue;
58+
}
59+
}
60+
}
61+
if ($finalHookScope === null) {
62+
$finalHookScope = $statementResult->getScope();
63+
continue;
64+
}
65+
66+
$finalHookScope = $finalHookScope->mergeWith($statementResult->getScope());
67+
}
68+
69+
foreach ($node->getReturnStatements() as $returnStatement) {
70+
if ($finalHookScope === null) {
71+
$finalHookScope = $returnStatement->getScope();
72+
continue;
73+
}
74+
$finalHookScope = $finalHookScope->mergeWith($returnStatement->getScope());
75+
}
76+
77+
if ($finalHookScope === null) {
78+
return [];
79+
}
80+
81+
$initExpr = new PropertyInitializationExpr($propertyName);
82+
$hasInit = $finalHookScope->hasExpressionType($initExpr);
83+
if ($hasInit->yes()) {
84+
return [];
85+
}
86+
87+
return [
88+
RuleErrorBuilder::message(sprintf(
89+
'Set hook for non-virtual property %s::$%s does not %sassign value to it.',
90+
$classReflection->getDisplayName(),
91+
$propertyName,
92+
$hasInit->maybe() ? 'always ' : '',
93+
))->identifier('propertySetHook.noAssign')->build(),
94+
];
95+
}
96+
97+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<SetNonVirtualPropertyHookAssignRule>
11+
*/
12+
class SetNonVirtualPropertyHookAssignRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new SetNonVirtualPropertyHookAssignRule();
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (PHP_VERSION_ID < 80400) {
23+
$this->markTestSkipped('Test requires PHP 8.4.');
24+
}
25+
26+
$this->analyse([__DIR__ . '/data/set-non-virtual-property-hook-assign.php'], [
27+
[
28+
'Set hook for non-virtual property SetNonVirtualPropertyHookAssign\Foo::$k does not assign value to it.',
29+
24,
30+
],
31+
[
32+
'Set hook for non-virtual property SetNonVirtualPropertyHookAssign\Foo::$k2 does not always assign value to it.',
33+
34,
34+
],
35+
]);
36+
}
37+
38+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php // lint >= 8.4
2+
3+
namespace SetNonVirtualPropertyHookAssign;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
get {
10+
return 1;
11+
}
12+
set {
13+
// virtual property
14+
$this->j = $value;
15+
}
16+
}
17+
18+
public int $j;
19+
20+
public int $k {
21+
get {
22+
return $this->k + 1;
23+
}
24+
set {
25+
// backed property, missing assign should be reported
26+
$this->j = $value;
27+
}
28+
}
29+
30+
public int $k2 {
31+
get {
32+
return $this->k2 + 1;
33+
}
34+
set {
35+
// backed property, missing assign should be reported
36+
if (rand(0, 1)) {
37+
return;
38+
}
39+
40+
$this->k2 = $value;
41+
}
42+
}
43+
44+
public int $k3 {
45+
get {
46+
return $this->k3 + 1;
47+
}
48+
set {
49+
// backed property, always assigned (or throws)
50+
if (rand(0, 1)) {
51+
throw new \Exception();
52+
}
53+
54+
$this->k3 = $value;
55+
}
56+
}
57+
58+
public int $k4 {
59+
get {
60+
return $this->k4 + 1;
61+
}
62+
set {
63+
// backed property, always assigned
64+
$this->k4 = $value;
65+
}
66+
}
67+
68+
}

0 commit comments

Comments
 (0)