Skip to content

Commit 691ef12

Browse files
committed
feat: add optional rule to report properties that should be promoted
1 parent 5878035 commit 691ef12

File tree

5 files changed

+177
-0
lines changed

5 files changed

+177
-0
lines changed

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ parameters:
6969
reportPossiblyNonexistentGeneralArrayOffset: false
7070
reportPossiblyNonexistentConstantArrayOffset: false
7171
checkMissingOverrideMethodAttribute: false
72+
reportPropertiesThatShouldBePromoted: false
7273
mixinExcludeClasses: []
7374
scanFiles: []
7475
scanDirectories: []

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ parametersSchema:
7979
reportPossiblyNonexistentGeneralArrayOffset: bool()
8080
reportPossiblyNonexistentConstantArrayOffset: bool()
8181
checkMissingOverrideMethodAttribute: bool()
82+
reportPropertiesThatShouldBePromoted: bool()
8283
parallel: structure([
8384
jobSize: int(),
8485
processTimeout: float(),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use Override;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\Assign;
9+
use PhpParser\Node\Expr\Error;
10+
use PhpParser\Node\Expr\PropertyFetch;
11+
use PhpParser\Node\Expr\Variable;
12+
use PhpParser\Node\Identifier;
13+
use PhpParser\Node\Stmt\ClassMethod;
14+
use PhpParser\Node\Stmt\Expression;
15+
use PHPStan\Analyser\Scope;
16+
use PHPStan\DependencyInjection\AutowiredParameter;
17+
use PHPStan\DependencyInjection\RegisteredRule;
18+
use PHPStan\Rules\Rule;
19+
use PHPStan\Rules\RuleErrorBuilder;
20+
use function sprintf;
21+
22+
/**
23+
* @implements Rule<ClassMethod>
24+
*/
25+
#[RegisteredRule(level: 0)]
26+
final class ReportPropertiesThatShouldBePromotedRule implements Rule
27+
{
28+
29+
public function __construct(
30+
#[AutowiredParameter]
31+
private bool $reportPropertiesThatShouldBePromoted,
32+
)
33+
{
34+
}
35+
36+
public function getNodeType(): string
37+
{
38+
return ClassMethod::class;
39+
}
40+
41+
#[Override]
42+
public function processNode(Node $node, Scope $scope): array
43+
{
44+
if (
45+
! $this->reportPropertiesThatShouldBePromoted
46+
|| $node->name->toLowerString() !== '__construct'
47+
|| $node->params === []
48+
) {
49+
return [];
50+
}
51+
$errors = [];
52+
foreach ($node->params as $param) {
53+
if (
54+
$param->isPromoted()
55+
|| $param->var instanceof Error
56+
|| $param->var->name instanceof Expr
57+
|| ! $this->assignsUnmodifiedVariableToProperty($param->var->name, $node)
58+
) {
59+
continue;
60+
}
61+
$errors[] = RuleErrorBuilder::message(sprintf('Property [%s] should be promoted.', $param->var->name))
62+
->identifier('property.shouldBePromoted')
63+
->line($param->var->getStartLine())
64+
->build();
65+
}
66+
return $errors;
67+
}
68+
69+
private function assignsUnmodifiedVariableToProperty(string $variable, ClassMethod $node): bool
70+
{
71+
foreach ($node->getStmts() as $stmt) {
72+
if (! $stmt instanceof Expression || ! $stmt->expr instanceof Assign) {
73+
continue;
74+
}
75+
$var = $stmt->expr->var;
76+
$expr = $stmt->expr->expr;
77+
if (! $var instanceof Variable && ! $var instanceof PropertyFetch) {
78+
continue;
79+
}
80+
if ($var instanceof Variable) {
81+
// The variable has been modified, so can't promote it.
82+
if ($var->name === $variable) {
83+
return false;
84+
}
85+
continue;
86+
}
87+
if (
88+
! $var->var instanceof Variable
89+
|| $var->var->name !== 'this'
90+
|| ! $var->name instanceof Identifier
91+
|| $var->name->toString() !== $variable
92+
) {
93+
continue;
94+
}
95+
if (! $expr instanceof Variable) {
96+
continue;
97+
}
98+
// The variable is being assigned to a property
99+
// of the same name, safe to promote it.
100+
if ($expr->name === $variable) {
101+
return true;
102+
}
103+
}
104+
return false;
105+
}
106+
107+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\Properties\ReportPropertiesThatShouldBePromoted;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use function sprintf;
9+
10+
/**
11+
* @extends RuleTestCase<ReportPropertiesThatShouldBePromoted>
12+
*/
13+
class ReportPropertiesThatShouldBePromotedRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new ReportPropertiesThatShouldBePromoted(true);
19+
}
20+
21+
public function testOverridingFinalMethod(): void
22+
{
23+
$error = static fn (string $property) => sprintf('Property [%s] should be promoted.', $property);
24+
25+
$this->analyse([__DIR__ . '/data/properties-that-should-be-promoted.php'], [
26+
[$error('name'), 20],
27+
]);
28+
}
29+
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace PropertiesThatShouldBePromoted;
4+
5+
class PromotedProperties
6+
{
7+
public string $name;
8+
9+
/** @var list<string> */
10+
public array $tags;
11+
12+
/** @var array<string, mixed> */
13+
public array $options;
14+
15+
public int $count;
16+
17+
public int $foo;
18+
19+
public function __construct(
20+
int $count,
21+
?string $name,
22+
public ?string $email,
23+
/** @var array<int, string> */
24+
array $tags,
25+
/** @var array<string, mixed> */
26+
array $options,
27+
int $bar,
28+
) {
29+
$this->count = $count;
30+
31+
$tags = array_values($tags);
32+
$this->tags = $tags;
33+
$this->options = array_filter($options);
34+
$this->email ??= '[email protected]';
35+
$this->name = $name ?? 'Default Name';
36+
$this->foo = $bar;
37+
}
38+
}

0 commit comments

Comments
 (0)