Description
The analyzer raises a false-positive never-return error when a function returns the result of assert() on a generic type whose parameter is the intersection of two class-string<T> types.
mago appears to evaluate class-string<A> & class-string<B> as never.
I first hit this via the PSL Type\* builders (Psl\Type\intersection(Psl\Type\class_string(...), Psl\Type\class_string(...))->assert(...)), so it's possible a dedicated PSL handler is involved. However, the self-contained reproducer below uses only hand-rolled generics (no PSL), and reproduces the same error, which points at the core intersection evaluation of class-string types rather than anything PSL-specific.
Reproduction
Self-contained (no dependencies):
<?php
declare(strict_types=1);
namespace Repro;
/** @template T */
interface TypeInterface
{
/** @return T */
public function assert(mixed $value): mixed;
}
interface A {}
interface B {}
/**
* @template TFirst
* @template TSecond
*
* @param TypeInterface<TFirst> $first
* @param TypeInterface<TSecond> $second
*
* @return TypeInterface<TFirst&TSecond>
*/
function intersection(TypeInterface $first, TypeInterface $second): TypeInterface
{
throw new \RuntimeException('stub');
}
/**
* @template T
*
* @param class-string<T> $classname
*
* @return TypeInterface<class-string<T>>
*/
function class_string(string $classname): TypeInterface
{
throw new \RuntimeException('stub');
}
// FALSE POSITIVE: `never-return` reported on this `return`.
function getClass(): string
{
return intersection(
class_string(A::class),
class_string(B::class),
)->assert('x');
}
// Control case: a single class-string (no intersection) is NOT flagged.
function getSingleClass(): string
{
return class_string(A::class)->assert('x');
}
$ mago analyze never_return_intersection.php
never_return_intersection.php:51:12: error[never-return]: Cannot return value with type 'never' from this function.
= A 'never' return type indicates that a function is guaranteed to exit the script, throw an exception, or loop indefinitely. Code following a call to such a function is unreachable.
error: found 1 issues: 1 error(s)
Only getClass() (intersection of two class-strings) is flagged; the control getSingleClass() (single class-string) is not - which isolates the bug to the intersection.
PSL form that triggers the same error:
use Psl\Type;
interface A {}
interface B {}
function getClass(): string
{
return Type\intersection(
Type\class_string(A::class),
Type\class_string(B::class),
)->assert('x'); // error[never-return]
}
Expected behavior
class-string<A> & class-string<B> should be treated as a non-empty type (a class-string of a class implementing both A and B), so assert() returns that type rather than never, and the return should not be flagged.
Environment
- mago 1.30.0
- PHP 8.4
- macOS (aarch64)
Description
The analyzer raises a false-positive
never-returnerror when a function returns the result ofassert()on a generic type whose parameter is the intersection of twoclass-string<T>types.mago appears to evaluate
class-string<A> & class-string<B>asnever.I first hit this via the PSL
Type\*builders (Psl\Type\intersection(Psl\Type\class_string(...), Psl\Type\class_string(...))->assert(...)), so it's possible a dedicated PSL handler is involved. However, the self-contained reproducer below uses only hand-rolled generics (no PSL), and reproduces the same error, which points at the core intersection evaluation ofclass-stringtypes rather than anything PSL-specific.Reproduction
Self-contained (no dependencies):
Only
getClass()(intersection of twoclass-strings) is flagged; the controlgetSingleClass()(singleclass-string) is not - which isolates the bug to the intersection.PSL form that triggers the same error:
Expected behavior
class-string<A> & class-string<B>should be treated as a non-empty type (aclass-stringof a class implementing bothAandB), soassert()returns that type rather thannever, and thereturnshould not be flagged.Environment