Repository: PHP-CS-Fixer/PHP-CS-Fixer (php)

Issue:
`ParallelConfigFactory` using named parameters behaves unexpectedly when omiting first argument
### Problem description

```php
$config->setParallelConfig(ParallelConfigFactory::detect(processTimeout: 300));
```

:point_up: the above code will create unexpected parallel config values. `$processTimeout` argument will be used as number of files per process instead. This is due to `array_filter` removing "falsy" values from argument list.

```php
return new ParallelConfig(
    ...array_filter(
        [self::$cpuDetector->getCount(), $filesPerProcess, $processTimeout],
        static fn ($value): bool => null !== $value
    )
);
```

### Minimal reproducer

```php
$config = new \PhpCsFixer\Config();
$config->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect(processTimeout: 300));
```
or
```php
$config = new \PhpCsFixer\Config();
$config->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect(null, 300));
```

### Fixer version

3.57.1

### PHP version

8.3

### How do you run Fixer?

Composer package

### Contribution Checks

- [X] I have verified if this problem was already reported
- [X] I am familiar with ["Feature or bug?"](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/feature-or-bug.rst)

Pull request description:
fix: Support named args in `ParallelConfigFactory::detect()`
Fixes #8025

Patch:
diff --git a/src/Runner/Parallel/ParallelConfig.php b/src/Runner/Parallel/ParallelConfig.php
index 2cfa9acef..7ccf9fdf6 100644
--- a/src/Runner/Parallel/ParallelConfig.php
+++ b/src/Runner/Parallel/ParallelConfig.php
@@ -19,8 +19,11 @@
  */
 final class ParallelConfig
 {
-    private const DEFAULT_FILES_PER_PROCESS = 10;
-    private const DEFAULT_PROCESS_TIMEOUT = 120;
+    /** @internal */
+    public const DEFAULT_FILES_PER_PROCESS = 10;
+
+    /** @internal */
+    public const DEFAULT_PROCESS_TIMEOUT = 120;
 
     private int $filesPerProcess;
     private int $maxProcesses;
diff --git a/src/Runner/Parallel/ParallelConfigFactory.php b/src/Runner/Parallel/ParallelConfigFactory.php
index cd6a56be6..162cf4374 100644
--- a/src/Runner/Parallel/ParallelConfigFactory.php
+++ b/src/Runner/Parallel/ParallelConfigFactory.php
@@ -48,10 +48,9 @@ public static function detect(
         }
 
         return new ParallelConfig(
-            ...array_filter(
-                [self::$cpuDetector->getCount(), $filesPerProcess, $processTimeout],
-                static fn ($value): bool => null !== $value
-            )
+            self::$cpuDetector->getCount(),
+            $filesPerProcess ?? ParallelConfig::DEFAULT_FILES_PER_PROCESS,
+            $processTimeout ?? ParallelConfig::DEFAULT_PROCESS_TIMEOUT
         );
     }
 }


Test patch:
diff --git a/tests/Runner/Parallel/ParallelConfigFactoryTest.php b/tests/Runner/Parallel/ParallelConfigFactoryTest.php
index 720b7fa8b..3c62fd468 100644
--- a/tests/Runner/Parallel/ParallelConfigFactoryTest.php
+++ b/tests/Runner/Parallel/ParallelConfigFactoryTest.php
@@ -16,6 +16,7 @@
 
 use Fidry\CpuCoreCounter\CpuCoreCounter;
 use Fidry\CpuCoreCounter\Finder\DummyCpuCoreFinder;
+use PhpCsFixer\Runner\Parallel\ParallelConfig;
 use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
 use PhpCsFixer\Tests\TestCase;
 
@@ -48,8 +49,8 @@ public function testDetectConfigurationWithoutParams(): void
         $config = ParallelConfigFactory::detect();
 
         self::assertSame(7, $config->getMaxProcesses());
-        self::assertSame(10, $config->getFilesPerProcess());
-        self::assertSame(120, $config->getProcessTimeout());
+        self::assertSame(ParallelConfig::DEFAULT_FILES_PER_PROCESS, $config->getFilesPerProcess());
+        self::assertSame(ParallelConfig::DEFAULT_PROCESS_TIMEOUT, $config->getProcessTimeout());
 
         $cpuDetector->setValue($parallelConfigFactoryReflection, null);
     }
@@ -61,4 +62,39 @@ public function testDetectConfigurationWithParams(): void
         self::assertSame(22, $config->getFilesPerProcess());
         self::assertSame(2_200, $config->getProcessTimeout());
     }
+
+    public function testDetectConfigurationWithDefaultValue(): void
+    {
+        $config = ParallelConfigFactory::detect(null, 60);
+
+        self::assertSame(ParallelConfig::DEFAULT_FILES_PER_PROCESS, $config->getFilesPerProcess());
+        self::assertSame(60, $config->getProcessTimeout());
+    }
+
+    /**
+     * @requires PHP 8.0
+     */
+    public function testDetectConfigurationWithNamedArgs(): void
+    {
+        // First argument omitted, second one provided via named argument
+        $config1 = \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['processTimeout' => 300]);
+
+        self::assertSame(ParallelConfig::DEFAULT_FILES_PER_PROCESS, $config1->getFilesPerProcess());
+        self::assertSame(300, $config1->getProcessTimeout());
+
+        // Flipped order of arguments using named arguments syntax
+        $config2 = \call_user_func_array([ParallelConfigFactory::class, 'detect'], [
+            'processTimeout' => 300,
+            'filesPerProcess' => 5,
+        ]);
+
+        self::assertSame(5, $config2->getFilesPerProcess());
+        self::assertSame(300, $config2->getProcessTimeout());
+
+        // Only first argument provided, but via named argument
+        $config3 = \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['filesPerProcess' => 7]);
+
+        self::assertSame(7, $config3->getFilesPerProcess());
+        self::assertSame(ParallelConfig::DEFAULT_PROCESS_TIMEOUT, $config3->getProcessTimeout());
+    }
 }