psalm icon indicating copy to clipboard operation
psalm copied to clipboard

Inconsistent handling of "empty"-status of arrays as return values

Open evang522 opened this issue 2 years ago • 4 comments

Description The assumption about whether arrays are non-empty seems to be inconsistent between the types list and array as output typing from a function.

To Reproduce https://psalm.dev/r/7320dbad2e In this first snippet, the return type of the function map is typed with "list<T>". When attempting to use the spread operator on this return value as the parameter of a function which requires at least 1 member to be present, it triggers a warning "possibly undefined".

https://psalm.dev/r/5f017e6180 The second snippet is the same setup -- but the return value is array<T> not list<T> -- but the above error does not occur.

Expected behavior I don't have intimate knowledge of Psalm internals, so I can't say whether the one behaviour or the other is correct -- but it does seem that it is inconsistent between the two.

I would expect that Psalm would consider both array<T> and list<T> to be potentially undefined in this spread operator -- or that they would both not cause a problem.

Environment (please complete the following information):

  • OS: Linux Fedora
  • PHP version 8.1
  • Psalm version 5.5.0

Thanks a lot for a great library !

evang522 avatar Jan 26 '23 12:01 evang522

I found these snippets:

https://psalm.dev/r/7320dbad2e
<?php

/**
 * @template Tk
 * @template Tv
 * @template T
 *
 * @param iterable<Tk, Tv> $iterable Iterable to be mapped over
 * @param (Closure(Tv): T) $function
 *
 * @return list<T>
 */
function map(iterable $iterable, Closure $function): array
{
    if (is_array($iterable)) {
        return array_values(array_map(
        /**
         * @param Tv $v
         */
            static fn($v) => $function($v),
            $iterable
        ));
    }

    $result = [];
    foreach ($iterable as $value) {
        $result[] = $function($value);
    }

    return $result;
}

function echoThings(string $first, string ...$rest): void
{
    foreach ([$first, ...$rest] as $thingToSay) {
        echo $thingToSay;
    }
}


/**
 * @var array<string> $things
 * @phpstan-var array<non-empty-string> $things
 * @psalm-var non-empty-array<non-empty-string> $things
 */
$things =  ['example'];

echoThings(...map($things, static fn (string $thing) => $thing));
Psalm output (using commit aec0edc):

ERROR: InvalidScalarArgument - 48:15 - Argument 1 of echoThings expects string, but possibly undefined non-empty-string provided
https://psalm.dev/r/5f017e6180
<?php

/**
 * @template Tk
 * @template Tv
 * @template T
 *
 * @param iterable<Tk, Tv> $iterable Iterable to be mapped over
 * @param (Closure(Tv): T) $function
 *
 * @return array<T>
 */
function map(iterable $iterable, Closure $function): array
{
    if (is_array($iterable)) {
        return array_values(array_map(
        /**
         * @param Tv $v
         */
            static fn($v) => $function($v),
            $iterable
        ));
    }

    $result = [];
    foreach ($iterable as $value) {
        $result[] = $function($value);
    }

    return $result;
}

function echoThings(string $first, string ...$rest): void
{
    foreach ([$first, ...$rest] as $thingToSay) {
        echo $thingToSay;
    }
}


/**
 * @var array<string> $things
 * @phpstan-var array<non-empty-string> $things
 * @psalm-var non-empty-array<non-empty-string> $things
 */
$things =  ['example'];

echoThings(...map($things, static fn (string $thing) => $thing));
Psalm output (using commit aec0edc):

No issues!

psalm-github-bot[bot] avatar Jan 26 '23 12:01 psalm-github-bot[bot]

Simplified: https://psalm.dev/r/8d4e5c66da

weirdan avatar Jan 26 '23 16:01 weirdan

I found these snippets:

https://psalm.dev/r/8d4e5c66da
<?php

function echoThings(string $_first, string ...$_rest): void {}

/** @return list<non-empty-string> */
function mapList(): array { throw new Exception; }

$mapped = mapList();
/** @psalm-trace $mapped */;
echoThings(...$mapped);

// -------------------------------

/** @return non-empty-array<non-empty-string> */
function mapArray(): array { throw new Exception; }

$mapped = mapArray();
/** @psalm-trace $mapped */;
echoThings(...$mapped);
Psalm output (using commit aec0edc):

INFO: Trace - 9:28 - $mapped: list<non-empty-string>

ERROR: InvalidScalarArgument - 10:15 - Argument 1 of echoThings expects string, but possibly undefined non-empty-string provided

INFO: Trace - 18:28 - $mapped: non-empty-array<array-key, non-empty-string>

psalm-github-bot[bot] avatar Jan 26 '23 16:01 psalm-github-bot[bot]

This is fixed in https://github.com/vimeo/psalm/pull/10777

kkmuffme avatar Mar 27 '24 23:03 kkmuffme