99use PHPStan \Analyser \Scope ;
1010use PHPStan \Reflection \ClassMemberReflection ;
1111use PHPStan \Rules \Rule ;
12+ use PHPStan \Rules \RuleError ;
1213use PHPStan \Rules \RuleErrorBuilder ;
1314use Svnldwg \PHPStan \Helper \AnnotationParser ;
1415use Svnldwg \PHPStan \Helper \BackwardsIterator ;
@@ -28,6 +29,13 @@ class ImmutableObjectRule implements Rule
2829 /** @var \PHPStan\Parser\Parser */
2930 private $ parser ;
3031
32+ /** @var string */
33+ private $ currentClass = '' ;
34+ /** @var string[] */
35+ private $ immutableProperties = [];
36+ /** @var bool */
37+ private $ isImmutable = false ;
38+
3139 public function __construct (\PHPStan \Parser \Parser $ parser )
3240 {
3341 $ this ->parser = $ parser ;
@@ -40,49 +48,42 @@ public function getNodeType(): string
4048
4149 public function processNode (Node $ node , Scope $ scope ): array
4250 {
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+ ) {
4455 return [];
4556 }
4657
4758 if (!$ scope ->isInClass ()) {
4859 return [];
4960 }
5061
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 );
5563
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 [];
6768 }
6869
69- if (! $ hasImmutableParent && ! $ hasImmutableClassAnnotation && empty ( $ immutableProperties ) ) {
70- return [] ;
70+ if ($ node instanceof Node \ Stmt \Property ) {
71+ return $ this -> assertImmutablePropertyIsNotPublic ( $ node ) ;
7172 }
7273
7374 while ($ node ->var instanceof Node \Expr \ArrayDimFetch) {
7475 $ node = $ node ->var ;
7576 }
7677
77- if (!empty ($ immutableProperties )
78+ if (!$ isImmutable
79+ && !empty ($ immutableProperties )
7880 && property_exists ($ node ->var , 'name ' )
7981 && !in_array ((string )$ node ->var ->name , $ immutableProperties )
8082 ) {
8183 return [];
8284 }
8385
84- if (
85- !$ node ->var instanceof Node \Expr \PropertyFetch
86+ if (!$ node ->var instanceof Node \Expr \PropertyFetch
8687 && !$ node ->var instanceof Node \Expr \StaticPropertyFetch
8788 ) {
8889 return [];
@@ -160,4 +161,69 @@ private function getInheritedImmutableProperties(Scope $scope): array
160161
161162 return ['properties ' => $ immutableParentProperties , 'hasImmutableParent ' => $ hasImmutableParent ];
162163 }
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+ }
163229}
0 commit comments