Skip to content

System error "array_merge(): Argument #2 must be of type array, PHPStan\Type\ClosureType given" in parallel workers since PHPStan Turbo (#8812) #9944

Closed

Description

@calebdw

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() in bin/rector.php does 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 --debug run 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

  1. samsonasik commented on Oct 27, 2026

    @samsonasik
    Member

    Turbo extension usage is new, I guess the option --disable-turbo is needed, but should be better to have it actually reproduced and fixed.

  2. TomasVotruba commented on Oct 27, 2026

    @TomasVotruba
    Member

    Could you create a minimal reproducer test via PR? Can be unit test or e2e tests.
    I want to avoid blind fixes.

  3. calebdw commented on Oct 28, 2026

    @calebdw
    Author

    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 ReturnHandler in the Turbo extension released the returned expression's result before the node callbacks ran, so a later getType() on that expression could read freed memory, and an unrelated ClosureType ended up where the impure points array should be. Rector calls Scope::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:

      /** @template TValue */
      final class Box
      {
          /** @param list<TValue> $items */
          public function __construct(private array $items) {}
      
          /**
           * @template TReturn
           * @param callable(TValue): list<TReturn> $callback
           * @return self<TReturn>
           */
          public function flatMap(callable $callback): self
          {
              return new self(array_merge(...array_map($callback, $this->items)));
          }
      
          /** @return list<TValue> */
          public function all(): array
          {
              return $this->items;
          }
      }
      
      final class Consumer
      {
          public function run(): array
          {
              return (new Box([1]))->flatMap(function (int $e): array {
                  return (new Box([$e]))->all();
              })->all();
          }
      
          private function unused(): void
          {
          }
      }

      With ->parallel(maxNumberOfProcess: 1), Rector 2.8.1 and PHPStan 2.3.2 fail on Consumer.php with the array_merge() error. Hiding vendor/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 main already. One suggestion: rector/rector currently requires phpstan/phpstan: ^2.3.2. Raising that to ^2.3.3 would 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions