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

Integer ranges not checked correctly #8838

Closed
sebastianbergmann opened this issue Dec 5, 2022 · 4 comments · Fixed by #8934
Closed

Integer ranges not checked correctly #8838

sebastianbergmann opened this issue Dec 5, 2022 · 4 comments · Fixed by #8934

Comments

@sebastianbergmann
Copy link
Contributor

I have a method Position::fromFileAndRank() that uses @psalm-param int<1,8> annotations to limit the range of integer parameters to values >= 1 && <= 8:

final class Position
{
    /**
     * @psalm-param int<1,8> $file
     * @psalm-param int<1,8> $rank
     */
    public static function fromFileAndRank(int $file, int $rank): self
    {
        return new self($file, $rank);
    }
}

I have code that calls the method shown above after ensuring that the arguments to be passed are within the range the parameters expect:

$file = $this->position()->file() + $offset['file'];
$rank = $this->position()->rank() + $offset['rank'];

if ($file >= 1 && $file <= 8 && $rank >= 1 && $rank <= 8) {
    $positions[] = Position::fromFileAndRank($file, $rank);
}

But Psalm claims that the arguments are not within the range the parameters expect:

ERROR: [InvalidArgument] ... Argument 1 of Position::fromFileAndRank expects int<1, 8>, but int<min, max> provided

ERROR: [InvalidArgument] ... Argument 2 of Position::fromFileAndRank expects int<1, 8>, but int<min, max> provided

Reproducing example: https://psalm.dev/r/75a668a862

From the documentation of integer ranges it is not clear to me whether Psalm only allows literal integers or whether it should be able to infer that a variable is within range.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/75a668a862
<?php declare(strict_types=1);
abstract class Piece
{
    private readonly Position $position;

    private function __construct(Position $position)
    {
        $this->position = $position;
    }

    /**
     * @psalm-mutation-free
     */
    public function position(): Position
    {
        return $this->position;
    }

    /**
     * @psalm-param list<array{file: int, rank: int}> $offsets
     */
    protected function offsetsToTargetPositionCandidates(array $offsets): void
    {
        $positions = [];

        foreach ($offsets as $offset) {
            $file = $this->position()->file() + $offset['file'];
            $rank = $this->position()->rank() + $offset['rank'];

            if ($file >= 1 && $file <= 8 && $rank >= 1 && $rank <= 8) {
                $positions[] = Position::fromFileAndRank($file, $rank);
            }
        }
    }
}

/**
 * @psalm-immutable
 */
final class Position
{
    /**
     * @psalm-var int<1,8>
     */
    private readonly int $file;

    /**
     * @psalm-var int<1,8>
     */
    private readonly int $rank;

    /**
     * @psalm-param int<1,8> $file
     * @psalm-param int<1,8> $rank
     */
    public static function fromFileAndRank(int $file, int $rank): self
    {
        return new self($file, $rank);
    }

    /**
     * @psalm-param int<1,8> $file
     * @psalm-param int<1,8> $rank
     */
    private function __construct(int $file, int $rank)
    {
        $this->file = $file;
        $this->rank = $rank;
    }

    /**
     * @psalm-return int<1,8>
     */
    public function file(): int
    {
        return $this->file;
    }

    /**
     * @psalm-return int<1,8>
     */
    public function rank(): int
    {
        return $this->rank;
    }
}
Psalm output (using commit d2f7d86):

ERROR: InvalidArgument - 31:58 - Argument 1 of Position::fromFileAndRank expects int<1, 8>, but int<min, max> provided

ERROR: InvalidArgument - 31:65 - Argument 2 of Position::fromFileAndRank expects int<1, 8>, but int<min, max> provided

INFO: UnusedVariable - 24:9 - $positions is never referenced or the value is not used

@still-dreaming-1
Copy link
Contributor

still-dreaming-1 commented Dec 10, 2022

Interestingly, this workaround avoids the issue somehow. Psalm is apparently able to infer the correct types of the ints after comparisons in this case: https://psalm.dev/r/9251fdd044

@psalm-github-bot
Copy link

psalm-github-bot bot commented Dec 10, 2022

I found these snippets:

https://psalm.dev/r/9251fdd044
<?php declare(strict_types=1);
abstract class Piece
{
    private readonly Position $position;

    private function __construct(Position $position)
    {
        $this->position = $position;
    }

    /**
     * @psalm-mutation-free
     */
    public function position(): Position
    {
        return $this->position;
    }

    /**
     * @psalm-param list<array{file: int, rank: int}> $offsets
     */
    protected function offsetsToTargetPositionCandidates(array $offsets): void
    {
        $positions = [];

        foreach ($offsets as $offset) {
            $file = $this->position()->file() + $offset['file'];
            $rank = $this->position()->rank() + $offset['rank'];

            $position = Position::fromFileAndRankIfWithinRange($file, $rank);
            if ($position !== null) {
                $positions[] = $position;
            }
        }
    }
}

/**
 * @psalm-immutable
 */
final class Position
{
    /**
     * @psalm-var int<1,8>
     */
    private readonly int $file;

    /**
     * @psalm-var int<1,8>
     */
    private readonly int $rank;

    /**
     * @psalm-param int<1,8> $file
     * @psalm-param int<1,8> $rank
     */
    public static function fromFileAndRank(int $file, int $rank): self
    {
        return new self($file, $rank);
    }
    
    /**
     * @psalm-param int $file
     * @psalm-param int $rank
     */
    public static function fromFileAndRankIfWithinRange(int $file, int $rank): ?self
    {
        if ($file >= 1 && $file <= 8 && $rank >= 1 && $rank <= 8) {
        	return self::fromFileAndRank($file, $rank);
        }
        return null;
    }

    /**
     * @psalm-param int<1,8> $file
     * @psalm-param int<1,8> $rank
     */
    private function __construct(int $file, int $rank)
    {
        $this->file = $file;
        $this->rank = $rank;
    }

    /**
     * @psalm-return int<1,8>
     */
    public function file(): int
    {
        return $this->file;
    }

    /**
     * @psalm-return int<1,8>
     */
    public function rank(): int
    {
        return $this->rank;
    }
}
Psalm output (using commit 16cdeb9):

INFO: UnusedVariable - 24:9 - $positions is never referenced or the value is not used

@theodorejb
Copy link
Contributor

@orklah It looks like this was also fixed by #8934.

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

Successfully merging a pull request may close this issue.

4 participants