← All results

Custom @assert-if-true predicate narrowing a string to a string subtype

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

A user-defined boolean predicate annotated to narrow its argument to non-empty-string should let a following call that requires non-empty-string type-check. Analyzers diverge on whether they honor the (non-vendor-prefixed) @assert-if-true form and propagate the narrowed string subtype through the guarded branch. Source lead: Mago analyzer test reconcile_non_empty_string (crates/analyzer/tests/cases). Mago narrows and accepts the guarded call; this file records every tool's verdict on the same construct.

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 Pass

No diagnostics reported.

phpstan 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 Pass reported Lv.7+
regressions_string_narrowing_assert_if_true.php:37: Parameter #1 $s of function Conformance\Tests\RegressionsStringNarrowingAssertIfTrue\takesNonEmpty expects non-empty-string, string given. [identifier=argument.type] [reported-from-level=7]
With strict-rules
regressions_string_narrowing_assert_if_true.php:37: Parameter #1 $s of function Conformance\Tests\RegressionsStringNarrowingAssertIfTrue\takesNonEmpty expects non-empty-string, string given. [identifier=argument.type]
psalm 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
Pass (pzoom≠)
regressions_string_narrowing_assert_if_true.php:37: Argument 1 of Conformance\Tests\RegressionsStringNarrowingAssertIfTrue\takesNonEmpty expects non-empty-string, but parent type string provided [ArgumentTypeCoercion]
pzoom (Psalm port)

No diagnostics from pzoom.

mago 1.46.0mago 1.46.0 Pass

No diagnostics reported.

mir 0.70.1mir 0.70.1 Pass

No diagnostics reported.

phpantom 0.9.0phpantom_lsp 0.9.0 Pass

No diagnostics reported.

intelephense 1.18.5intelephense 1.18.5 Pass

No diagnostics reported.

phpy 0.2.0phpy 0.2.0 Pass

No diagnostics reported.

qodana 262.8665.325Qodana 262.8665.325 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_string_narrowing_assert_if_true.php:29: param $s miss matched with phpdoc type <<non-empty-string>> [funcParamTypeMissMatch]
steins 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) Pass

No diagnostics reported.

phpstan-strict 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 Pass
regressions_string_narrowing_assert_if_true.php:37: Parameter #1 $s of function Conformance\Tests\RegressionsStringNarrowingAssertIfTrue\takesNonEmpty expects non-empty-string, string given. [identifier=argument.type]
psalm-next 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 Pass
regressions_string_narrowing_assert_if_true.php:37: Argument 1 of Conformance\Tests\RegressionsStringNarrowingAssertIfTrue\takesNonEmpty expects non-empty-string, but parent type string provided [ArgumentTypeCoercion]

Source

<?php

declare(strict_types=1);

namespace Conformance\Tests\RegressionsStringNarrowingAssertIfTrue;

/**
 * Custom @assert-if-true predicate narrowing a string to a string subtype.
 *
 * A user-defined boolean predicate annotated to narrow its argument to `non-empty-string`
 * should let a following call that requires `non-empty-string` type-check. Analyzers
 * diverge on whether they honor the (non-vendor-prefixed) `@assert-if-true` form and
 * propagate the narrowed string subtype through the guarded branch.
 *
 * Source lead: Mago analyzer test `reconcile_non_empty_string`
 * (crates/analyzer/tests/cases). Mago narrows and accepts the guarded call; this file
 * records every tool's verdict on the same construct.
 */

/**
 * @assert-if-true non-empty-string $s
 */
function isNonEmpty(string $s): bool
{
    return $s !== '';
}

/** @param non-empty-string $s */
function takesNonEmpty(string $s): void // E<noverify>: NoVerify cannot evaluate the non-empty-string param type and flags the signature itself
{
    echo $s;
}

function caller(string $s): void
{
    if (isNonEmpty($s)) {
        takesNonEmpty($s); // E<phpstan>: does not narrow $s to non-empty-string through the bare @assert-if-true predicate, so reports argument.type // E<phpstan-strict>: same coercion under strict rules // E<psalm>: reports ArgumentTypeCoercion; parent type stays string. Mago and Phan honor the assertion and stay clean
    }
}