Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit immutable object mutation is not detected #6881

Open
vudaltsov opened this issue Nov 10, 2021 · 8 comments
Open

Implicit immutable object mutation is not detected #6881

vudaltsov opened this issue Nov 10, 2021 · 8 comments

Comments

@vudaltsov
Copy link
Contributor

https://psalm.dev/r/bdd3b6d567

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/bdd3b6d567
<?php

/**
 * @psalm-immutable
 */
final class A
{
    public stdClass $b;
    
	public function __construct()
    {
        $this->b = new stdClass();
    }
}

$a = new A();
$a->b->c = 2; // mutates $a!
Psalm output (using commit d72b384):

No issues!

@weirdan
Copy link
Collaborator

weirdan commented Nov 10, 2021

$a->b->c = 2; // mutates $a!

Does it?

@weirdan
Copy link
Collaborator

weirdan commented Nov 10, 2021

I mean, Psalm allows immutable objects to reference mutable structures: https://psalm.dev/r/7992cc4b39, so why would stdClass be any different? In other words, immutability is shallow and does not extend to values of properties. One popular example of that are immutable message objects in PSR-7 that compose mutable body streams.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/7992cc4b39
<?php

class B {
    public int $c = 1;
}

/**
 * @psalm-immutable
 */
final class A
{
    public B $b;
    
	public function __construct()
    {
        $this->b = new B;
    }
}

$a = new A();
$a->b->c = 2; // mutates $a!
Psalm output (using commit d72b384):

No issues!

@vudaltsov
Copy link
Contributor Author

I completely agree that immutable object should be allowed to contain mutable structures. The problem is not in stdClass, it's just an example.

The problem is that when you pass that mutable structure somewhere else and mutate it, the immutable object changes and you cannot rely on it anymore, although the instantiating code considered it to be unchangeable.

Recently we found a really painful bug. We didn't clone an immutable command's mutable property in the middleware and the handler was getting a different version of the command.

Maybe we could have @psalm-immutable-strict or @psalm-immutable-recursive to mark an object for which Psalm forbids any implicit external mutations?

Also, consider this example: https://psalm.dev/r/563c78d9ec. Isn't it kind of counterintuitive that line 26 is ok, while line 21 isn't?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/563c78d9ec
<?php

class B {
    public int $c = 1;
}

/**
 * @psalm-immutable
 */
final class A
{
    public B $b;
    
	public function __construct()
    {
        $this->b = new B;
    }
    
    private function mutateB(): void
    {
		$this->b->c = 2;
    }
}

$a = new A();
$a->b->c = 2; // same thing as on line 21, but this time allowed!
Psalm output (using commit 8c761dd):

ERROR: ImpurePropertyAssignment - 21:3 - Cannot assign to a property from a mutation-free context

@weirdan
Copy link
Collaborator

weirdan commented Nov 11, 2021

@vudaltsov how would you answer the inlined questions in the following snippet: https://psalm.dev/r/23dd904a7a

I suspect the thing that you seem to suggest cannot be implemented without ownership tracking like Rust has.

Alternatively we could require all object properties of fully immutable (for the lack of better name) object to be themselves fully immutable. This way both your snippets would emit errors as neither B nor stdClass are fully immutable.

Isn't it kind of counterintuitive that line 26 is ok, while line 21 isn't?

It could be confusing at first glance, for sure. But if you consult the definition of @psalm-immutable it becomes quite obvious.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/23dd904a7a
<?php

class B { public int $c = 1; }

/** @psalm-immutable */
final class A {    
	public function __construct(
        public B $b = new B
    ) {}
}

$a = new A();
$a->b->c = 2; // assuming this one is flagged

$z = $a->b;
$z->c = 10; // would this have to be flagged as well?
f($a->b); // would this?

function f(object $o): void {
    $o->c = 30;
}
Psalm output (using commit e07e359):

No issues!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants