9
9
use PHPStan \Analyser \Scope ;
10
10
use PHPStan \Reflection \ClassMemberReflection ;
11
11
use PHPStan \Rules \Rule ;
12
+ use PHPStan \Rules \RuleError ;
12
13
use PHPStan \Rules \RuleErrorBuilder ;
13
14
use Svnldwg \PHPStan \Helper \AnnotationParser ;
14
15
use Svnldwg \PHPStan \Helper \BackwardsIterator ;
@@ -28,6 +29,13 @@ class ImmutableObjectRule implements Rule
28
29
/** @var \PHPStan\Parser\Parser */
29
30
private $ parser ;
30
31
32
+ /** @var string */
33
+ private $ currentClass = '' ;
34
+ /** @var string[] */
35
+ private $ immutableProperties = [];
36
+ /** @var bool */
37
+ private $ isImmutable = false ;
38
+
31
39
public function __construct (\PHPStan \Parser \Parser $ parser )
32
40
{
33
41
$ this ->parser = $ parser ;
@@ -40,49 +48,42 @@ public function getNodeType(): string
40
48
41
49
public function processNode (Node $ node , Scope $ scope ): array
42
50
{
43
- if (!$ node instanceof Node \Expr \AssignOp && !$ node instanceof Assign) {
51
+ if (!$ node instanceof Node \Expr \AssignOp
52
+ && !$ node instanceof Assign
53
+ && !$ node instanceof Node \Stmt \Property
54
+ ) {
44
55
return [];
45
56
}
46
57
47
58
if (!$ scope ->isInClass ()) {
48
59
return [];
49
60
}
50
61
51
- ['properties ' => $ immutableProperties , 'hasImmutableParent ' => $ hasImmutableParent ] = $ this ->getInheritedImmutableProperties ($ scope );
52
-
53
- $ nodes = $ this ->parser ->parseFile ($ scope ->getFile ());
54
- $ hasImmutableClassAnnotation = AnnotationParser::classHasAnnotation (self ::WHITELISTED_ANNOTATIONS , $ nodes );
62
+ $ this ->detectImmutableProperties ($ scope );
55
63
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 );
62
- }
63
- }
64
-
65
- if (empty ($ immutableProperties )) {
66
- $ immutableProperties = AnnotationParser::propertiesWithWhitelistedAnnotations (self ::WHITELISTED_ANNOTATIONS , $ nodes );
64
+ $ isImmutable = $ this ->isImmutable ;
65
+ $ immutableProperties = $ this ->immutableProperties ;
66
+ if (!$ isImmutable && empty ($ immutableProperties )) {
67
+ return [];
67
68
}
68
69
69
- if (! $ hasImmutableParent && ! $ hasImmutableClassAnnotation && empty ( $ immutableProperties ) ) {
70
- return [] ;
70
+ if ($ node instanceof Node \ Stmt \Property ) {
71
+ return $ this -> assertImmutablePropertyIsNotPublic ( $ node ) ;
71
72
}
72
73
73
74
while ($ node ->var instanceof Node \Expr \ArrayDimFetch) {
74
75
$ node = $ node ->var ;
75
76
}
76
77
77
- if (!empty ($ immutableProperties )
78
+ if (!$ isImmutable
79
+ && !empty ($ immutableProperties )
78
80
&& property_exists ($ node ->var , 'name ' )
79
81
&& !in_array ((string )$ node ->var ->name , $ immutableProperties )
80
82
) {
81
83
return [];
82
84
}
83
85
84
- if (
85
- !$ node ->var instanceof Node \Expr \PropertyFetch
86
+ if (!$ node ->var instanceof Node \Expr \PropertyFetch
86
87
&& !$ node ->var instanceof Node \Expr \StaticPropertyFetch
87
88
) {
88
89
return [];
@@ -160,4 +161,69 @@ private function getInheritedImmutableProperties(Scope $scope): array
160
161
161
162
return ['properties ' => $ immutableParentProperties , 'hasImmutableParent ' => $ hasImmutableParent ];
162
163
}
164
+
165
+ /**
166
+ * @param Scope $scope
167
+ */
168
+ private function detectImmutableProperties (Scope $ scope ): void
169
+ {
170
+ $ classReflection = $ scope ->getClassReflection ();
171
+ if ($ classReflection === null ) {
172
+ $ this ->isImmutable = false ;
173
+ $ this ->immutableProperties = [];
174
+ $ this ->currentClass = '' ;
175
+
176
+ return ;
177
+ }
178
+ $ currentClassName = $ classReflection ->getName ();
179
+ if ($ this ->currentClass === $ currentClassName ) {
180
+ return ;
181
+ }
182
+
183
+ ['properties ' => $ immutableProperties , 'hasImmutableParent ' => $ hasImmutableParent ] = $ this ->getInheritedImmutableProperties ($ scope );
184
+
185
+ $ nodes = $ this ->parser ->parseFile ($ scope ->getFile ());
186
+ $ isImmutable = $ hasImmutableParent || AnnotationParser::classHasAnnotation (self ::WHITELISTED_ANNOTATIONS , $ nodes );
187
+
188
+ if (!empty ($ immutableProperties )) {
189
+ $ classNode = NodeParser::getClassNode ($ nodes );
190
+ if ($ classNode !== null ) {
191
+ $ classProperties = NodeParser::getClassProperties ($ classNode );
192
+ $ classPropertyNames = Converter::propertyStringNames ($ classProperties );
193
+ $ immutableProperties = array_merge ($ immutableProperties , $ classPropertyNames );
194
+ }
195
+ }
196
+
197
+ if (empty ($ immutableProperties )) {
198
+ $ immutableProperties = AnnotationParser::propertiesWithWhitelistedAnnotations (self ::WHITELISTED_ANNOTATIONS , $ nodes );
199
+ }
200
+
201
+ $ this ->immutableProperties = $ immutableProperties ;
202
+ $ this ->isImmutable = $ isImmutable ;
203
+ $ this ->currentClass = $ classReflection ->getName ();
204
+ }
205
+
206
+ /**
207
+ * @param Node\Stmt\Property $property
208
+ *
209
+ * @throws \PHPStan\ShouldNotHappenException
210
+ *
211
+ * @return RuleError[]
212
+ */
213
+ private function assertImmutablePropertyIsNotPublic (Node \Stmt \Property $ property ): array
214
+ {
215
+ $ propertyName = Converter::propertyToString ($ property );
216
+ $ propertyIsImmutable = $ this ->isImmutable || in_array ($ propertyName , $ this ->immutableProperties );
217
+
218
+ if ($ propertyIsImmutable && $ property ->isPublic ()) {
219
+ return [
220
+ RuleErrorBuilder::message (sprintf (
221
+ 'Property "%s" is declared immutable, but is public and therefore mutable ' ,
222
+ $ propertyName
223
+ ))->build (),
224
+ ];
225
+ }
226
+
227
+ return [];
228
+ }
163
229
}
0 commit comments