mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Detect assigning collections of mutable objects in immutable
Fixes #2946
This commit is contained in:
parent
43189b3d55
commit
2c0cf4030f
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Psalm\Internal\TypeVisitor;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Issue\ImpurePropertyAssignment;
|
||||
use Psalm\Type\NodeVisitor;
|
||||
use Psalm\Type\Union;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\TypeNode;
|
||||
|
||||
class ImmutablePropertyAssignmentVisitor extends NodeVisitor
|
||||
{
|
||||
private $statements_analyzer;
|
||||
private $stmt;
|
||||
|
||||
public function __construct(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\PropertyFetch $stmt
|
||||
) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -610,6 +610,29 @@ class ImmutableAnnotationTest extends TestCase
|
||||
final class NotReallyImmutableClass extends MutableParent {}',
|
||||
'error_message' => 'MutableDependency'
|
||||
],
|
||||
'preventAssigningArrayToImmutableProperty' => [
|
||||
'<?php
|
||||
class Item {}
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class Immutable {
|
||||
/**
|
||||
* @var Item[]
|
||||
*/
|
||||
private $items;
|
||||
|
||||
/**
|
||||
* @param Item[] $items
|
||||
*/
|
||||
public function __construct(array $items)
|
||||
{
|
||||
$this->items = $items;
|
||||
}
|
||||
}',
|
||||
'error_message' => 'ImpurePropertyAssignment',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user