@mixin (depth ≥ 2)Outer mixes in Middle, which mixes in Inner. Honouring the chain makes
Outer::answer() the int from Inner. A tool that only walks one hop
sees Middle with no answer() and routes the call through __call as
mixed. Depth one is already measured in phpdoc_advanced_mixin.
Source: vimeo/psalm#11868 (6.19.0), from Laravel empty query-builder contracts.
References:
- psalm#11868| 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 | Recognized (no probes) | No diagnostics reported.
|
| phpstan | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Enforced | No diagnostics reported.
|
| psalm | 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c next: 7.0.0-rc2 |
Enforced (pzoom≠) | pzoom (Psalm port)phpdoc_advanced_mixin_transitive.php:73: Argument 1 of takesInt cannot be mixed, expecting int [MixedArgument] phpdoc_advanced_mixin_transitive.php:73: Magic method Conformance\Tests\PhpdocAdvancedMixinTransitive\Outer::answer does not exist [UndefinedMagicMethod]
|
| mago | 1.57.0mago 1.57.0 | Enforced | No diagnostics reported.
|
| mir | 0.87.0mir 0.87.0 | Recognized (no probes) | No diagnostics reported.
|
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Recognized (no probes) | No diagnostics reported.
|
| intelephense | 1.18.5intelephense 1.18.5 | Not enforced | phpdoc_advanced_mixin_transitive.php:70: Expected type 'int'. Found 'mixed'. [P1006] phpdoc_advanced_mixin_transitive.php:73: Expected type 'int'. Found 'mixed'. [P1006]
|
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Recognized (no probes) | No diagnostics reported.
|
| phpy | 1.0.19409phpy 1.0.19409 | Recognized (no probes) | phpdoc_advanced_mixin_transitive.php:73: Method Outer::answer() is not defined. The call is passed to the magic Outer::__call() method.
|
| qodana | 262.10968.76PhpStorm 262.10968.76 | Recognized (no probes) | 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 | Recognized (no probes) | phpdoc_advanced_mixin_transitive.php:37: param $arguments miss matched with phpdoc type <<list<mixed>>> [funcParamTypeMissMatch] phpdoc_advanced_mixin_transitive.php:55: param $arguments miss matched with phpdoc type <<list<mixed>>> [funcParamTypeMissMatch]
|
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Recognized (no probes) | No diagnostics reported.
|
| phpstan-strict | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Enforced | No diagnostics reported.
|
| psalm-next | 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 | Enforced | No diagnostics reported.
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\PhpdocAdvancedMixinTransitive;
/**
* Transitive `@mixin` (depth ≥ 2).
*
* `Outer` mixes in `Middle`, which mixes in `Inner`. Honouring the chain makes
* `Outer::answer()` the `int` from `Inner`. A tool that only walks one hop
* sees `Middle` with no `answer()` and routes the call through `__call` as
* `mixed`. Depth one is already measured in `phpdoc_advanced_mixin`.
*
* Source: vimeo/psalm#11868 (6.19.0), from Laravel empty query-builder contracts.
*
* References:
* - https://github.com/vimeo/psalm/pull/11868
*/
final class Inner
{
public function answer(): int
{
return 42;
}
}
/**
* @mixin Inner
*/
final class Middle // T: @mixin
{
/**
* @param list<mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed // E?[noise]: some tools flag the docblock/native array mismatch
{
if ($name === 'answer') {
return (new Inner())->answer();
}
throw new \BadMethodCallException($name);
}
}
/**
* @mixin Middle
*/
final class Outer // T: @mixin
{
/**
* @param list<mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed // E?[noise]: some tools flag the docblock/native array mismatch
{
if ($name === 'answer') {
return (new Middle())->answer();
}
throw new \BadMethodCallException($name);
}
}
function takesInt(int $value): void
{
}
// Depth one, for contrast: Middle declares @mixin Inner.
takesInt((new Middle())->answer()); // Q?<phpstan> // Q?<phpstan-strict> // Q?<psalm> // Q?<pzoom> // Q?<mago> // Q?<intelephense> // E?[noise]
// Depth two: Outer only declares @mixin Middle.
takesInt((new Outer())->answer()); // Q?<phpstan> // Q?<phpstan-strict> // Q?<psalm> // Q?<pzoom> // Q?<mago> // Q?<intelephense> // E?[noise]