← All results

is_callable() must refine an array arm of array|callable to something that can actually be invoked

Group: Cross-analyzer regressions · Category: regression · File: regressions_is_callable_array_narrowing.php

Mago 1.49 left the array arm as array<array-key, mixed> after the guard. Three ways of consuming the narrowed value then disagreed, three lines apart: a direct call, a first-class callable, and Closure::fromCallable(). The unguarded call is the control. PHPStan rejects it; Psalm and mago stay silent on invoking array|callable even without the guard, so it is optional. Source: carthage-software/mago#2385 (fixed in 1.50.0). References: - PHP is_callable() - mago#2385

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 Pass

No diagnostics reported.

phpstan 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 Pass reported Lv.7+
regressions_is_callable_array_narrowing.php:47: Trying to invoke array<mixed>|(callable(): mixed) but it might not be a callable. [reported-from-level=7]
With strict-rules
regressions_is_callable_array_narrowing.php:47: Trying to invoke array<mixed>|(callable(): mixed) but it might not be a callable.
psalm 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c
next: 7.0.0-rc2
Pass

No diagnostics reported.

mago 1.57.0mago 1.57.0 Pass

No diagnostics reported.

mir 0.87.0mir 0.87.0 Pass

No diagnostics reported.

phpantom 0.11.1phpantom_lsp 0.11.1 Pass

No diagnostics reported.

intelephense 1.18.5intelephense 1.18.5 Pass

No diagnostics reported.

phpactor 2026.10.07.0Phpactor 2026.10.07.0 Pass

No diagnostics reported.

phpy 1.0.19409phpy 1.0.19409 Pass

No diagnostics reported.

qodana 262.10968.76PhpStorm 262.10968.76 Pass

No diagnostics reported.

noverify 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 Pass
regressions_is_callable_array_narrowing.php:37: not null safety call in function Conformance\Tests\RegressionsIsCallableArrayNarrowing\takesClosure signature of param callback when calling function [notNullSafetyFunctionArgumentFunctionCall]
regressions_is_callable_array_narrowing.php:38: not null safety call in function fromCallable signature of param callback [notNullSafetyFunctionArgumentVariable]
steins 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) Pass

No diagnostics reported.

phpstan-strict 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 Pass
regressions_is_callable_array_narrowing.php:47: Trying to invoke array<mixed>|(callable(): mixed) but it might not be a callable.
psalm-next 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 Pass

No diagnostics reported.

Source

<?php

declare(strict_types=1);

namespace Conformance\Tests\RegressionsIsCallableArrayNarrowing;

/**
 * `is_callable()` must refine an `array` arm of `array|callable` to something
 * that can actually be invoked.
 *
 * Mago 1.49 left the array arm as `array<array-key, mixed>` after the guard.
 * Three ways of consuming the narrowed value then disagreed, three lines apart:
 * a direct call, a first-class callable, and `Closure::fromCallable()`. The
 * unguarded call is the control. PHPStan rejects it; Psalm and mago stay
 * silent on invoking `array|callable` even without the guard, so it is
 * optional.
 *
 * Source: carthage-software/mago#2385 (fixed in 1.50.0).
 *
 * References:
 * - PHP `is_callable()`
 * - https://github.com/carthage-software/mago/issues/2385
 */

function takesClosure(\Closure $callback): void
{
    $callback();
}

/**
 * @param array<mixed>|callable $value
 */
function afterGuard(array|callable $value): void
{
    if (\is_callable($value)) {
        $value(); // Q: the array arm is callable after the guard
        takesClosure($value(...)); // Q?: first-class callable of the narrowed value
        takesClosure(\Closure::fromCallable($value)); // Q?: fromCallable of the narrowed value
    }
}

/**
 * @param array<mixed>|callable $value
 */
function beforeGuard(array|callable $value): void
{
    $value(); // E?: the array arm is not callable without the guard — Psalm and mago stay silent here
}