Skip to content

analyser: False positive never-return: intersection of two class-string<T> types is evaluated as never #1985

Description

@bendavies

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    c-analyzerPHP static analyzer and type error detectiont-bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions