← All results

Loose comparison against a non-numeric string literal narrows a key union

Group: Assertions and narrowing · Category: ecosystem · File: assertions_key_loose_narrowing.php

After $key == 'foo' on 'foo'|'bar'|int, $key is 'foo'. The E probe is a value the original union admits ('bar') that the narrowed type excludes — a hit on takesInt would fire even without narrowing, because the union still contains 'foo'|'bar'. Source: vimeo/psalm#12153 (7.0.0-rc2). References: - psalm#12153

Analyzer results

AnalyzerVersionResultDiagnostics
phan 6.0.11Phan 6.0.11 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 Pass
assertions_key_loose_narrowing.php:56: Argument 1 ($value) is $key of type int but \Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo() takes string defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/assertions_key_loose_narrowing.php:24 [PhanTypeMismatchArgument]
phpstan 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 Fail reported Lv.7+
assertions_key_loose_narrowing.php:41: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given. [reported-from-level=7]
assertions_key_loose_narrowing.php:55: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesInt expects int, int|string given. [reported-from-level=7]
assertions_key_loose_narrowing.php:56: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given. [reported-from-level=7]
With strict-rules
assertions_key_loose_narrowing.php:40: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:41: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given.
assertions_key_loose_narrowing.php:51: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:51: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:55: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesInt expects int, int|string given.
assertions_key_loose_narrowing.php:56: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given.
Expectation diff
Line 41: Expected silence (quiet probe), got ["Parameter #1 $value of function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesFoo expects 'foo', 'bar'|'foo'|int given. [reported-from-level=7]"]
Line 55: Expected silence (quiet probe), got ["Parameter #1 $value of function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesInt expects int, int|string given. [reported-from-level=7]"]
psalm 6.20.9Psalm 6.20.9@f67170ac3c3b931f5088681e21d6c8babc601c08
next: 7.0.4
Pass
assertions_key_loose_narrowing.php:42: 'bar' does not contain 'foo' [DocblockTypeContradiction]
assertions_key_loose_narrowing.php:42: Docblock-defined type 'foo' for $key is never =string(bar) [DocblockTypeContradiction]
assertions_key_loose_narrowing.php:42: Docblock-defined type 'foo' for $key is never =string(bar) [RedundantConditionGivenDocblockType]
assertions_key_loose_narrowing.php:56: Argument 1 of Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', but int provided [InvalidArgument]
mago 1.59.0mago 1.59.0 Pass
assertions_key_loose_narrowing.php:42: Redundant `===` comparison: left-hand side is never identical to right-hand side. [redundant-comparison]
assertions_key_loose_narrowing.php:42: Redundant ternary operator: condition is always falsy. [impossible-condition]
assertions_key_loose_narrowing.php:56: Invalid argument type for argument #1 of `Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo`: expected `string('foo')`, but found `int`. [invalid-argument]
mir 0.91.0mir 0.91.0 Fail
assertions_key_loose_narrowing.php:41: PossiblyInvalidArgument: Argument $value of takesFoo() expects '"foo"', possibly different type '"foo"|"bar"|int' provided [MIR0105]
assertions_key_loose_narrowing.php:55: PossiblyInvalidArgument: Argument $value of takesInt() expects 'int', possibly different type '"foo"|"bar"|int' provided [MIR0105]
assertions_key_loose_narrowing.php:56: PossiblyInvalidArgument: Argument $value of takesFoo() expects '"foo"', possibly different type '"foo"|"bar"|int' provided [MIR0105]
Expectation diff
Line 41: Expected silence (quiet probe), got ["PossiblyInvalidArgument: Argument $value of takesFoo() expects '\"foo\"', possibly different type '\"foo\"|\"bar\"|int' provided [MIR0105]"]
Line 55: Expected silence (quiet probe), got ["PossiblyInvalidArgument: Argument $value of takesInt() expects 'int', possibly different type '\"foo\"|\"bar\"|int' provided [MIR0105]"]
phpantom 0.11.1phpantom_lsp 0.11.1 Fail
assertions_key_loose_narrowing.php:41: Argument 1 ($value) expects 'foo', got 'foo'|int (int does not satisfy 'foo') [type_mismatch_argument]
assertions_key_loose_narrowing.php:56: Argument 1 ($value) expects 'foo', got int [type_mismatch_argument]
Expectation diff
Line 41: Expected silence (quiet probe), got ["Argument 1 ($value) expects 'foo', got 'foo'|int (int does not satisfy 'foo') [type_mismatch_argument]"]
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.19490phpy 1.0.19490 Pass

