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:
parent
f4485cc529
commit
7d99a15072
@ -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" />
|
||||
|
@ -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.
|
||||
|
@ -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') {
|
||||
|
@ -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;
|
||||
|
||||
|
7
src/Psalm/Issue/ImpureArgument.php
Normal file
7
src/Psalm/Issue/ImpureArgument.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class ImpureArgument extends CodeIssue
|
||||
{
|
||||
const ERROR_LEVEL = -1;
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user