Skip to content

Commit 78f2f0e

Browse files
committed
#2 handles immutability with multiple inheritance, handles inherited immutability
1 parent 958a5fc commit 78f2f0e

File tree

13 files changed

+190
-22
lines changed

13 files changed

+190
-22
lines changed

src/Helper/BackwardsIterator.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Helper;
6+
7+
class BackwardsIterator
8+
{
9+
/**
10+
* @param mixed[] $array
11+
*
12+
* @return \Generator<mixed>
13+
*/
14+
public static function iterateBackwards(array $array): \Generator
15+
{
16+
for (end($array); key($array) !== null; prev($array)) {
17+
yield current($array);
18+
}
19+
}
20+
}

src/Helper/Converter.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Helper;
6+
7+
use PhpParser\Node;
8+
9+
class Converter
10+
{
11+
/**
12+
* @param array<Node\Stmt\Property> $properties
13+
*
14+
* @return string[]
15+
*/
16+
public static function propertyStringNames(array $properties)
17+
{
18+
return array_map(static function (Node\Stmt\Property $property): string {
19+
return (string)reset($property->props)->name;
20+
}, $properties);
21+
}
22+
}

src/Rules/ImmutableObjectRule.php

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use Svnldwg\PHPStan\Helper\AnnotationParser;
14+
use Svnldwg\PHPStan\Helper\BackwardsIterator;
15+
use Svnldwg\PHPStan\Helper\Converter;
1416
use Svnldwg\PHPStan\Helper\NodeParser;
1517

1618
/**
@@ -46,21 +48,28 @@ public function processNode(Node $node, Scope $scope): array
4648
return [];
4749
}
4850

49-
$immutableProperties = $this->getParentClasses($scope);
51+
[$immutableProperties, $hasImmutableParent] = $this->getInheritedImmutableProperties($scope);
5052

5153
$nodes = $this->parser->parseFile($scope->getFile());
54+
$hasImmutableClassAnnotation = AnnotationParser::classHasAnnotation(self::WHITELISTED_ANNOTATIONS, $nodes);
5255

53-
if (!AnnotationParser::classHasAnnotation(self::WHITELISTED_ANNOTATIONS, $nodes)) {
54-
$immutableProperties = array_merge(
55-
$immutableProperties,
56-
AnnotationParser::propertiesWithWhitelistedAnnotations(self::WHITELISTED_ANNOTATIONS, $nodes)
57-
);
58-
59-
if (empty($immutableProperties)) {
60-
return [];
56+
if (!empty($immutableProperties)) {
57+
$classNode = NodeParser::getClassNode($nodes);
58+
if ($classNode !== null) {
59+
$classProperties = NodeParser::getClassProperties($classNode);
60+
$classPropertyNames = Converter::propertyStringNames($classProperties);
61+
$immutableProperties = array_merge($immutableProperties, $classPropertyNames);
6162
}
6263
}
6364

65+
if (empty($immutableProperties)) {
66+
$immutableProperties = AnnotationParser::propertiesWithWhitelistedAnnotations(self::WHITELISTED_ANNOTATIONS, $nodes);
67+
}
68+
69+
if (!$hasImmutableParent && !$hasImmutableClassAnnotation && empty($immutableProperties)) {
70+
return [];
71+
}
72+
6473
while ($node->var instanceof Node\Expr\ArrayDimFetch) {
6574
$node = $node->var;
6675
}
@@ -112,17 +121,23 @@ public function processNode(Node $node, Scope $scope): array
112121
];
113122
}
114123

115-
private function getParentClasses(Scope $scope): array
124+
/**
125+
* @param Scope $scope
126+
*
127+
* @return array<string[]>
128+
*/
129+
private function getInheritedImmutableProperties(Scope $scope): array
116130
{
117131
if ($scope->getClassReflection() === null) {
118132
return [];
119133
}
120134

121135
$immutableParentProperties = [];
122136

123-
// TODO: consider multiple layers of inheritance (parent 1 is not declared immutable, but parent 2 is, so properties of parent 1 need to inherit immutability
124-
125-
foreach ($scope->getClassReflection()->getParents() as $parent) {
137+
$parents = $scope->getClassReflection()->getParents();
138+
$parentsTopDown = BackwardsIterator::iterateBackwards($parents);
139+
$hasImmutableParent = false;
140+
foreach ($parentsTopDown as $parent) {
126141
$fileName = $parent->getFileName();
127142
if (!$fileName) {
128143
continue;
@@ -134,15 +149,15 @@ private function getParentClasses(Scope $scope): array
134149
continue;
135150
}
136151

137-
if (AnnotationParser::classHasAnnotation(self::WHITELISTED_ANNOTATIONS, $nodes)) {
138-
$immutableParentProperties += array_map(static function (Node\Stmt\Property $property): string {
139-
return (string)reset($property->props)->name;
140-
}, NodeParser::getNonPrivateProperties($classNode));
152+
if ($hasImmutableParent || AnnotationParser::classHasAnnotation(self::WHITELISTED_ANNOTATIONS, $nodes)) {
153+
$hasImmutableParent = true;
154+
155+
$immutableParentProperties += Converter::propertyStringNames(NodeParser::getNonPrivateProperties($classNode));
141156
}
142157

143-
// @TODO: detect non private parent properties annotated as immutable
158+
// @TODO: detect non private parent properties annotated as immutable (instead of whole class)
144159
}
145160

146-
return $immutableParentProperties;
161+
return [$immutableParentProperties, $hasImmutableParent];
147162
}
148163
}

test/Fixture/ImmutableObjectRule/Failure/MutationInChildClass.php renamed to test/Fixture/ImmutableObjectRule/Failure/Inheritance/Basic/MutationInChildClass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure;
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\Basic;
66

77
/**
88
* @immutable

test/Fixture/ImmutableObjectRule/Failure/MutationInChildClassChild.php renamed to test/Fixture/ImmutableObjectRule/Failure/Inheritance/Basic/MutationInChildClassChild.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure;
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\Basic;
66

77
class MutationInChildClassChild extends MutationInChildClass
88
{
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\Deep;
6+
7+
class ChildClass extends ParentClass
8+
{
9+
public function mutate(): void
10+
{
11+
$this->parentValue = 10;
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\Deep;
6+
7+
/**
8+
* @immutable
9+
*/
10+
class GrandParentClass
11+
{
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\Deep;
6+
7+
class ParentClass extends GrandParentClass
8+
{
9+
protected $parentValue;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\MutateChildPropertyWithImmutableParent;
6+
7+
class ChildPropertyMutation extends ImmutableParent
8+
{
9+
private $childProperty;
10+
11+
public function setChildProperty()
12+
{
13+
$this->childProperty = true;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Svnldwg\PHPStan\Test\Fixture\ImmutableObjectRule\Failure\Inheritance\MutateChildPropertyWithImmutableParent;
6+
7+
class ChildPropertyMutation2 extends ImmutableParentWithProperty
8+
{
9+
private $immu;
10+
11+
public function set()
12+
{
13+
$this->immu = 'mutated';
14+
}
15+
}

0 commit comments

Comments
 (0)