1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Fix #2805 - forbid passing in mutable class to mutation-free context

This commit is contained in:
Brown 2020-02-21 18:25:35 -05:00
parent f4485cc529
commit 7d99a15072
6 changed files with 146 additions and 1 deletions

View File

@ -183,6 +183,7 @@
<xs:element name="ImplementedParamTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplementedReturnTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplicitToStringCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureArgument" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureByReferenceAssignment" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureFunctionCall" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureMethodCall" type="IssueHandlerType" minOccurs="0" />

View File

@ -332,6 +332,43 @@ function takesString(string $s) : void {}
takesString(new A);
```
### ImpureArgument
Emitted when passing a mutable value into a function or method marked as mutation-free.
```php
class Item {
private int $i = 0;
public function mutate(): void {
$this->i++;
}
/** @psalm-mutation-free */
public function get(): int {
return $this->i;
}
}
/**
* @psalm-immutable
*/
class Immutable {
private Item $item;
public function __construct(Item $item) {
$this->item = $item;
}
public function get(): int {
return $this->item->get();
}
}
$item = new Item();
new Immutable($item);
```
### ImpureByReferenceAssignment
Emitted when assigning a passed-by-reference variable inside a function or method marked as mutation-free.

View File

@ -19,6 +19,7 @@ use Psalm\Internal\Type\UnionTemplateHandler;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\ImpureArgument;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidPassByReference;
use Psalm\Issue\InvalidScalarArgument;
@ -1395,6 +1396,34 @@ class CallAnalyzer
) === false) {
return false;
}
if ((($function_storage
&& $function_storage->mutation_free
&& (!$function_storage instanceof \Psalm\Storage\MethodStorage
|| !$function_storage->mutation_free_inferred))
|| ($class_storage
&& $class_storage->mutation_free))
&& !$statements_analyzer->node_data->isPureCompatible($arg->value)
&& ($node_type = $statements_analyzer->node_data->getType($arg->value))
) {
foreach ($node_type->getAtomicTypes() as $atomic_arg_type) {
if ($atomic_arg_type instanceof Type\Atomic\TNamedObject) {
$class_storage = $codebase->classlike_storage_provider->get($atomic_arg_type->value);
if (!$class_storage->mutation_free) {
if (IssueBuffer::accepts(
new ImpureArgument(
'Cannot pass mutable value to ' . $cased_method_id,
new CodeLocation($statements_analyzer->getSource(), $arg->value)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
}
}
if ($method_id === 'array_map' || $method_id === 'array_filter') {

View File

@ -260,7 +260,8 @@ class Reflection
$storage->is_static = $method->isStatic();
$storage->abstract = $method->isAbstract();
$storage->mutation_free = $storage->external_mutation_free = $method_name_lc === '__construct';
$storage->mutation_free = $storage->external_mutation_free
= $method_name_lc === '__construct' && $fq_class_name_lc === 'datetimezone';
$declaring_method_id = $declaring_class->name . '::' . $method_name_lc;

View File

@ -0,0 +1,7 @@
<?php
namespace Psalm\Issue;
class ImpureArgument extends CodeIssue
{
const ERROR_LEVEL = -1;
}

View File

@ -297,6 +297,42 @@ class ImmutableAnnotationTest extends TestCase
private function test(): void {}
}'
],
'canPassImmutableIntoImmutable' => [
'<?php
/**
* @psalm-immutable
*/
class Item {
private int $i;
public function __construct(int $i) {
$this->i = $i;
}
/** @psalm-mutation-free */
public function get(): int {
return $this->i;
}
}
/**
* @psalm-immutable
*/
class Immutable {
private $item;
public function __construct(Item $item) {
$this->item = $item;
}
public function get(): int {
return $this->item->get();
}
}
$item = new Item(5);
new Immutable($item);',
],
];
}
@ -459,6 +495,40 @@ class ImmutableAnnotationTest extends TestCase
}',
'error_message' => 'MissingImmutableAnnotation',
],
'preventPassingMutableIntoImmutable' => [
'<?php
class Item {
private int $i = 0;
public function mutate(): void {
$this->i++;
}
/** @psalm-mutation-free */
public function get(): int {
return $this->i;
}
}
/**
* @psalm-immutable
*/
class Immutable {
private $item;
public function __construct(Item $item) {
$this->item = $item;
}
public function get(): int {
return $this->item->get();
}
}
$item = new Item();
new Immutable($item);',
'error_message' => 'ImpureArgument',
],
];
}
}