Skip to content
Permalink
Browse files

Allow immutable objects to be cloned

Fixes #2111
  • Loading branch information...
muglug committed Sep 9, 2019
1 parent 21aa162 commit b49444b8adaefc9799eb486caa36d7e4abe42416
@@ -178,6 +178,13 @@ public function analyze(
}
} 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;
}
@@ -568,18 +568,6 @@ public static function analyze(
$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 @@ public static function analyze(
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
) {
@@ -1767,6 +1767,8 @@ protected static function analyzeClone(
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 @@ protected static function analyzeClone(
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;
}
}
}
@@ -262,7 +262,7 @@ private function populateClassLikeStorage(ClassLikeStorage $storage, array $depe
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;
}
@@ -112,7 +112,55 @@ public static function fromString(string $id): self
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 @@ public function setA(int $a): void {
$this->a = $a;
}
}',
'error_message' => 'ImpurePropertyAssignment',
'error_message' => 'InaccessibleProperty',
],
'immutablePropertyAssignmentExternally' => [
'<?php
@@ -220,6 +268,34 @@ public static function setRedirectHeader(string $s) : void {
}',
'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',
],
];
}
}

0 comments on commit b49444b

Please sign in to comment.
You can’t perform that action at this time.