1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Allow immutable objects to be cloned

Fixes #2111
This commit is contained in:
Brown 2019-09-09 11:14:40 -04:00
parent 21aa162d0a
commit b49444b8ad
5 changed files with 123 additions and 15 deletions

View File

@ -178,6 +178,13 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
} elseif ($context->self) {
$context->vars_in_scope['$this'] = new Type\Union([new TNamedObject($context->self)]);
if ($storage->external_mutation_free
&& !$storage->mutation_free_inferred
) {
$context->vars_in_scope['$this']->external_mutation_free = true;
}
$context->vars_possibly_in_scope['$this'] = true;
}

View File

@ -568,18 +568,6 @@ class AssignmentAnalyzer
$assign_value_type
);
} elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) {
if ($context->mutation_free && !$context->collect_mutations && !$context->collect_initializations) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $assign_var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$assign_var->name instanceof PhpParser\Node\Identifier) {
// this can happen when the user actually means to type $this-><autocompleted>, but there's
// a variable on the next line
@ -635,6 +623,25 @@ class AssignmentAnalyzer
if ($var_id) {
$context->vars_possibly_in_scope[$var_id] = true;
}
$method_pure_compatible = !empty($assign_var->var->inferredType->external_mutation_free)
|| isset($assign_var->var->pure);
if (($context->mutation_free
|| ($context->external_mutation_free && !$method_pure_compatible))
&& !$context->collect_mutations
&& !$context->collect_initializations
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $assign_var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
} elseif ($assign_var instanceof PhpParser\Node\Expr\StaticPropertyFetch &&
$assign_var->class instanceof PhpParser\Node\Name
) {

View File

@ -1767,6 +1767,8 @@ class ExpressionAnalyzer
if (isset($stmt->expr->inferredType)) {
$clone_type = $stmt->expr->inferredType;
$immutable_cloned = false;
foreach ($clone_type->getTypes() as $clone_type_part) {
if (!$clone_type_part instanceof TNamedObject
&& !$clone_type_part instanceof TObject
@ -1797,9 +1799,25 @@ class ExpressionAnalyzer
return;
}
$codebase = $statements_analyzer->getCodebase();
if ($clone_type_part instanceof TNamedObject
&& $codebase->classExists($clone_type_part->value)
) {
$class_storage = $codebase->classlike_storage_provider->get($clone_type_part->value);
if ($class_storage->mutation_free) {
$immutable_cloned = true;
}
}
}
$stmt->inferredType = $stmt->expr->inferredType;
if ($immutable_cloned) {
$stmt->inferredType->external_mutation_free = true;
}
}
}

View File

@ -262,7 +262,7 @@ class Populator
if ($storage->mutation_free || $storage->external_mutation_free) {
foreach ($storage->methods as $method) {
if (!$method->is_static) {
if (!$method->is_static && !$method->external_mutation_free) {
$method->mutation_free = $storage->mutation_free;
$method->external_mutation_free = $storage->external_mutation_free;
}

View File

@ -112,7 +112,55 @@ class ImmutableAnnotationTest extends TestCase
return new self($id . rand(0, 1));
}
}'
]
],
'allowPropertySetOnNewInstance' => [
'<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
/**
* @psalm-external-mutation-free
*/
public function withBar(string $bar): self {
$new = new Foo("hello");
/** @psalm-suppress InaccessibleProperty */
$new->bar = $bar;
return $new;
}
}'
],
'allowClone' => [
'<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
/**
* @psalm-external-mutation-free
*/
public function withBar(string $bar): self {
$new = clone $this;
/** @psalm-suppress InaccessibleProperty */
$new->bar = $bar;
return $new;
}
}'
],
];
}
@ -143,7 +191,7 @@ class ImmutableAnnotationTest extends TestCase
$this->a = $a;
}
}',
'error_message' => 'ImpurePropertyAssignment',
'error_message' => 'InaccessibleProperty',
],
'immutablePropertyAssignmentExternally' => [
'<?php
@ -220,6 +268,34 @@ class ImmutableAnnotationTest extends TestCase
}',
'error_message' => 'ImpureMethodCall',
],
'cloneMutatingClass' => [
'<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
/**
* @psalm-external-mutation-free
*/
public function withBar(Bar $b): Bar {
$new = clone $b;
$b->a = $this->bar;
return $new;
}
}
class Bar {
public string $a = "hello";
}',
'error_message' => 'ImpurePropertyAssignment',
],
];
}
}