int + int is int; any float operand makes the result float.
Tools that expand both layers accept add(1, 2) as int and reject
add(1, 1.5) as int. Tools that fall back to the native int|float
reject the valid control as well.
References:
- PHPStan 1.6.0: nested ($a is int ? ($b is int ? int : float) : float) // E<noverify>: NoVerify reads $left in the condition as a class name
- PHPStan phpdoc-types: Conditional return types
- Psalm conditional_types.md: Nested conditionals
- Intelephense Type-System.md: Conditional Return Type| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Enforced | conditionals_nested_return.php:48: Argument 1 ($value) is add(1, 1.5) of type float but \Conformance\Tests\ConditionalsNestedReturn\takesInt() takes int defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/conditionals_nested_return.php:36 [PhanTypeMismatchArgument] conditionals_nested_return.php:49: Argument 1 ($value) is add(1.5, 1) of type float but \Conformance\Tests\ConditionalsNestedReturn\takesInt() takes int defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/conditionals_nested_return.php:36 [PhanTypeMismatchArgument]
|
| phpstan | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Enforced reported Lv.5+ | conditionals_nested_return.php:48: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given. [reported-from-level=5] conditionals_nested_return.php:49: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given. [reported-from-level=5] With strict-rulesconditionals_nested_return.php:48: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given. conditionals_nested_return.php:49: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given.
|
| psalm | 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c next: 7.0.0-rc2 |
Enforced | conditionals_nested_return.php:48: Argument 1 of Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, but float provided [InvalidScalarArgument] conditionals_nested_return.php:49: Argument 1 of Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, but float provided [InvalidScalarArgument]
|
| mago | 1.57.0mago 1.57.0 | Enforced | conditionals_nested_return.php:48: Possible argument type mismatch for argument #1 of `Conformance\Tests\ConditionalsNestedReturn\takesInt`: expected `int`, but possibly received `float`. [possibly-invalid-argument] conditionals_nested_return.php:49: Possible argument type mismatch for argument #1 of `Conformance\Tests\ConditionalsNestedReturn\takesInt`: expected `int`, but possibly received `float`. [possibly-invalid-argument]
|
| mir | 0.87.0mir 0.87.0 | Not enforced | No diagnostics reported.
|
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Enforced | conditionals_nested_return.php:48: Argument 1 ($value) expects int, got float [type_mismatch_argument] conditionals_nested_return.php:49: Argument 1 ($value) expects int, got float [type_mismatch_argument]
|
| intelephense | 1.18.5intelephense 1.18.5 | Enforced | conditionals_nested_return.php:48: Expected type 'int'. Found 'float'. [P1006] conditionals_nested_return.php:49: Expected type 'int'. Found 'float'. [P1006]
|
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Not enforced | No diagnostics reported.
|
| phpy | 1.0.19409phpy 1.0.19409 | Not enforced | No diagnostics reported.
|
| qodana | 262.10968.76PhpStorm 262.10968.76 | Widened to int|float | No diagnostics reported.
Notes: The conditional is not evaluated: `add(1, 2)` and `add(1, 1.5)` are both inferred as `float|int`, the union of its branches, and that union passes the `int` parameter because PhpStorm accepts a union wherever one of its members fits. |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Partly enforced (1/3) | conditionals_nested_return.php:16: Class or interface named \Conformance\Tests\ConditionalsNestedReturn\$left does not exist [undefinedClass]
|
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Not enforced | No diagnostics reported.
|
| phpstan-strict | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Enforced | conditionals_nested_return.php:48: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given. conditionals_nested_return.php:49: Parameter #1 $value of function Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, float given.
|
| psalm-next | 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 | Enforced | conditionals_nested_return.php:48: Argument 1 of Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, but float provided [InvalidScalarArgument] conditionals_nested_return.php:49: Argument 1 of Conformance\Tests\ConditionalsNestedReturn\takesInt expects int, but float provided [InvalidScalarArgument]
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\ConditionalsNestedReturn;
/**
* Nested conditional return type.
*
* `int + int` is `int`; any float operand makes the result `float`.
* Tools that expand both layers accept `add(1, 2)` as `int` and reject
* `add(1, 1.5)` as `int`. Tools that fall back to the native `int|float`
* reject the valid control as well.
*
* References:
* - PHPStan 1.6.0: nested `($a is int ? ($b is int ? int : float) : float)` // E<noverify>: NoVerify reads $left in the condition as a class name
* - PHPStan phpdoc-types: Conditional return types
* - Psalm conditional_types.md: Nested conditionals
* - Intelephense Type-System.md: Conditional Return Type
*/
/**
* @param int|float $left
* @param int|float $right
* @return ($left is int ? ($right is int ? int : float) : float)
*/
function add(int|float $left, int|float $right): int|float // T: ($left is int ? ($right is int ? int : float) : float)
{
if (is_int($left) && is_int($right)) {
return $left + $right;
}
return 0.5;
}
function takesInt(int $value): void
{
}
function takesFloat(float $value): void
{
}
takesInt(add(1, 2)); // V: int + int is int
takesFloat(add(1, 1.5)); // V: int + float is float
takesFloat(add(1.5, 1)); // V: float + int is float
takesInt(add(1, 1.5)); // E?: int + float is float, not int
takesInt(add(1.5, 1)); // E?: float + int is float, not int