From 4b4a1c517cbee8a36e12f70d7cc58ad85f783a4c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 26 Oct 2026 19:52:09 +0700 Subject: [PATCH] [Turbo] Fix crash cache due to PHPStan turbo --- .../Parameter/SimpleParameterProvider.php | 38 ++++++++++++++++++- .../ConfigurationFactoryTest.php | 29 ++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Configuration/Parameter/SimpleParameterProvider.php b/src/Configuration/Parameter/SimpleParameterProvider.php index e3aad337649..6a7203d0d9f 100644 --- a/src/Configuration/Parameter/SimpleParameterProvider.php +++ b/src/Configuration/Parameter/SimpleParameterProvider.php @@ -175,7 +175,7 @@ public static function hashForCacheInvalidation(): string ksort($strictParameters); - return sha1(serialize($strictParameters)); + return sha1(serialize(self::canonicalizeObjects($strictParameters, []))); } /** @@ -231,6 +231,42 @@ private static function relativizeProjectPaths(array $parameters, string $projec return $parameters; } + /** + * serialize() writes object properties in declaration order, which differs for the same class + * between runtimes: PHPStan Turbo replaces e.g. PHPStan\Type\MixedType with a native class + * declaring its properties in another order. The main process and the Turbo workers would then + * hash the same configuration differently and keep clearing each other's cache. + * + * @param array $visitedObjectIds + */ + private static function canonicalizeObjects(mixed $value, array $visitedObjectIds): mixed + { + if (is_array($value)) { + return array_map( + static fn (mixed $item): mixed => self::canonicalizeObjects($item, $visitedObjectIds), + $value + ); + } + + if (! is_object($value)) { + return $value; + } + + $objectId = spl_object_id($value); + if (isset($visitedObjectIds[$objectId])) { + return $value::class; + } + + $visitedObjectIds[$objectId] = true; + + $properties = (array) $value; + ksort($properties); + + return [ + $value::class => self::canonicalizeObjects($properties, $visitedObjectIds), + ]; + } + /** * Normalized to "/" separators, so the prefix compares alike with config paths on every platform. * Empty when the working directory cannot be resolved, which makes the relativizing above a diff --git a/tests/Configuration/ConfigurationFactoryTest.php b/tests/Configuration/ConfigurationFactoryTest.php index adae31167c1..fdd51ecd78f 100644 --- a/tests/Configuration/ConfigurationFactoryTest.php +++ b/tests/Configuration/ConfigurationFactoryTest.php @@ -10,6 +10,7 @@ use Rector\Console\ProcessConfigureDecorator; use Rector\FileSystem\FilesFinder; use Rector\Testing\PHPUnit\AbstractLazyTestCase; +use stdClass; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; @@ -20,6 +21,7 @@ protected function tearDown(): void SimpleParameterProvider::setParameter(Option::IS_RUN_NARROWED, false); SimpleParameterProvider::setParameter(Option::SOURCE, []); SimpleParameterProvider::setParameter(Option::PATHS, []); + SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, []); } public function test(): void @@ -93,6 +95,33 @@ public function testSourceParameterIsOutsideTheCacheInvalidationHash(): void $this->assertSame($hashForOnePath, SimpleParameterProvider::hashForCacheInvalidation()); } + public function testObjectPropertyOrderIsOutsideTheCacheInvalidationHash(): void + { + // PHPStan Turbo workers declare e.g. MixedType properties in another order than the main process, + // the same configured value must hash alike in both + $firstOrder = new stdClass(); + $firstOrder->isExplicitMixed = false; + $firstOrder->subtractedType = null; + + SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, [ + 'SomeRector' => [$firstOrder], + ]); + $firstOrderHash = SimpleParameterProvider::hashForCacheInvalidation(); + + $secondOrder = new stdClass(); + $secondOrder->subtractedType = null; + $secondOrder->isExplicitMixed = false; + + SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, [ + 'SomeRector' => [$secondOrder], + ]); + + $this->assertSame($firstOrderHash, SimpleParameterProvider::hashForCacheInvalidation()); + + $secondOrder->isExplicitMixed = true; + $this->assertNotSame($firstOrderHash, SimpleParameterProvider::hashForCacheInvalidation()); + } + /** * @param string[] $paths */