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

False positives testing for array emptiness #10825

Open
flaviovs opened this issue Mar 14, 2024 · 19 comments
Open

False positives testing for array emptiness #10825

flaviovs opened this issue Mar 14, 2024 · 19 comments

Comments

@flaviovs
Copy link

https://psalm.dev/r/4609d92f6f

Note: using empty() because it's a common pattern in PHP apps to assert nullable arrays. However, removing empty() (i.e. doing if ($this->arr)...) still generates warnings, albeit different ones:

ERROR: [TypeDoesNotContainType](https://psalm.dev/056) - 13:13 - Operand of type array<never, never> is always falsy
ERROR: [TypeDoesNotContainType](https://psalm.dev/056) - 13:13 - Type array<never, never> for $this->arr is always !falsy
Copy link

I found these snippets:

https://psalm.dev/r/4609d92f6f
<?php

class Foo {
    /** @var string[]|null */
    protected ?array $arr;

    function __construct()
    {
        $this->arr = [];

        // ...do stuff here that *may* populate $this->arr, or set it to NULL...

        if (empty($this->arr)) {
            echo "empty\n";
        }
    }
}
Psalm output (using commit ef3b018):

ERROR: RedundantCondition - 13:13 - Operand of type true is always truthy

ERROR: RedundantCondition - 13:13 - Type array<never, never> for $this->arr is never non-empty

@kkmuffme
Copy link
Contributor

Could you provide a full example?
When I call something that modifies it as your comment says, it works fine: https://psalm.dev/r/d12fcff59a

Copy link

I found these snippets:

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

class Foo {
    /** @var string[]|null */
    protected ?array $arr;

    function __construct()
    {
        $this->arr = [];

        // ...do stuff here that *may* populate $this->arr, or set it to NULL...
        $this->foo();

        if ($this->arr === [] || $this->arr === null) {
            echo "empty\n";
        }
    }
    
    function foo() : void {
        if (rand(0, 1) > 0) {
        	$this->arr = ['world'];    
        }
    }
}
Psalm output (using commit ef3b018):

No issues!

@flaviovs
Copy link
Author

Could you provide a full example? When I call something that modifies it as your comment says, it works fine: https://psalm.dev/r/d12fcff59a

Hi @kkmuffme - I'm not sure I follow.

The snippet I posted is a minimal example that reproduces the issue. It is from a big app codebase that started to emit warnings after upgrading from 5.19.0 to latest Psalm. I don't see anything wrong with that code.

IMHO your changed code not issuing any warnings just confirm the problem, since your version do the same thing, i.e. $arr === [] || $a === null is almost the same as empty($arr), just more verbose.

Copy link

I found these snippets:

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

class Foo {
    /** @var string[]|null */
    protected ?array $arr;

    function __construct()
    {
        $this->arr = [];

        // ...do stuff here that *may* populate $this->arr, or set it to NULL...
        $this->foo();

        if ($this->arr === [] || $this->arr === null) {
            echo "empty\n";
        }
    }
    
    function foo() : void {
        if (rand(0, 1) > 0) {
        	$this->arr = ['world'];    
        }
    }
}
Psalm output (using commit ef3b018):

No issues!

@flaviovs
Copy link
Author

Here's another minimal example that reproduces the issue, extracted from the same app that started to emit warnings after upgrade:

https://psalm.dev/r/44845ca739

Copy link

I found these snippets:

https://psalm.dev/r/44845ca739
<?php

$arr = explode('.', phpversion()); 

if (empty($arr)) {
    echo "Empty\n";
}
Psalm output (using commit ef3b018):

ERROR: DocblockTypeContradiction - 5:5 - Operand of type false is always falsy

ERROR: DocblockTypeContradiction - 5:5 - Docblock-defined type non-empty-list<string> for $arr is never falsy

@ArtemGoutsoul
Copy link
Contributor

Maybe a somewhat more minimal case:

/**
 * @param true[] $array
 */
function check_key_in_array(array $array, int $key): void
{
    if (!empty($array[$key])) echo "key $key not empty";
}

gives: ERROR: [RedundantConditionGivenDocblockType](https://psalm.dev/156) - 8:9 - Operand of type true is always truthy

the element is ALWAYS truthy when present, but it can be missing -> empty -> falsy, since the keys are not specified.

https://psalm.dev/r/557faa3f96

Copy link

I found these snippets:

https://psalm.dev/r/557faa3f96
<?php

/**
 * @param true[] $array
 */
function check_key_in_array(array $array, int $key): void
{
    if (!empty($array[$key])) echo "key $key not empty";
}
Psalm output (using commit ef3b018):

ERROR: RedundantConditionGivenDocblockType - 8:9 - Operand of type true is always truthy

@ArtemGoutsoul
Copy link
Contributor

@kkmuffme
Copy link
Contributor

kkmuffme commented Mar 21, 2024

@flaviovs your first example is not a minimal example - the place where you put

// ...do stuff here that may populate $this->arr, or set it to NULL...

is the issue - since this code that you claim may modify it, is analyzed by psalm. This is why I provided an example where this code is actually there (and not just a comment) to show it works if there's actual code there, and not just a comment. But feel free to improve upon my example, since again - this is just one example to showcase it works with actual code that is not a comment.

$arr === [] || $a === null is almost the same as empty($arr)

Yeah, almost but not entirely. Anyway you can just use empty: https://psalm.dev/r/206b194230 (the error you get now is bc it's almost the same but not entirely as the verbose check - but this is unrelted to this question. I just wanted to clarify this to prevent further questions in this regard, since that's not the issue we agree.)

Here's another minimal example that reproduces the issue, extracted from the same app that started to emit warnings after upgrade:
https://psalm.dev/r/44845ca739

This error is correct too. explode never returns a falsy (empty) value.


@ArtemGoutsoul can you please open a separate issue, since this is an entirely different issue caused by the true array (afaik there's an issue for that alraedy, but I couldn't find it right now). As a temp workaround you can just use isset() there
EDIT: you already reported this here #10715 which is fixed - what's the purpose of tacking this on here?

Copy link

I found these snippets:

https://psalm.dev/r/206b194230
<?php

class Foo {
    /** @var string[]|null */
    protected ?array $arr;

    function __construct()
    {
        $this->arr = [];

        // ...do stuff here that *may* populate $this->arr, or set it to NULL...
        $this->foo();

        if (empty($this->arr)) {
            echo "empty\n";
        }
    }
    
    function foo() : void {
        if (rand(0, 1) > 0) {
        	$this->arr = ['world'];    
        }
    }
}
Psalm output (using commit ef3b018):

ERROR: RiskyTruthyFalsyComparison - 14:13 - Operand of type array<array-key, string>|null contains type array<array-key, string>, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead.
https://psalm.dev/r/44845ca739
<?php

$arr = explode('.', phpversion()); 

if (empty($arr)) {
    echo "Empty\n";
}
Psalm output (using commit ef3b018):

ERROR: DocblockTypeContradiction - 5:5 - Operand of type false is always falsy

ERROR: DocblockTypeContradiction - 5:5 - Docblock-defined type non-empty-list<string> for $arr is never falsy

@ArtemGoutsoul
Copy link
Contributor

@kkmuffme you are right, maybe it's a separate issue, but should not be #10715, since this issue is still present in 5.23.1

If the fix for #10715 is in the upcoming release and not in 5.23.1, then my comments are a brainfart on my part and should be erased, I should just wait for the next release.

@M1ke
Copy link
Contributor

M1ke commented Jun 28, 2024

My team are debating this assertion

$arr === [] || $arr === null is almost the same as empty($arr)

Does anyone have a value of $arr (where arr meets type definition array|null) where the empty case returns a different bool to the || case?

Tried a set here: https://3v4l.org/VlLK6#v8.3.8

@kkmuffme
Copy link
Contributor

Can you please create an example on psalm.dev bc I have no idea what psalm error you're referring too, since the OP issue was a non-issue, then it got hijacked by someone with a completely different issue and I have no idea what you're referring to :-)

@ArtemGoutsoul
Copy link
Contributor

ArtemGoutsoul commented Jul 4, 2024

My team are debating this assertion

$arr === [] || $arr === null is almost the same as empty($arr)

Does anyone have a value of $arr (where arr meets type definition array|null) where the empty case returns a different bool to the || case?

Tried a set here: https://3v4l.org/VlLK6#v8.3.8

For a defined $arr that is array|null the expression $arr === [] || $arr === null is identical to empty($arr), since empty essentially casts to bool (and returns false for undefined vars):

@M1ke
Copy link
Contributor

M1ke commented Jul 5, 2024

OK great; I was wondering about the "almost the same" comment because I really couldn't see why they were not identical. Someone pointed out that empty is safe for checking undefined values, but that's a different condition.

@flaviovs
Copy link
Author

flaviovs commented Jul 5, 2024

OP here. I've done some more tests an here's what I came up:

Leaving null check out, same results: https://psalm.dev/r/ba93e8414a

Using count($arr) === 0 or $arr === [], same results:

So the issue may not be even related to empty() after all.

Copy link

I found these snippets:

https://psalm.dev/r/ba93e8414a
<?php
class Foo
{
	public array $data = [];

    public function resetData(): void
    {
        $this->data = [];

        $this->changeData();
        if (empty($this->data)) {
            echo "Is empty\n";
        }
    }
    
    public function changeData(): void
    {
        if (rand(0, 10) > 5) {
        	$this->data[] = 1;
    	}
    }
}
Psalm output (using commit 16b24bd):

ERROR: RedundantCondition - 11:13 - Operand of type true is always truthy

ERROR: RedundantCondition - 11:13 - Type array<never, never> for $this->data is never non-empty
https://psalm.dev/r/a93bf069be
<?php
class Foo
{
	public array $data = [];

    public function resetData(): void
    {
        $this->data = [];

        $this->changeData();
        if (count($this->data) === 0) {
            echo "Is empty\n";
        }
    }
    
    public function changeData(): void
    {
        if (rand(0, 10) > 5) {
        	$this->data[] = 1;
    	}
    }
}
Psalm output (using commit 16b24bd):

ERROR: RedundantCondition - 11:13 - Type array<never, never> for $this->data is never non-empty-countable
https://psalm.dev/r/ee7c8a7f07
<?php
class Foo
{
	public array $data = [];

    public function resetData(): void
    {
        $this->data = [];

        $this->changeData();
        if ($this->data === []) {
            echo "Is empty\n";
        }
    }
    
    public function changeData(): void
    {
        if (rand(0, 10) > 5) {
        	$this->data[] = 1;
    	}
    }
}
Psalm output (using commit 16b24bd):

ERROR: RedundantCondition - 11:13 - Type array<never, never> for $this->data is never non-empty-countable

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

No branches or pull requests

4 participants