Skip to content

Commit

Permalink
Fix #2729 - allow mutation of otherwise-readonly properties
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Feb 2, 2020
1 parent 8d7fb2b commit 93fc1b1
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/Psalm/DocComment.php
Expand Up @@ -151,7 +151,7 @@ public static function parse($docblock, $line_number = null, $preserve_format =
'ignore-variable-method', 'ignore-variable-property', 'internal',
'taint-sink', 'taint-source', 'assert-untainted', 'scope-this',
'mutation-free', 'external-mutation-free', 'immutable', 'readonly',
'remove-taint',
'remove-taint', 'allow-private-mutation',
],
true
)) {
Expand Down Expand Up @@ -275,7 +275,7 @@ public static function parsePreservingLength(\PhpParser\Comment\Doc $docblock)
'ignore-variable-method', 'ignore-variable-property', 'internal',
'taint-sink', 'taint-source', 'assert-untainted', 'scope-this',
'mutation-free', 'external-mutation-free', 'immutable', 'readonly',
'remove-taint',
'remove-taint', 'allow-private-mutation',
],
true
)) {
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Internal/Analyzer/CommentAnalyzer.php
Expand Up @@ -182,6 +182,8 @@ public static function arrayToDocblocks(
$var_comment->internal = isset($parsed_docblock['specials']['internal']);
$var_comment->readonly = isset($parsed_docblock['specials']['readonly'])
|| isset($parsed_docblock['specials']['psalm-readonly']);
$var_comment->allow_private_mutation
= isset($parsed_docblock['specials']['psalm-allow-private-mutation']);
$var_comment->remove_taint = isset($parsed_docblock['specials']['psalm-remove-taint']);

if (isset($parsed_docblock['specials']['psalm-internal'])) {
Expand Down Expand Up @@ -214,6 +216,7 @@ public static function arrayToDocblocks(
$var_comment->internal = isset($parsed_docblock['specials']['internal']);
$var_comment->readonly = isset($parsed_docblock['specials']['readonly'])
|| isset($parsed_docblock['specials']['psalm-readonly']);
$var_comment->allow_private_mutation = isset($parsed_docblock['specials']['psalm-allow-private-mutation']);
$var_comment->remove_taint = isset($parsed_docblock['specials']['psalm-remove-taint']);

$var_comments[] = $var_comment;
Expand Down
Expand Up @@ -659,6 +659,7 @@ public static function analyzeInstance(
&& (!$context->calling_function_id
|| \strpos($context->calling_function_id, '::__construct')
|| \strpos($context->calling_function_id, '::unserialize')
|| $property_storage->allow_private_mutation
|| $property_pure_compatible)
)
) {
Expand Down
7 changes: 7 additions & 0 deletions src/Psalm/Internal/Scanner/VarDocblockComment.php
Expand Up @@ -66,6 +66,13 @@ class VarDocblockComment
*/
public $readonly = false;

/**
* Whether or not to allow mutation by internal methods
*
* @var bool
*/
public $allow_private_mutation = false;

/**
* @var bool
*/
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Visitor/ReflectorVisitor.php
Expand Up @@ -3214,6 +3214,7 @@ private function visitPropertyDeclaration(
$property_storage->internal = $var_comment ? $var_comment->internal : false;
$property_storage->psalm_internal = $var_comment ? $var_comment->psalm_internal : null;
$property_storage->readonly = $var_comment ? $var_comment->readonly : false;
$property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false;

if (!$signature_type && !$doc_var_group_type) {
if ($property->default) {
Expand Down
7 changes: 7 additions & 0 deletions src/Psalm/Storage/PropertyStorage.php
Expand Up @@ -74,6 +74,13 @@ class PropertyStorage
*/
public $readonly = false;

/**
* Whether or not to allow mutation by internal methods
*
* @var bool
*/
public $allow_private_mutation = false;

/**
* @var null|string
*/
Expand Down
38 changes: 38 additions & 0 deletions tests/PropertyTypeTest.php
Expand Up @@ -1698,6 +1698,22 @@ public function __construct() {
echo (new A)->bar;'
],
'readonlyWithPrivateMutationsAllowedPropertySetInAnotherMEthod' => [
'<?php
class A {
/**
* @readonly
* @psalm-allow-private-mutation
*/
public ?string $bar = null;
public function setBar(string $s) : void {
$this->bar = $s;
}
}
echo (new A)->bar;'
],
'readonlyPropertySetChildClass' => [
'<?php
abstract class A {
Expand Down Expand Up @@ -2821,6 +2837,28 @@ public function __construct() {
$a->bar = "goodbye";',
'error_message' => 'InaccessibleProperty',
],
'readonlyPropertySetInConstructorAndAlsoOutsideClass' => [
'<?php
class A {
/**
* @readonly
* @psalm-allow-private-mutation
*/
public string $bar;
public function __construct() {
$this->bar = "hello";
}
public function setAgain() : void {
$this->bar = "hello";
}
}
$a = new A();
$a->bar = "goodbye";',
'error_message' => 'InaccessibleProperty - src/somefile.php:19:21',
],
'addNullToMixedAfterNullablePropertyFetch' => [
'<?php
class A {
Expand Down

0 comments on commit 93fc1b1

Please sign in to comment.