Skip to content

Commit 958a5fc

Browse files
committed
implements basic immutability inheritance
1 parent f607111 commit 958a5fc

File tree

4 files changed

+76
-12
lines changed

4 files changed

+76
-12
lines changed

src/Helper/AnnotationParser.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,8 @@ public static function propertiesWithWhitelistedAnnotations(array $annotations,
4040
return $whitelistedProperties;
4141
}
4242

43-
foreach ($classNode->stmts as $property) {
44-
if (!$property instanceof Node\Stmt\Property) {
45-
continue;
46-
}
47-
43+
$classProperties = NodeParser::getClassProperties($classNode);
44+
foreach ($classProperties as $property) {
4845
$whitelisted = self::isWhitelisted($property, $annotations);
4946
if ($whitelisted) {
5047
foreach ($property->props as $prop) {
@@ -58,6 +55,7 @@ public static function propertiesWithWhitelistedAnnotations(array $annotations,
5855

5956
/**
6057
* @param Node $node
58+
*
6159
* @return string[]
6260
*/
6361
public static function getAnnotations(Node $node): array

src/Helper/NodeParser.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,31 @@ public static function getClassNode(array $nodes): ?Node\Stmt\Class_
4040

4141
return null;
4242
}
43+
44+
/**
45+
* @param Node\Stmt\Class_ $classNode
46+
*
47+
* @return Node\Stmt\Property[]
48+
*/
49+
public static function getClassProperties(Node\Stmt\Class_ $classNode): array
50+
{
51+
$properties = [];
52+
53+
foreach ($classNode->stmts as $property) {
54+
if ($property instanceof Node\Stmt\Property) {
55+
$properties[] = $property;
56+
}
57+
}
58+
59+
return $properties;
60+
}
61+
62+
public static function getNonPrivateProperties(Node\Stmt\Class_ $classNode): array
63+
{
64+
$properties = self::getClassProperties($classNode);
65+
66+
return array_filter($properties, static function (Node\Stmt\Property $property): bool {
67+
return !$property->isPrivate();
68+
});
69+
}
4370
}

src/Rules/ImmutableObjectRule.php

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use Svnldwg\PHPStan\Helper\AnnotationParser;
14+
use Svnldwg\PHPStan\Helper\NodeParser;
1415

1516
/**
1617
* @template-implements Rule<Node>
@@ -45,11 +46,15 @@ public function processNode(Node $node, Scope $scope): array
4546
return [];
4647
}
4748

49+
$immutableProperties = $this->getParentClasses($scope);
50+
4851
$nodes = $this->parser->parseFile($scope->getFile());
4952

50-
$immutableProperties = null;
5153
if (!AnnotationParser::classHasAnnotation(self::WHITELISTED_ANNOTATIONS, $nodes)) {
52-
$immutableProperties = AnnotationParser::propertiesWithWhitelistedAnnotations(self::WHITELISTED_ANNOTATIONS, $nodes);
54+
$immutableProperties = array_merge(
55+
$immutableProperties,
56+
AnnotationParser::propertiesWithWhitelistedAnnotations(self::WHITELISTED_ANNOTATIONS, $nodes)
57+
);
5358

5459
if (empty($immutableProperties)) {
5560
return [];
@@ -106,4 +111,38 @@ public function processNode(Node $node, Scope $scope): array
106111
))->build(),
107112
];
108113
}
114+
115+
private function getParentClasses(Scope $scope): array
116+
{
117+
if ($scope->getClassReflection() === null) {
118+
return [];
119+
}
120+
121+
$immutableParentProperties = [];
122+
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) {
126+
$fileName = $parent->getFileName();
127+
if (!$fileName) {
128+
continue;
129+
}
130+
131+
$nodes = $this->parser->parseFile($fileName);
132+
$classNode = NodeParser::getClassNode($nodes);
133+
if (!$classNode) {
134+
continue;
135+
}
136+
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));
141+
}
142+
143+
// @TODO: detect non private parent properties annotated as immutable
144+
}
145+
146+
return $immutableParentProperties;
147+
}
109148
}

test/Integration/Rules/ImmutableObjectRuleTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ public function provideCasesWhereAnalysisShouldFail(): iterable
5757
22,
5858
],
5959
],
60-
/*'mutation-in-child-class' => [
61-
__DIR__ . '/../../Fixture/ImmutableObjectRule/Failure/MutationInChildClass.php',
60+
'mutation-in-child-class' => [
61+
__DIR__ . '/../../Fixture/ImmutableObjectRule/Failure/MutationInChildClassChild.php',
6262
[
63-
'Class is declared immutable, but class property "value" is modified in method "setValue"',
64-
26,
63+
'Property is declared immutable, but class property "value" is modified in method "setValue"',
64+
11,
6565
],
66-
],*/
66+
],
6767
];
6868

6969
foreach ($paths as $description => [$path, $error]) {

0 commit comments

Comments
 (0)