Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rule about readonly properties in constructor
- Loading branch information
1 parent
28bd563
commit 44fd938
Showing
5 changed files
with
312 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Properties; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Node\ClassPropertiesNode; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\ShouldNotHappenException; | ||
use ReflectionException; | ||
use function array_key_exists; | ||
use function explode; | ||
use function sprintf; | ||
|
||
/** | ||
* @implements Rule<ClassPropertiesNode> | ||
*/ | ||
class MissingReadOnlyPropertyAssignRule implements Rule | ||
{ | ||
|
||
/** @var array<string, string[]> */ | ||
private array $additionalConstructorsCache = []; | ||
|
||
/** | ||
* @param string[] $additionalConstructors | ||
*/ | ||
public function __construct( | ||
private array $additionalConstructors, | ||
) | ||
{ | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return ClassPropertiesNode::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$scope->isInClass()) { | ||
throw new ShouldNotHappenException(); | ||
} | ||
$classReflection = $scope->getClassReflection(); | ||
[$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->getConstructors($classReflection), []); | ||
|
||
$errors = []; | ||
foreach ($properties as $propertyName => $propertyNode) { | ||
if (!$propertyNode->isReadOnly()) { | ||
continue; | ||
} | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'Class %s has an uninitialized readonly property $%s. Assign it in the constructor.', | ||
$classReflection->getDisplayName(), | ||
$propertyName, | ||
))->line($propertyNode->getLine())->build(); | ||
} | ||
|
||
foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { | ||
if (!$propertyNode->isReadOnly()) { | ||
continue; | ||
} | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'Access to an uninitialized readonly property %s::$%s.', | ||
$classReflection->getDisplayName(), | ||
$propertyName, | ||
))->line($line)->build(); | ||
} | ||
|
||
foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { | ||
if (!$propertyNode->isReadOnly()) { | ||
continue; | ||
} | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'Readonly property %s::$%s is already assigned.', | ||
$classReflection->getDisplayName(), | ||
$propertyName, | ||
))->line($line)->build(); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
/** | ||
* @return string[] | ||
*/ | ||
private function getConstructors(ClassReflection $classReflection): array | ||
{ | ||
if (array_key_exists($classReflection->getName(), $this->additionalConstructorsCache)) { | ||
return $this->additionalConstructorsCache[$classReflection->getName()]; | ||
} | ||
$constructors = []; | ||
if ($classReflection->hasConstructor()) { | ||
$constructors[] = $classReflection->getConstructor()->getName(); | ||
} | ||
|
||
$nativeReflection = $classReflection->getNativeReflection(); | ||
foreach ($this->additionalConstructors as $additionalConstructor) { | ||
[$className, $methodName] = explode('::', $additionalConstructor); | ||
if (!$nativeReflection->hasMethod($methodName)) { | ||
continue; | ||
} | ||
$nativeMethod = $nativeReflection->getMethod($methodName); | ||
if ($nativeMethod->getDeclaringClass()->getName() !== $nativeReflection->getName()) { | ||
continue; | ||
} | ||
|
||
try { | ||
$prototype = $nativeMethod->getPrototype(); | ||
} catch (ReflectionException) { | ||
$prototype = $nativeMethod; | ||
} | ||
|
||
if ($prototype->getDeclaringClass()->getName() !== $className) { | ||
continue; | ||
} | ||
|
||
$constructors[] = $methodName; | ||
} | ||
|
||
$this->additionalConstructorsCache[$classReflection->getName()] = $constructors; | ||
|
||
return $constructors; | ||
} | ||
|
||
} |
60 changes: 60 additions & 0 deletions
60
tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Properties; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
use const PHP_VERSION_ID; | ||
|
||
/** | ||
* @extends RuleTestCase<MissingReadOnlyPropertyAssignRule> | ||
*/ | ||
class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new MissingReadOnlyPropertyAssignRule([ | ||
'MissingReadOnlyPropertyAssign\\TestCase::setUp', | ||
]); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
if (PHP_VERSION_ID < 80100) { | ||
$this->markTestSkipped('Test requires PHP 8.1.'); | ||
} | ||
|
||
$this->analyse([__DIR__ . '/data/missing-readonly-property-assign.php'], [ | ||
[ | ||
'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned. Assign it in the constructor.', | ||
14, | ||
], | ||
[ | ||
'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned2. Assign it in the constructor.', | ||
16, | ||
], | ||
[ | ||
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\Foo::$readBeforeAssigned.', | ||
33, | ||
], | ||
[ | ||
'Readonly property MissingReadOnlyPropertyAssign\Foo::$doubleAssigned is already assigned.', | ||
37, | ||
], | ||
[ | ||
'Class MissingReadOnlyPropertyAssign\BarDoubleAssignInSetter has an uninitialized readonly property $foo. Assign it in the constructor.', | ||
53, | ||
], | ||
[ | ||
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$foo.', | ||
85, | ||
], | ||
[ | ||
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$bar.', | ||
87, | ||
], | ||
]); | ||
} | ||
|
||
} |
103 changes: 103 additions & 0 deletions
103
tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?php // lint >= 8.1 | ||
|
||
namespace MissingReadOnlyPropertyAssign; | ||
|
||
class Foo | ||
{ | ||
|
||
private readonly int $assigned; | ||
|
||
private int $unassignedButNotReadOnly; | ||
|
||
private int $readBeforeAssignedNotReadOnly; | ||
|
||
private readonly int $unassigned; | ||
|
||
private readonly int $unassigned2; | ||
|
||
private readonly int $readBeforeAssigned; | ||
|
||
private readonly int $doubleAssigned; | ||
|
||
private int $doubleAssignedNotReadOnly; | ||
|
||
public function __construct( | ||
private readonly int $promoted, | ||
) | ||
{ | ||
$this->assigned = 1; | ||
|
||
echo $this->readBeforeAssignedNotReadOnly; | ||
$this->readBeforeAssignedNotReadOnly = 1; | ||
|
||
echo $this->readBeforeAssigned; | ||
$this->readBeforeAssigned = 1; | ||
|
||
$this->doubleAssigned = 1; | ||
$this->doubleAssigned = 2; | ||
|
||
$this->doubleAssignedNotReadOnly = 1; | ||
$this->doubleAssignedNotReadOnly = 2; | ||
} | ||
|
||
public function setUnassigned2(int $i): void | ||
{ | ||
$this->unassigned2 = $i; | ||
} | ||
|
||
} | ||
|
||
class BarDoubleAssignInSetter | ||
{ | ||
|
||
private readonly int $foo; | ||
|
||
public function setFoo(int $i) | ||
{ | ||
// reported in ReadOnlyPropertyAssignRule | ||
$this->foo = $i; | ||
$this->foo = $i; | ||
} | ||
|
||
} | ||
|
||
class TestCase | ||
{ | ||
|
||
private readonly int $foo; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->foo = 1; | ||
} | ||
|
||
} | ||
|
||
class AssignOp | ||
{ | ||
|
||
private readonly int $foo; | ||
|
||
private readonly ?int $bar; | ||
|
||
public function __construct(int $foo) | ||
{ | ||
$this->foo .= $foo; | ||
|
||
$this->bar ??= 3; | ||
} | ||
|
||
|
||
} | ||
|
||
class AssignRef | ||
{ | ||
|
||
private readonly int $foo; | ||
|
||
public function __construct(int $foo) | ||
{ | ||
$this->foo = &$foo; | ||
} | ||
|
||
} |