mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
parent
21aa162d0a
commit
b49444b8ad
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user