diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php index 2e6b2dc54..97a9bbe72 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php @@ -683,29 +683,13 @@ class PropertyAssignmentAnalyzer )) { // fall through } - } elseif ($class_storage->mutation_free - && !$assignment_value_type->reference_free - ) { - foreach ($assignment_value_type->getAtomicTypes() as $atomic_arg_type) { - if ($atomic_arg_type instanceof Type\Atomic\TNamedObject) { - $object_storage = $codebase->classlike_storage_provider->get( - $atomic_arg_type->value - ); + } elseif ($class_storage->mutation_free) { + $visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor( + $statements_analyzer, + $stmt + ); - if (!$object_storage->mutation_free) { - if (IssueBuffer::accepts( - new ImpurePropertyAssignment( - 'Cannot store a reference to an externally-mutable object' - . ' inside an immutable object – consider using __clone', - new CodeLocation($statements_analyzer->getSource(), $stmt) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } - } - } - } + $visitor->traverse($assignment_value_type); } } } diff --git a/src/Psalm/Internal/TypeVisitor/ImmutablePropertyAssignmentVisitor.php b/src/Psalm/Internal/TypeVisitor/ImmutablePropertyAssignmentVisitor.php new file mode 100644 index 000000000..5a2c1ef48 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ImmutablePropertyAssignmentVisitor.php @@ -0,0 +1,56 @@ +statements_analyzer = $statements_analyzer; + $this->stmt = $stmt; + } + + public function enterNode(TypeNode $type) : ?int + { + if ($type instanceof Union && $type->reference_free) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if ($type instanceof TNamedObject) { + $codebase = $this->statements_analyzer->getCodebase(); + + $object_storage = $codebase->classlike_storage_provider->get( + $type->value + ); + + if (!$object_storage->mutation_free) { + if (IssueBuffer::accepts( + new ImpurePropertyAssignment( + 'Cannot store a reference to an externally-mutable object' + . ' inside an immutable object – consider using __clone', + new CodeLocation($this->statements_analyzer, $this->stmt) + ), + $this->statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } + } + + return null; + } +} diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php index dff7d92de..4f806334c 100644 --- a/tests/ImmutableAnnotationTest.php +++ b/tests/ImmutableAnnotationTest.php @@ -610,6 +610,29 @@ class ImmutableAnnotationTest extends TestCase final class NotReallyImmutableClass extends MutableParent {}', 'error_message' => 'MutableDependency' ], + 'preventAssigningArrayToImmutableProperty' => [ + 'items = $items; + } + }', + 'error_message' => 'ImpurePropertyAssignment', + ], ]; } }