No diagnostics reported.

qodana 262.11879.104PhpStorm 262.11879.104 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 Fail
assertions_key_loose_narrowing.php:41: potentially not safe call in function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo signature of param value [notSafeCall]
assertions_key_loose_narrowing.php:55: potentially not safe call in function Conformance\Tests\AssertionsKeyLooseNarrowing\takesInt signature of param value [notSafeCall]
assertions_key_loose_narrowing.php:56: potentially not safe call in function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo signature of param value [notSafeCall]
Expectation diff
Line 41: Expected silence (quiet probe), got ["potentially not safe call in function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesFoo signature of param value [notSafeCall]"]
Line 55: Expected silence (quiet probe), got ["potentially not safe call in function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesInt signature of param value [notSafeCall]"]
steins 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) Pass

No diagnostics reported.

phpstan-strict 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 Fail
assertions_key_loose_narrowing.php:40: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:41: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given.
assertions_key_loose_narrowing.php:51: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:51: Loose comparison via "==" between int|string and string is not allowed.
assertions_key_loose_narrowing.php:55: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesInt expects int, int|string given.
assertions_key_loose_narrowing.php:56: Parameter #1 $value of function Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', 'bar'|'foo'|int given.
Expectation diff
Line 41: Expected silence (quiet probe), got ["Parameter #1 $value of function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesFoo expects 'foo', 'bar'|'foo'|int given."]
Line 55: Expected silence (quiet probe), got ["Parameter #1 $value of function Conformance\\Tests\\AssertionsKeyLooseNarrowing\\takesInt expects int, int|string given."]
Line 40: Unexpected errors ["Loose comparison via \"==\" between int|string and string is not allowed."]
Line 51: Unexpected errors ["Loose comparison via \"==\" between int|string and string is not allowed.","Loose comparison via \"==\" between int|string and string is not allowed."]
psalm-next 7.0.4Psalm 7.0.4@fa5b1f6e665cb743838ffd865b8fc96207f9409f Pass
assertions_key_loose_narrowing.php:42: 'bar' does not contain 'foo' [DocblockTypeContradiction]
assertions_key_loose_narrowing.php:42: Docblock-defined type 'foo' for $key is never =string(bar) [DocblockTypeContradiction]
assertions_key_loose_narrowing.php:42: Docblock-defined type 'foo' for $key is never =string(bar) [RedundantConditionGivenDocblockType]
assertions_key_loose_narrowing.php:56: Argument 1 of Conformance\Tests\AssertionsKeyLooseNarrowing\takesFoo expects 'foo', but int provided [InvalidArgument]

Source

<?php

declare(strict_types=1);

namespace Conformance\Tests\AssertionsKeyLooseNarrowing;

/**
 * Loose comparison against a non-numeric string literal narrows a key union.
 *
 * After `$key == 'foo'` on `'foo'|'bar'|int`, `$key` is `'foo'`. The E probe
 * is a value the original union admits (`'bar'`) that the narrowed type
 * excludes — a hit on `takesInt` would fire even without narrowing, because
 * the union still contains `'foo'|'bar'`.
 *
 * Source: vimeo/psalm#12153 (7.0.0-rc2).
 *
 * References:
 * - https://github.com/vimeo/psalm/pull/12153
 */

/**
 * @param 'foo' $value
 */
function takesFoo($value): void
{
}

/**
 * @param int $value
 */
function takesInt($value): void
{
}

/**
 * @param 'foo'|'bar'|int $key
 */
function afterLooseFoo(string|int $key): void
{
    if ($key == 'foo') {
        takesFoo($key); // Q: == 'foo' narrows the union to 'foo'
        echo $key === 'bar' ? 'bar' : 'foo'; // E?: after == 'foo', === 'bar' is always false
    }
}

/**
 * @param 'foo'|'bar'|int $key
 */
function remainingIsInt(string|int $key): void
{
    if ($key == 'foo' || $key == 'bar') {
        return;
    }

    takesInt($key); // Q: the remaining case is int
    takesFoo($key); // E?: int is not 'foo'
}