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

Unused method calls when class is immutable #6967

Closed
christian-kolb opened this issue Nov 22, 2021 · 13 comments · Fixed by #6972
Closed

Unused method calls when class is immutable #6967

christian-kolb opened this issue Nov 22, 2021 · 13 comments · Fixed by #6972

Comments

@christian-kolb
Copy link
Contributor

Recently there was an issue which was fixed that mentioned unused method calls (#5528) which was marked as resolved and solved the issue when @psalm-assert is used.

Unfortunately there is still an issue when there is no @psalm-assert. For some cases using @psalm-assert doesn't make sense because we don't assert the type of parameter (we already handle that through typing) or the method doesn't have a parameter to begin with.

I didn't add a reproducible case back then, my bad.

Here are two cases which reproduce the open issue. One which is the simplest way to reproduce it and another that is a real life example:

Small reproduction:

https://psalm.dev/r/50306152ed

Real life example:

https://psalm.dev/r/df40430282

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/50306152ed
<?php

declare(strict_types=1);

/** @psalm-immutable */
final class UserList 
{    
    public function validate(): void
    {
        // Some validation happens here
		throw new \InvalidArgumentException();
    }
}

$a = new UserList();
$a->validate();
Psalm output (using commit 9943efb):

ERROR: UnusedMethodCall - 16:5 - The call to UserList::validate is not used
https://psalm.dev/r/df40430282
<?php

declare(strict_types=1);

/** @psalm-immutable */
final class UserList 
{    
    private array $userIds = [];
    
    public function __construct(array $userIds)
    {
        $this->userIds = $userIds;
    }
    
    public function add(string $userId): self
    {
        $newUserIds = $this->userIds;
        $newUserIds[] = $userId;
        
        return new self($newUserIds);
    }
    
    public function mustContainUserId(string $userId): void
    {
        if (!in_array($userId, $this->userIds, true)) {
			throw new \DomainException('Missing user');            
        }
    }
    
    public function mustNotBeEmpty(): void   
    {
        if (count($this->userIds) === 0) {
            throw new \DomainException('No user available');
        }
    }
}

$a = new UserList([]);
$a->mustNotBeEmpty();
$a->mustContainUserId('d90c8652-1dab-4bc1-9d51-a02c758408c5');
Psalm output (using commit 9943efb):

ERROR: UnusedMethodCall - 39:5 - The call to UserList::mustNotBeEmpty is not used

ERROR: UnusedMethodCall - 40:5 - The call to UserList::mustContainUserId is not used

@orklah
Copy link
Collaborator

orklah commented Nov 22, 2021

Yeah, this is an issue. It has to do with the definition of immutable and the assumption that void immutable function are useless.

Psalm rightfully assume that a true immutable function who doesn't return is useless. However, throwing an exception is hardly true immutability.

I guess this could be solved by removing the unused method call when @throws are present in a docblock, but my feeling here is that it's a deeper issue with the definition of immutability

@christian-kolb
Copy link
Contributor Author

@orklah Thanks for the quick reply.

Psalm rightfully assume that a true immutable function who doesn't return is useless. However, throwing an exception is hardly true immutability.

What part of immutability goes against throwing an exception? I've just reread the Psalm article about immutability, but didn't find a reference to it. It's quite possible that I'm missing something, I just don't understand what part 🙂

We use immutable objects a lot with value objects. And put validation in the value objects themselves. This way we have readable code like:

$command->userId->mustNotBeEqualTo($requestingUser->id);

$newsArticle->status->mustNotBePublished();

userId and status are immutable value objects in that example.

Unfortunately using a @throws annotation leads to the same result:

https://psalm.dev/r/e281137268

@psalm-github-bot
Copy link

I found these snippets:

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

declare(strict_types=1);

/** @psalm-immutable */
final class UserList 
{    
    private array $userIds = [];
    
    public function __construct(array $userIds)
    {
        $this->userIds = $userIds;
    }
    
    public function add(string $userId): self
    {
        $newUserIds = $this->userIds;
        $newUserIds[] = $userId;
        
        return new self($newUserIds);
    }
    
    /** @throws \DomainException */
    public function mustContainUserId(string $userId): void
    {
        if (!in_array($userId, $this->userIds, true)) {
			throw new \DomainException('Missing user');            
        }
    }
    
    /** @throws \DomainException */
    public function mustNotBeEmpty(): void   
    {
        if (count($this->userIds) === 0) {
            throw new \DomainException('No user available');
        }
    }
}

$a = new UserList([]);
$a->mustNotBeEmpty();
$a->mustContainUserId('d90c8652-1dab-4bc1-9d51-a02c758408c5');
Psalm output (using commit 9943efb):

ERROR: UnusedMethodCall - 41:5 - The call to UserList::mustNotBeEmpty is not used

ERROR: UnusedMethodCall - 42:5 - The call to UserList::mustContainUserId is not used

@orklah
Copy link
Collaborator

orklah commented Nov 22, 2021

What I meant to say is:

  • whether Psalm considers that immutable function can't throw exceptions
  • or it considers that no immutable function can be assumed as unused upfront

Currently it does both and so it suggest removing function that have external side effects (namely, throwing an exception)

My take is that immutable function shouldn't be able to throw, but it's not the current definition in Psalm. It could be debated that immutable stays with the current definition and we need a new term for when a function doesn't have side effects AND doesn't throw, but it will start getting confusing.

In the meantime, a change could be made so that Psalm stop emitting UnusedMethodCall when the method has @throws (but it doesn't currently work)

@christian-kolb
Copy link
Contributor Author

@orklah My understanding is that pure functions aren't allowed to throw exceptions. But immutable classes doesn't have to be pure. They can but pure would be the next step.

I would be very much in favor of allowing exceptions to be thrown on functions within immutable classes 🙂

@orklah
Copy link
Collaborator

orklah commented Nov 22, 2021

Psalm just don't consider exceptions at all for pure/immutable: https://psalm.dev/r/5e92a0f653

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/5e92a0f653
<?php

/**
 * @psalm-pure
 */
function takesAnInt(int $i): void {
    throw new Exception((string)$i);
}
Psalm output (using commit e13c48a):

No issues!

@weirdan
Copy link
Collaborator

weirdan commented Nov 22, 2021

My take is that immutable function shouldn't be able to throw, but it's not the current definition in Psalm. It could be debated that immutable stays with the current definition

See #2160

and we need a new term for when a function doesn't have side effects AND doesn't throw, but it will start getting confusing.

It doesn't have to be a single term. @Ocramius proposed @psalm-never-throw in #2912, and I think that would solve this as an orthogonal concern.

@orklah
Copy link
Collaborator

orklah commented Nov 22, 2021

It doesn't have to be a single term.

Yeah, but requiring people to add both immutable and never-throw just to be able to flag the function as unused -and only if it returns void- seems a bit much...

@Ocramius
Copy link
Contributor

My take is that immutable function shouldn't be able to throw

Throw can be pure - the only impure fact in it is the current stack trace, but there's nothing impure in a thrown exception otherwise.

@orklah
Copy link
Collaborator

orklah commented Nov 22, 2021

Throw can be pure

What I don't get is that somehow, those two aren't treated the same way:
https://psalm.dev/r/d114adc5ee

According to Psalm's doc, a pure function is

one whose output is just a function of its input.

Either references are not considered as outputs and the first one should be fine or the reference is an output, and in this case, the exception should be too, no?

@psalm-github-bot
Copy link

I found these snippets:

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

class A{
    /** @psalm-pure */
    public static function B(string &$error): void{
        $error = 'this is an error';
    }
}

class C{
    /** @psalm-pure */
    public static function D(): void{
        throw new Exception('this is an error');
    }
}

/** @psalm-pure */
function pure(): void{
    $myError = '';
    A::B($myError);
    
    $_myOtherError = '';
    try{
        C::D();
    }
  	catch(Exception $e){
     	$_myOtherError = $e->getMessage();   
    }
}
Psalm output (using commit eca56c0):

ERROR: ImpureByReferenceAssignment - 6:9 - Variable $error cannot be assigned to as it is passed by reference

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