Repository navigation
System error "array_merge(): Argument #2 must be of type array, PHPStan\Type\ClosureType given" in parallel workers since PHPStan Turbo (#8812) #9944
Description
Bug Report
| Subject | Details |
|---|---|
| Rector version | 2.8.1 (also 2.8.0). Last good: dev-main afc60fc9 (between 2.7.0 and 2.8.0) |
| PHP | 8.5.11 NTS, Linux x86_64 (Debian 12, glibc 2.36) |
| PHPStan | 2.3.2, with the bundled turbo-ext/linux-gnu-x86_64/phpstan_turbo-8.5.so |
| Config | ->withParallel(maxNumberOfProcess: 8), RemoveUnusedPrivateMethodRector (dead code set) |
Since 2.8.0, every parallel run on our Laravel application (~14.6k PHP files) fails on the same three files:
[ERROR] Could not process ".../FaxChannel.php" file, due to:
"System error: "array_merge(): Argument #2 must be of type array, PHPStan\Type\ClosureType given"
Run Rector with "--debug" option and post the report here: https://github.com/rectorphp/rector/issues/new". On line: 59
I bisected the rectorphp/rector builds between afc60fc9 and 2.8.0. The errors start with the build of rectorphp/rector-src 70e25dc1, "Run the workers with PHPStan Turbo" (#8812):
| rectorphp/rector build | rector-src | Errors |
|---|---|---|
afc60fc9 (dev-main we used before updating) |
1133b774 |
0 |
9a730780 (last good) |
2c5dcce0 |
0 |
d3ed9390 (first bad) |
70e25dc1 |
3 |
| 2.8.0, 2.8.1 | 3 |
Every build was run with the same vendor/, PHPStan 2.3.2 and driftingly/rector-laravel 2.7.1, with an empty cache.
What I narrowed down
- Hiding
vendor/phpstan/phpstan/turbo-ext/, so that the workers start without the extension, makes the errors go away on a bad build. - Commenting out
TurboExtensionEnabler::trustOwnTypesIfSuitable()inbin/rector.phpdoes not; only the extension being active matters. - It does not happen when the three files are processed on their own, or in a full single-process
--debugrun with the extension loaded through-d extension=. It seems to depend on what a long-running worker has processed before.
Rector only shows the trace with --debug, which disables parallel, so I patched resolveSystemError() to always include it:
System error: "array_merge(): Argument #2 must be of type array, PHPStan\Type\ClosureType given"
vendor/rector/rector/src/NodeTypeResolver/NodeTypeResolver/StaticCallMethodCallTypeResolver.php:59
#0 .../StaticCallMethodCallTypeResolver.php(59): PHPStan\Analyser\MutatingScope->getType(Object(PhpParser\Node\Expr\MethodCall))
#1 .../NodeTypeResolver.php(433): ...StaticCallMethodCallTypeResolver->resolve(Object(PhpParser\Node\Expr\MethodCall))
#2 .../NodeTypeResolver.php(198): ...NodeTypeResolver->resolveByNodeTypeResolvers(Object(PhpParser\Node\Expr\MethodCall))
#3 rules/DeadCode/NodeAnalyzer/CallCollectionAnalyzer.php(41): ...NodeTypeResolver->getType(Object(PhpParser\Node\Expr\MethodCall))
#4 rules/DeadCode/NodeAnalyzer/IsClassMethodUsedAnalyzer.php(110): ...CallCollectionAnalyzer->isExists(Array, 'cacheKey', '...')
#5 rules/DeadCode/NodeAnalyzer/IsClassMethodUsedAnalyzer.php(80): ...IsClassMethodUsedAnalyzer->isClassMethodCalledInLocalMethodCall(Object(PhpParser\Node\Stmt\Class_), 'cacheKey')
#6 rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodRector.php(129): ...IsClassMethodUsedAnalyzer->isClassMethodUsed(...)
#7 src/Rector/RectorRunner.php(60): ...RemoveUnusedPrivateMethodRector->refactor(Object(PhpParser\Node\Stmt\Class_))
The TypeError is reported at Rector's call to MutatingScope::getType() (frame #0), with no PHP frames below it. That points to the native code in the extension rather than to Rector. In all three files the call is a private-method call on $this ($this->cacheKey($user) used as an array key in a ??= assignment, and $this->getMail(...)).
Minimal PHP Code Causing Issue
I don't have a minimal reproduction; this only shows up partway through a full parallel run on our codebase. I'm happy to run a build or a branch that prints more diagnostics.
Expected Behaviour
No system errors with the Turbo extension, or a way to opt the workers out of it until it is fixed.
Activity
samsonasik commentedon Oct 27, 2026
Turbo extension usage is new, I guess the option
--disable-turbois needed, but should be better to have it actually reproduced and fixed.TomasVotruba commentedon Oct 27, 2026
Could you create a minimal reproducer test via PR? Can be unit test or e2e tests.
I want to avoid blind fixes.
calebdw commentedon Oct 28, 2026
I got to the bottom of this one: it is phpstan/phpstan-src#6852 ("Keep the expression result alive in the native ReturnHandler"), released in PHPStan 2.3.3 earlier today. The native
ReturnHandlerin the Turbo extension released the returned expression's result before the node callbacks ran, so a latergetType()on that expression could read freed memory, and an unrelatedClosureTypeended up where the impure points array should be. Rector callsScope::getType()on nodes after PHPStan's traversal, which is exactly that pattern, and it explains why the error depended on what a worker had processed before.What I checked:
Minimal case, reduced from our project: it needs a worker with Turbo loaded,
featureToggles.unresolvedTemplateArguments: true(from bleeding edge),RemoveUnusedPrivateMethodRector, and a non-arrow closure that returns a method call, passed to a generic method:With
->parallel(maxNumberOfProcess: 1), Rector 2.8.1 and PHPStan 2.3.2 fail onConsumer.phpwith thearray_merge()error. Hidingvendor/phpstan/phpstan/turbo-ext/or turning the feature toggle off makes it pass.The same case passes with PHPStan 2.3.3, and as an e2e fixture on rector-src
main(which locks 2.3.3).Our full project on Rector 2.8.1 with PHPStan 2.3.3: 0 errors, previously 3 on every run.
So there is nothing left to fix in Rector itself, and a reproducer PR would pass on
mainalready. One suggestion:rector/rectorcurrently requiresphpstan/phpstan: ^2.3.2. Raising that to^2.3.3would stop projects that already lock 2.3.2 from picking up the Turbo workers together with this bug.Closing, since it is fixed by PHPStan 2.3.3.