From bbf4f1a74c295303514e173b85ba48d0d3cbcfa6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 24 Sep 2026 08:11:04 +0200 Subject: [PATCH 1/3] Add experimental --turbo option running the reco Go binary --- README.md | 2 + docs/turbo.md | 39 ++++++++++++++++++++ src/Console/Command/CheckCommand.php | 11 ++++++ src/Turbo/RecoBinaryLocator.php | 31 ++++++++++++++++ src/Turbo/TurboRunner.php | 53 +++++++++++++++++++++++++++ tests/Turbo/RecoBinaryLocatorTest.php | 34 +++++++++++++++++ tests/Turbo/TurboRunnerTest.php | 33 +++++++++++++++++ 7 files changed, 203 insertions(+) create mode 100644 docs/turbo.md create mode 100644 src/Turbo/RecoBinaryLocator.php create mode 100644 src/Turbo/TurboRunner.php create mode 100644 tests/Turbo/RecoBinaryLocatorTest.php create mode 100644 tests/Turbo/TurboRunnerTest.php diff --git a/README.md b/README.md index be431afa9d..6b4dbaed2b 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,5 @@ composer lint.fix # auto-fix rector + coding standard ``` Please make sure `composer lint` and `composer test` pass before sending a pull request. + +Experimental [`--turbo` mode](docs/turbo.md) runs the reco Go binary instead of the PHP engine. diff --git a/docs/turbo.md b/docs/turbo.md new file mode 100644 index 0000000000..b5b5c88fd9 --- /dev/null +++ b/docs/turbo.md @@ -0,0 +1,39 @@ +# Turbo mode (experimental) + +`--turbo` hands the run over to the [`reco`](https://github.com/TomasVotruba/reco) Go binary instead of the PHP engine: + +```bash +vendor/bin/ecs check --turbo # report only (like a normal check) +vendor/bin/ecs check --turbo --fix # rewrite files in place +vendor/bin/ecs check src --turbo # limit to a path - pass the path before the flag +``` + +Pass any explicit path before the flags (`check src --turbo`), as with `--fix`; a path written after a boolean flag is consumed as that flag's value. + +ECS resolves the paths from `ecs.php` (and any paths passed on the command line) exactly as usual, then invokes reco over them. The check/fix split maps onto reco directly: + +- `--turbo` runs `reco run --dry-run ` - reports without writing. +- `--turbo --fix` runs `reco run ` - rewrites in place. + +The exit code of `reco` is passed through. + +## Binary resolution + +The reco binary is looked up in this order: + +1. the `ECS_TURBO_BIN` environment variable, if it points to an existing file; +2. `vendor/bin/reco`, if present; +3. `reco` on the `PATH`. + +## Prototype caveat + +This is an RFC-stage prototype. In turbo mode reco runs **its own native rule set**, not the sniffs and fixers configured in `ecs.php`. The rule selection in your config is ignored for now - only the paths are shared. Do not treat a turbo run as equivalent to a normal ECS run yet. + +## Remaining delivery work + +For `--turbo` to work out of the box, the reco binary has to be installed alongside ECS. The planned path: + +- ship reco as downloadable per-OS binaries via goreleaser releases; +- add a Composer post-install step that downloads the matching binary into `vendor/bin/reco`. + +Until then, build reco yourself and point `ECS_TURBO_BIN` at it, or put it on your `PATH`. diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index 3bea4a5dd1..e448ba286f 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -13,6 +13,7 @@ use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter; use Symplify\EasyCodingStandard\MemoryLimitter; use Symplify\EasyCodingStandard\Reporter\ProcessedFileReporter; +use Symplify\EasyCodingStandard\Turbo\TurboRunner; final readonly class CheckCommand implements CommandInterface, DefaultCommandInterface { @@ -22,6 +23,7 @@ public function __construct( private ConfigInitializer $configInitializer, private EasyCodingStandardApplication $easyCodingStandardApplication, private ConfigurationFactory $configurationFactory, + private TurboRunner $turboRunner, ) { } @@ -36,6 +38,7 @@ public function getDescription(): string } /** + * @param bool $turbo [EXPERIMENTAL] run the reco Go binary instead of the PHP engine * @param string $config Path to config file * @param string $outputFormat Select output format * @param string $memoryLimit Memory limit for check @@ -60,6 +63,7 @@ public function run( bool $noErrorTable = false, bool $noDiffs = false, bool $debug = false, + bool $turbo = false, string $config = '', string $outputFormat = ConsoleOutputFormatter::NAME, string $memoryLimit = '', @@ -87,6 +91,13 @@ public function run( $memoryLimit !== '' ? $memoryLimit : null, $debug, ); + + // experimental: hand the resolved paths to the reco Go binary and skip the PHP engine + if ($turbo) { + $turboExitCode = $this->turboRunner->run($configuration->getSources(), $fix); + return $turboExitCode === ExitCode::SUCCESS ? ExitCode::SUCCESS : ExitCode::CHANGED_CODE_OR_FOUND_ERRORS; + } + $this->memoryLimitter->adjust($configuration); $errorsAndDiffs = $this->easyCodingStandardApplication->run($configuration); diff --git a/src/Turbo/RecoBinaryLocator.php b/src/Turbo/RecoBinaryLocator.php new file mode 100644 index 0000000000..02ff06a8b2 --- /dev/null +++ b/src/Turbo/RecoBinaryLocator.php @@ -0,0 +1,31 @@ +recoBinaryLocator->locate(); + $arguments = $this->createArguments($binary, $paths, $isFixMode); + + // symfony/process is in "replace"; passthru streams reco's output straight through + $command = implode(' ', array_map(escapeshellarg(...), $arguments)); + + $exitCode = 0; + passthru($command, $exitCode); + + return $exitCode; + } + + /** + * @param string[] $paths + * @return string[] + */ + public function createArguments(string $binary, array $paths, bool $isFixMode): array + { + $arguments = [$binary, 'run']; + + // reco rewrites in place by default; --dry-run only reports, matching the + // check-vs-fix split of ECS itself + if (! $isFixMode) { + $arguments[] = '--dry-run'; + } + + return array_merge($arguments, array_values($paths)); + } +} diff --git a/tests/Turbo/RecoBinaryLocatorTest.php b/tests/Turbo/RecoBinaryLocatorTest.php new file mode 100644 index 0000000000..60c3593640 --- /dev/null +++ b/tests/Turbo/RecoBinaryLocatorTest.php @@ -0,0 +1,34 @@ +recoBinaryLocator = new RecoBinaryLocator(); + } + + public function testEnvironmentOverrideWins(): void + { + putenv('ECS_TURBO_BIN=' . __FILE__); + + $this->assertSame(__FILE__, $this->recoBinaryLocator->locate()); + + putenv('ECS_TURBO_BIN'); + } + + public function testFallsBackToPathWhenNoBinaryFound(): void + { + putenv('ECS_TURBO_BIN'); + + $this->assertSame('reco', $this->recoBinaryLocator->locate()); + } +} diff --git a/tests/Turbo/TurboRunnerTest.php b/tests/Turbo/TurboRunnerTest.php new file mode 100644 index 0000000000..4fe0615d12 --- /dev/null +++ b/tests/Turbo/TurboRunnerTest.php @@ -0,0 +1,33 @@ +turboRunner = new TurboRunner(new RecoBinaryLocator()); + } + + public function testCreateArgumentsInCheckModeAddsDryRun(): void + { + $arguments = $this->turboRunner->createArguments('reco', ['src', 'tests'], false); + + $this->assertSame(['reco', 'run', '--dry-run', 'src', 'tests'], $arguments); + } + + public function testCreateArgumentsInFixModeRewritesInPlace(): void + { + $arguments = $this->turboRunner->createArguments('reco', ['src'], true); + + $this->assertSame(['reco', 'run', 'src'], $arguments); + } +} From 04310c90843315ac32343f9bb81674d92046a2fd Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 24 Sep 2026 10:33:36 +0200 Subject: [PATCH 2/3] Add dump-config command and feed the ECS config to reco in turbo mode Turbo mode now resolves the full ecs.php config - paths, rules (with each fixer's configuration) and skips - dumps it to a temp JSON file, and passes it to reco via --ecs-config, so reco maps the ECS rules to its native ones rather than running a fixed set. The same JSON is exposed standalone via a new dump-config command. RecoConfigDumper builds the JSON; TurboRunner writes and runs it. --- docs/turbo.md | 35 ++++- src/Console/Command/CheckCommand.php | 7 +- src/Console/Command/DumpConfigCommand.php | 69 ++++++++++ src/Turbo/RecoConfigDumper.php | 126 ++++++++++++++++++ src/Turbo/TurboRunner.php | 47 +++++-- .../Command/CommandRegistrationTest.php | 1 + tests/Turbo/RecoConfigDumperTest.php | 67 ++++++++++ tests/Turbo/Source/configured-ecs.php | 18 +++ tests/Turbo/TurboRunnerTest.php | 8 +- 9 files changed, 355 insertions(+), 23 deletions(-) create mode 100644 src/Console/Command/DumpConfigCommand.php create mode 100644 src/Turbo/RecoConfigDumper.php create mode 100644 tests/Turbo/RecoConfigDumperTest.php create mode 100644 tests/Turbo/Source/configured-ecs.php diff --git a/docs/turbo.md b/docs/turbo.md index b5b5c88fd9..e82ebe62a4 100644 --- a/docs/turbo.md +++ b/docs/turbo.md @@ -10,13 +10,40 @@ vendor/bin/ecs check src --turbo # limit to a path - pass the path before t Pass any explicit path before the flags (`check src --turbo`), as with `--fix`; a path written after a boolean flag is consumed as that flag's value. -ECS resolves the paths from `ecs.php` (and any paths passed on the command line) exactly as usual, then invokes reco over them. The check/fix split maps onto reco directly: +ECS resolves the configuration from `ecs.php` (paths, rules and skips) exactly as usual, dumps it to a temporary JSON file, and hands that file to reco via `--ecs-config`. reco maps each ECS rule to its native equivalent, applies the shared paths and skips, and reports every rule it could not map. The check/fix split maps onto reco directly: -- `--turbo` runs `reco run --dry-run ` - reports without writing. -- `--turbo --fix` runs `reco run ` - rewrites in place. +- `--turbo` runs `reco run --ecs-config --dry-run` - reports without writing. +- `--turbo --fix` runs `reco run --ecs-config ` - rewrites in place. The exit code of `reco` is passed through. +## Dumping the config + +The JSON reco consumes can also be inspected on its own: + +```bash +vendor/bin/ecs dump-config # print the resolved paths, rules and skips as JSON +``` + +The shape is: + +```json +{ + "paths": ["/abs/src"], + "rules": [ + { "class": "PhpCsFixer\\Fixer\\ArrayNotation\\ArraySyntaxFixer", "config": { "syntax": "short" } }, + { "class": "PhpCsFixer\\Fixer\\CastNotation\\LowercaseCastFixer", "config": {} } + ], + "skips": [ + { "path": "*/Legacy/*" }, + { "class": "PhpCsFixer\\Fixer\\Import\\OrderedImportsFixer" }, + { "class": "PhpCsFixer\\Fixer\\CastNotation\\LowercaseCastFixer", "paths": ["*/tests/*"] } + ] +} +``` + +Configured fixer options are read off the fixer instance; sniff properties are not extracted yet. + ## Binary resolution The reco binary is looked up in this order: @@ -27,7 +54,7 @@ The reco binary is looked up in this order: ## Prototype caveat -This is an RFC-stage prototype. In turbo mode reco runs **its own native rule set**, not the sniffs and fixers configured in `ecs.php`. The rule selection in your config is ignored for now - only the paths are shared. Do not treat a turbo run as equivalent to a normal ECS run yet. +This is an RFC-stage prototype. reco now reads the rules and skips from your `ecs.php` (not just the paths), but it only maps rules that have an exact native equivalent. Every unmapped rule is reported and skipped, and a rule configured in a way reco does not replicate is reported as unsupported rather than run under the wrong behaviour. So a turbo run covers a growing subset of your config, never silently more or less - but it is not yet equivalent to a full ECS run. The reco side lists what maps today. ## Remaining delivery work diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index e448ba286f..55ad639fa4 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -13,6 +13,7 @@ use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter; use Symplify\EasyCodingStandard\MemoryLimitter; use Symplify\EasyCodingStandard\Reporter\ProcessedFileReporter; +use Symplify\EasyCodingStandard\Turbo\RecoConfigDumper; use Symplify\EasyCodingStandard\Turbo\TurboRunner; final readonly class CheckCommand implements CommandInterface, DefaultCommandInterface @@ -24,6 +25,7 @@ public function __construct( private EasyCodingStandardApplication $easyCodingStandardApplication, private ConfigurationFactory $configurationFactory, private TurboRunner $turboRunner, + private RecoConfigDumper $recoConfigDumper, ) { } @@ -92,9 +94,10 @@ public function run( $debug, ); - // experimental: hand the resolved paths to the reco Go binary and skip the PHP engine + // experimental: hand the resolved config to the reco Go binary and skip the PHP engine if ($turbo) { - $turboExitCode = $this->turboRunner->run($configuration->getSources(), $fix); + $configData = $this->recoConfigDumper->dump($configuration->getSources()); + $turboExitCode = $this->turboRunner->run($configData, $fix); return $turboExitCode === ExitCode::SUCCESS ? ExitCode::SUCCESS : ExitCode::CHANGED_CODE_OR_FOUND_ERRORS; } diff --git a/src/Console/Command/DumpConfigCommand.php b/src/Console/Command/DumpConfigCommand.php new file mode 100644 index 0000000000..58586e4f39 --- /dev/null +++ b/src/Console/Command/DumpConfigCommand.php @@ -0,0 +1,69 @@ +configurationFactory->create( + array_values($paths), + false, + false, + false, + false, + false, + ConsoleOutputFormatter::NAME, + $config !== '' ? $config : null, + '', + '', + null, + false, + ); + + $data = $this->recoConfigDumper->dump($configuration->getSources()); + + echo Json::encode($data, Json::PRETTY) . PHP_EOL; + + return ExitCode::SUCCESS; + } +} diff --git a/src/Turbo/RecoConfigDumper.php b/src/Turbo/RecoConfigDumper.php new file mode 100644 index 0000000000..a411f1c35b --- /dev/null +++ b/src/Turbo/RecoConfigDumper.php @@ -0,0 +1,126 @@ +}>, skips: array} + */ + public function dump(array $paths): array + { + return [ + 'paths' => array_values($paths), + 'rules' => $this->dumpRules(), + 'skips' => $this->dumpSkips(), + ]; + } + + /** + * @return array}> + */ + private function dumpRules(): array + { + $rules = []; + + foreach ($this->fixerFileProcessor->getCheckers() as $fixer) { + $rules[] = [ + 'class' => $fixer::class, + 'config' => $this->extractFixerConfiguration($fixer), + ]; + } + + foreach ($this->sniffFileProcessor->getCheckers() as $sniff) { + $rules[] = [ + 'class' => $sniff::class, + // sniff properties are not extracted yet; reco maps only config-less sniffs + 'config' => new stdClass(), + ]; + } + + return $rules; + } + + /** + * @return array + */ + private function dumpSkips(): array + { + $skips = []; + + foreach ($this->skippedPathsResolver->resolve() as $path) { + $skips[] = [ + 'path' => $path, + ]; + } + + foreach ($this->skippedClassResolver->resolve() as $checkerClass => $paths) { + if ($paths === null) { + $skips[] = [ + 'class' => $checkerClass, + ]; + continue; + } + + $skips[] = [ + 'class' => $checkerClass, + 'paths' => array_values($paths), + ]; + } + + return $skips; + } + + /** + * Reads a configured fixer's options off the ConfigurableFixerTrait's + * `configuration` property. An unconfigured or non-configurable fixer yields + * an empty object, which reco reads as the config-less form. + * + * @return object|array + */ + private function extractFixerConfiguration(FixerInterface $fixer): object|array + { + if (! $fixer instanceof ConfigurableFixerInterface) { + return new stdClass(); + } + + if (! property_exists($fixer, 'configuration')) { + return new stdClass(); + } + + $reflectionProperty = new ReflectionProperty($fixer, 'configuration'); + + $configuration = $reflectionProperty->getValue($fixer); + + if (! is_array($configuration) || $configuration === []) { + return new stdClass(); + } + + return $configuration; + } +} diff --git a/src/Turbo/TurboRunner.php b/src/Turbo/TurboRunner.php index 2e71b20597..626b864550 100644 --- a/src/Turbo/TurboRunner.php +++ b/src/Turbo/TurboRunner.php @@ -4,9 +4,13 @@ namespace Symplify\EasyCodingStandard\Turbo; +use Nette\Utils\Json; + /** * Experimental --turbo mode: hands the run over to the "reco" Go binary instead - * of the PHP engine. See docs/turbo.md for the current limitations. + * of the PHP engine. The resolved ecs.php config (paths, rules, skips) is written + * to a temp JSON file and passed to reco via --ecs-config, so reco maps the ECS + * rules to its native ones. See docs/turbo.md for the current limitations. * * @see \Symplify\EasyCodingStandard\Tests\Turbo\TurboRunnerTest */ @@ -18,29 +22,35 @@ public function __construct( } /** - * @param string[] $paths + * @param array{paths: string[], rules: array, skips: array} $configData */ - public function run(array $paths, bool $isFixMode): int + public function run(array $configData, bool $isFixMode): int { $binary = $this->recoBinaryLocator->locate(); - $arguments = $this->createArguments($binary, $paths, $isFixMode); - // symfony/process is in "replace"; passthru streams reco's output straight through - $command = implode(' ', array_map(escapeshellarg(...), $arguments)); + $configPath = $this->writeConfig($configData); + + try { + $arguments = $this->createArguments($binary, $configPath, $isFixMode); + + // symfony/process is in "replace"; passthru streams reco's output straight through + $command = implode(' ', array_map(escapeshellarg(...), $arguments)); - $exitCode = 0; - passthru($command, $exitCode); + $exitCode = 0; + passthru($command, $exitCode); - return $exitCode; + return $exitCode; + } finally { + @unlink($configPath); + } } /** - * @param string[] $paths * @return string[] */ - public function createArguments(string $binary, array $paths, bool $isFixMode): array + public function createArguments(string $binary, string $configPath, bool $isFixMode): array { - $arguments = [$binary, 'run']; + $arguments = [$binary, 'run', '--ecs-config', $configPath]; // reco rewrites in place by default; --dry-run only reports, matching the // check-vs-fix split of ECS itself @@ -48,6 +58,17 @@ public function createArguments(string $binary, array $paths, bool $isFixMode): $arguments[] = '--dry-run'; } - return array_merge($arguments, array_values($paths)); + return $arguments; + } + + /** + * @param array{paths: string[], rules: array, skips: array} $configData + */ + private function writeConfig(array $configData): string + { + $configPath = tempnam(sys_get_temp_dir(), 'ecs-turbo-') . '.json'; + file_put_contents($configPath, Json::encode($configData, Json::PRETTY)); + + return $configPath; } } diff --git a/tests/Console/Command/CommandRegistrationTest.php b/tests/Console/Command/CommandRegistrationTest.php index 5fafbd9ca1..39e279c6eb 100644 --- a/tests/Console/Command/CommandRegistrationTest.php +++ b/tests/Console/Command/CommandRegistrationTest.php @@ -29,6 +29,7 @@ public function testCommandsAreRegistered(): void $this->assertTrue($this->commandRegistry->has('check')); $this->assertTrue($this->commandRegistry->has('worker')); $this->assertTrue($this->commandRegistry->has('list-checkers')); + $this->assertTrue($this->commandRegistry->has('dump-config')); } public function testCheckIsTheDefaultCommand(): void diff --git a/tests/Turbo/RecoConfigDumperTest.php b/tests/Turbo/RecoConfigDumperTest.php new file mode 100644 index 0000000000..8659b9618b --- /dev/null +++ b/tests/Turbo/RecoConfigDumperTest.php @@ -0,0 +1,67 @@ +createContainerWithConfigs([__DIR__ . '/Source/configured-ecs.php']); + $this->recoConfigDumper = $this->make(RecoConfigDumper::class); + } + + public function testDumpPassesPathsThrough(): void + { + $data = $this->recoConfigDumper->dump(['src', 'tests']); + + $this->assertSame(['src', 'tests'], $data['paths']); + } + + public function testDumpExtractsConfiguredFixerConfig(): void + { + $data = $this->recoConfigDumper->dump([]); + + $configByClass = []; + foreach ($data['rules'] as $rule) { + $configByClass[$rule['class']] = $rule['config']; + } + + $this->assertArrayHasKey(ArraySyntaxFixer::class, $configByClass); + $this->assertSame([ + 'syntax' => 'short', + ], $configByClass[ArraySyntaxFixer::class]); + } + + public function testDumpReportsPathSkipAndClassSkip(): void + { + $data = $this->recoConfigDumper->dump([]); + + $skipPaths = []; + $skipClasses = []; + foreach ($data['skips'] as $skip) { + if (isset($skip['path'])) { + $skipPaths[] = $skip['path']; + } elseif (isset($skip['class']) && ! isset($skip['paths'])) { + $skipClasses[] = $skip['class']; + } + } + + $this->assertContains(LowercaseKeywordsFixer::class, $skipClasses); + + $matchedGlob = array_filter($skipPaths, static fn (string $path): bool => str_contains($path, 'legacy')); + $this->assertNotEmpty($matchedGlob, 'expected the legacy/* path skip to be dumped'); + } +} diff --git a/tests/Turbo/Source/configured-ecs.php b/tests/Turbo/Source/configured-ecs.php new file mode 100644 index 0000000000..c5a627a2a1 --- /dev/null +++ b/tests/Turbo/Source/configured-ecs.php @@ -0,0 +1,18 @@ +withConfiguredRule(ArraySyntaxFixer::class, [ + 'syntax' => 'short', + ]) + ->withRules([LowercaseCastFixer::class]) + ->withSkip([ + __DIR__ . '/legacy/*', + LowercaseKeywordsFixer::class, + ]); diff --git a/tests/Turbo/TurboRunnerTest.php b/tests/Turbo/TurboRunnerTest.php index 4fe0615d12..8384cf9f22 100644 --- a/tests/Turbo/TurboRunnerTest.php +++ b/tests/Turbo/TurboRunnerTest.php @@ -19,15 +19,15 @@ protected function setUp(): void public function testCreateArgumentsInCheckModeAddsDryRun(): void { - $arguments = $this->turboRunner->createArguments('reco', ['src', 'tests'], false); + $arguments = $this->turboRunner->createArguments('reco', '/tmp/ecs-turbo.json', false); - $this->assertSame(['reco', 'run', '--dry-run', 'src', 'tests'], $arguments); + $this->assertSame(['reco', 'run', '--ecs-config', '/tmp/ecs-turbo.json', '--dry-run'], $arguments); } public function testCreateArgumentsInFixModeRewritesInPlace(): void { - $arguments = $this->turboRunner->createArguments('reco', ['src'], true); + $arguments = $this->turboRunner->createArguments('reco', '/tmp/ecs-turbo.json', true); - $this->assertSame(['reco', 'run', 'src'], $arguments); + $this->assertSame(['reco', 'run', '--ecs-config', '/tmp/ecs-turbo.json'], $arguments); } } From 2c9471c98259ccae83447261400e41f7c3afd260 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 24 Sep 2026 10:55:02 +0200 Subject: [PATCH 3/3] Point turbo mode at the ecs-go binary instead of reco The config consumer now lives in tomasvotruba/ecs-go, whose fixers are named by their PHP-CS-Fixer class, so the dumped config maps across by name. Rename RecoBinaryLocator to EcsGoBinaryLocator (locating ecs-go / vendor/bin/ecs-go, ECS_TURBO_BIN unchanged) and RecoConfigDumper to TurboConfigDumper, and adjust the invocation: ecs-go reports by default and rewrites with --fix, so turbo passes --ecs-config and adds --fix in fix mode. --- docs/turbo.md | 28 +++++++++---------- src/Console/Command/CheckCommand.php | 10 +++---- src/Console/Command/DumpConfigCommand.php | 10 +++---- ...naryLocator.php => EcsGoBinaryLocator.php} | 10 +++---- ...ConfigDumper.php => TurboConfigDumper.php} | 8 +++--- src/Turbo/TurboRunner.php | 20 ++++++------- ...torTest.php => EcsGoBinaryLocatorTest.php} | 12 ++++---- ...mperTest.php => TurboConfigDumperTest.php} | 14 +++++----- tests/Turbo/TurboRunnerTest.php | 14 +++++----- 9 files changed, 63 insertions(+), 63 deletions(-) rename src/Turbo/{RecoBinaryLocator.php => EcsGoBinaryLocator.php} (65%) rename src/Turbo/{RecoConfigDumper.php => TurboConfigDumper.php} (94%) rename tests/Turbo/{RecoBinaryLocatorTest.php => EcsGoBinaryLocatorTest.php} (53%) rename tests/Turbo/{RecoConfigDumperTest.php => TurboConfigDumperTest.php} (80%) diff --git a/docs/turbo.md b/docs/turbo.md index e82ebe62a4..25b6c03b69 100644 --- a/docs/turbo.md +++ b/docs/turbo.md @@ -1,6 +1,6 @@ # Turbo mode (experimental) -`--turbo` hands the run over to the [`reco`](https://github.com/TomasVotruba/reco) Go binary instead of the PHP engine: +`--turbo` hands the run over to the [`ecs-go`](https://github.com/TomasVotruba/ecs-go) Go binary instead of the PHP engine: ```bash vendor/bin/ecs check --turbo # report only (like a normal check) @@ -10,16 +10,16 @@ vendor/bin/ecs check src --turbo # limit to a path - pass the path before t Pass any explicit path before the flags (`check src --turbo`), as with `--fix`; a path written after a boolean flag is consumed as that flag's value. -ECS resolves the configuration from `ecs.php` (paths, rules and skips) exactly as usual, dumps it to a temporary JSON file, and hands that file to reco via `--ecs-config`. reco maps each ECS rule to its native equivalent, applies the shared paths and skips, and reports every rule it could not map. The check/fix split maps onto reco directly: +ECS resolves the configuration from `ecs.php` (paths, rules and skips) exactly as usual, dumps it to a temporary JSON file, and hands that file to ecs-go via `--ecs-config`. ecs-go maps each ECS rule onto its own fixer by class name, applies the shared paths and skips, and reports every rule it could not map. The check/fix split maps onto ecs-go directly: -- `--turbo` runs `reco run --ecs-config --dry-run` - reports without writing. -- `--turbo --fix` runs `reco run --ecs-config ` - rewrites in place. +- `--turbo` runs `ecs-go --ecs-config ` - reports without writing. +- `--turbo --fix` runs `ecs-go --ecs-config --fix` - rewrites in place. -The exit code of `reco` is passed through. +The exit code of `ecs-go` is passed through. ## Dumping the config -The JSON reco consumes can also be inspected on its own: +The JSON ecs-go consumes can also be inspected on its own: ```bash vendor/bin/ecs dump-config # print the resolved paths, rules and skips as JSON @@ -46,21 +46,21 @@ Configured fixer options are read off the fixer instance; sniff properties are n ## Binary resolution -The reco binary is looked up in this order: +The ecs-go binary is looked up in this order: 1. the `ECS_TURBO_BIN` environment variable, if it points to an existing file; -2. `vendor/bin/reco`, if present; -3. `reco` on the `PATH`. +2. `vendor/bin/ecs-go`, if present; +3. `ecs-go` on the `PATH`. ## Prototype caveat -This is an RFC-stage prototype. reco now reads the rules and skips from your `ecs.php` (not just the paths), but it only maps rules that have an exact native equivalent. Every unmapped rule is reported and skipped, and a rule configured in a way reco does not replicate is reported as unsupported rather than run under the wrong behaviour. So a turbo run covers a growing subset of your config, never silently more or less - but it is not yet equivalent to a full ECS run. The reco side lists what maps today. +This is an RFC-stage prototype. ecs-go reads the rules and skips from your `ecs.php` (not just the paths), and because every ecs-go fixer is named by its PHP-CS-Fixer class, the rules map straight across by name. Every rule ecs-go has no fixer for is reported and skipped, and a configured rule runs with ecs-go's built-in behaviour (its configuration is not modelled yet) and is noted in the report. So a turbo run is never silently narrower than your config, but it is not yet equivalent to a full ECS run. ## Remaining delivery work -For `--turbo` to work out of the box, the reco binary has to be installed alongside ECS. The planned path: +For `--turbo` to work out of the box, the ecs-go binary has to be installed alongside ECS. The planned path: -- ship reco as downloadable per-OS binaries via goreleaser releases; -- add a Composer post-install step that downloads the matching binary into `vendor/bin/reco`. +- require `tomasvotruba/ecs-go` as a dev dependency, which exposes `vendor/bin/ecs-go`; +- or ship ecs-go as downloadable per-OS binaries and point `ECS_TURBO_BIN` at one. -Until then, build reco yourself and point `ECS_TURBO_BIN` at it, or put it on your `PATH`. +Until then, build ecs-go yourself and point `ECS_TURBO_BIN` at it, or put it on your `PATH`. diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index 55ad639fa4..756e055c93 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -13,7 +13,7 @@ use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter; use Symplify\EasyCodingStandard\MemoryLimitter; use Symplify\EasyCodingStandard\Reporter\ProcessedFileReporter; -use Symplify\EasyCodingStandard\Turbo\RecoConfigDumper; +use Symplify\EasyCodingStandard\Turbo\TurboConfigDumper; use Symplify\EasyCodingStandard\Turbo\TurboRunner; final readonly class CheckCommand implements CommandInterface, DefaultCommandInterface @@ -25,7 +25,7 @@ public function __construct( private EasyCodingStandardApplication $easyCodingStandardApplication, private ConfigurationFactory $configurationFactory, private TurboRunner $turboRunner, - private RecoConfigDumper $recoConfigDumper, + private TurboConfigDumper $turboConfigDumper, ) { } @@ -40,7 +40,7 @@ public function getDescription(): string } /** - * @param bool $turbo [EXPERIMENTAL] run the reco Go binary instead of the PHP engine + * @param bool $turbo [EXPERIMENTAL] run the ecs-go Go binary instead of the PHP engine * @param string $config Path to config file * @param string $outputFormat Select output format * @param string $memoryLimit Memory limit for check @@ -94,9 +94,9 @@ public function run( $debug, ); - // experimental: hand the resolved config to the reco Go binary and skip the PHP engine + // experimental: hand the resolved config to the ecs-go Go binary and skip the PHP engine if ($turbo) { - $configData = $this->recoConfigDumper->dump($configuration->getSources()); + $configData = $this->turboConfigDumper->dump($configuration->getSources()); $turboExitCode = $this->turboRunner->run($configData, $fix); return $turboExitCode === ExitCode::SUCCESS ? ExitCode::SUCCESS : ExitCode::CHANGED_CODE_OR_FOUND_ERRORS; } diff --git a/src/Console/Command/DumpConfigCommand.php b/src/Console/Command/DumpConfigCommand.php index 58586e4f39..4a1f48120e 100644 --- a/src/Console/Command/DumpConfigCommand.php +++ b/src/Console/Command/DumpConfigCommand.php @@ -9,17 +9,17 @@ use Symplify\EasyCodingStandard\Configuration\ConfigurationFactory; use Symplify\EasyCodingStandard\Console\ExitCode; use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter; -use Symplify\EasyCodingStandard\Turbo\RecoConfigDumper; +use Symplify\EasyCodingStandard\Turbo\TurboConfigDumper; /** * Dumps the resolved ecs.php configuration - paths, rules and skips - as JSON, - * for the reco turbo runner to consume. See docs/turbo.md. + * for the ecs-go turbo runner to consume. See docs/turbo.md. */ final readonly class DumpConfigCommand implements CommandInterface { public function __construct( private ConfigurationFactory $configurationFactory, - private RecoConfigDumper $recoConfigDumper, + private TurboConfigDumper $turboConfigDumper, ) { } @@ -30,7 +30,7 @@ public function getName(): string public function getDescription(): string { - return 'Dump the resolved configuration (paths, rules, skips) as JSON for the reco turbo runner'; + return 'Dump the resolved configuration (paths, rules, skips) as JSON for the ecs-go turbo runner'; } /** @@ -60,7 +60,7 @@ public function run(string $config = '', string ...$paths): int false, ); - $data = $this->recoConfigDumper->dump($configuration->getSources()); + $data = $this->turboConfigDumper->dump($configuration->getSources()); echo Json::encode($data, Json::PRETTY) . PHP_EOL; diff --git a/src/Turbo/RecoBinaryLocator.php b/src/Turbo/EcsGoBinaryLocator.php similarity index 65% rename from src/Turbo/RecoBinaryLocator.php rename to src/Turbo/EcsGoBinaryLocator.php index 02ff06a8b2..02c49a9835 100644 --- a/src/Turbo/RecoBinaryLocator.php +++ b/src/Turbo/EcsGoBinaryLocator.php @@ -5,11 +5,11 @@ namespace Symplify\EasyCodingStandard\Turbo; /** - * Resolves the "reco" Go binary that powers the experimental --turbo mode. + * Resolves the "ecs-go" Go binary that powers the experimental --turbo mode. * - * @see \Symplify\EasyCodingStandard\Tests\Turbo\RecoBinaryLocatorTest + * @see \Symplify\EasyCodingStandard\Tests\Turbo\EcsGoBinaryLocatorTest */ -final class RecoBinaryLocator +final class EcsGoBinaryLocator { private const string ENV_OVERRIDE = 'ECS_TURBO_BIN'; @@ -20,12 +20,12 @@ public function locate(): string return $envBinary; } - $vendorBinary = getcwd() . '/vendor/bin/reco'; + $vendorBinary = getcwd() . '/vendor/bin/ecs-go'; if (is_file($vendorBinary)) { return $vendorBinary; } // fall back to the binary on PATH - return 'reco'; + return 'ecs-go'; } } diff --git a/src/Turbo/RecoConfigDumper.php b/src/Turbo/TurboConfigDumper.php similarity index 94% rename from src/Turbo/RecoConfigDumper.php rename to src/Turbo/TurboConfigDumper.php index a411f1c35b..c39363d417 100644 --- a/src/Turbo/RecoConfigDumper.php +++ b/src/Turbo/TurboConfigDumper.php @@ -14,11 +14,11 @@ use Symplify\EasyCodingStandard\SniffRunner\Application\SniffFileProcessor; /** - * Turns the resolved ecs.php configuration into the JSON shape the reco turbo + * Turns the resolved ecs.php configuration into the JSON shape the ecs-go turbo * runner consumes: paths, rules (each a class and its config) and skips. See * docs/turbo.md for the schema. */ -final readonly class RecoConfigDumper +final readonly class TurboConfigDumper { public function __construct( private SniffFileProcessor $sniffFileProcessor, @@ -58,7 +58,7 @@ private function dumpRules(): array foreach ($this->sniffFileProcessor->getCheckers() as $sniff) { $rules[] = [ 'class' => $sniff::class, - // sniff properties are not extracted yet; reco maps only config-less sniffs + // sniff properties are not extracted yet; ecs-go maps only config-less sniffs 'config' => new stdClass(), ]; } @@ -99,7 +99,7 @@ private function dumpSkips(): array /** * Reads a configured fixer's options off the ConfigurableFixerTrait's * `configuration` property. An unconfigured or non-configurable fixer yields - * an empty object, which reco reads as the config-less form. + * an empty object, which ecs-go reads as the config-less form. * * @return object|array */ diff --git a/src/Turbo/TurboRunner.php b/src/Turbo/TurboRunner.php index 626b864550..3ac7b8c54c 100644 --- a/src/Turbo/TurboRunner.php +++ b/src/Turbo/TurboRunner.php @@ -7,17 +7,17 @@ use Nette\Utils\Json; /** - * Experimental --turbo mode: hands the run over to the "reco" Go binary instead + * Experimental --turbo mode: hands the run over to the "ecs-go" Go binary instead * of the PHP engine. The resolved ecs.php config (paths, rules, skips) is written - * to a temp JSON file and passed to reco via --ecs-config, so reco maps the ECS - * rules to its native ones. See docs/turbo.md for the current limitations. + * to a temp JSON file and passed to ecs-go via --ecs-config, so ecs-go maps the + * ECS rules onto its own fixers. See docs/turbo.md for the current limitations. * * @see \Symplify\EasyCodingStandard\Tests\Turbo\TurboRunnerTest */ final readonly class TurboRunner { public function __construct( - private RecoBinaryLocator $recoBinaryLocator, + private EcsGoBinaryLocator $ecsGoBinaryLocator, ) { } @@ -26,14 +26,14 @@ public function __construct( */ public function run(array $configData, bool $isFixMode): int { - $binary = $this->recoBinaryLocator->locate(); + $binary = $this->ecsGoBinaryLocator->locate(); $configPath = $this->writeConfig($configData); try { $arguments = $this->createArguments($binary, $configPath, $isFixMode); - // symfony/process is in "replace"; passthru streams reco's output straight through + // symfony/process is in "replace"; passthru streams ecs-go's output straight through $command = implode(' ', array_map(escapeshellarg(...), $arguments)); $exitCode = 0; @@ -50,12 +50,12 @@ public function run(array $configData, bool $isFixMode): int */ public function createArguments(string $binary, string $configPath, bool $isFixMode): array { - $arguments = [$binary, 'run', '--ecs-config', $configPath]; + $arguments = [$binary, '--ecs-config', $configPath]; - // reco rewrites in place by default; --dry-run only reports, matching the + // ecs-go reports by default; --fix rewrites in place, matching the // check-vs-fix split of ECS itself - if (! $isFixMode) { - $arguments[] = '--dry-run'; + if ($isFixMode) { + $arguments[] = '--fix'; } return $arguments; diff --git a/tests/Turbo/RecoBinaryLocatorTest.php b/tests/Turbo/EcsGoBinaryLocatorTest.php similarity index 53% rename from tests/Turbo/RecoBinaryLocatorTest.php rename to tests/Turbo/EcsGoBinaryLocatorTest.php index 60c3593640..b342e55fe8 100644 --- a/tests/Turbo/RecoBinaryLocatorTest.php +++ b/tests/Turbo/EcsGoBinaryLocatorTest.php @@ -5,22 +5,22 @@ namespace Symplify\EasyCodingStandard\Tests\Turbo; use PHPUnit\Framework\TestCase; -use Symplify\EasyCodingStandard\Turbo\RecoBinaryLocator; +use Symplify\EasyCodingStandard\Turbo\EcsGoBinaryLocator; -final class RecoBinaryLocatorTest extends TestCase +final class EcsGoBinaryLocatorTest extends TestCase { - private RecoBinaryLocator $recoBinaryLocator; + private EcsGoBinaryLocator $ecsGoBinaryLocator; protected function setUp(): void { - $this->recoBinaryLocator = new RecoBinaryLocator(); + $this->ecsGoBinaryLocator = new EcsGoBinaryLocator(); } public function testEnvironmentOverrideWins(): void { putenv('ECS_TURBO_BIN=' . __FILE__); - $this->assertSame(__FILE__, $this->recoBinaryLocator->locate()); + $this->assertSame(__FILE__, $this->ecsGoBinaryLocator->locate()); putenv('ECS_TURBO_BIN'); } @@ -29,6 +29,6 @@ public function testFallsBackToPathWhenNoBinaryFound(): void { putenv('ECS_TURBO_BIN'); - $this->assertSame('reco', $this->recoBinaryLocator->locate()); + $this->assertSame('ecs-go', $this->ecsGoBinaryLocator->locate()); } } diff --git a/tests/Turbo/RecoConfigDumperTest.php b/tests/Turbo/TurboConfigDumperTest.php similarity index 80% rename from tests/Turbo/RecoConfigDumperTest.php rename to tests/Turbo/TurboConfigDumperTest.php index 8659b9618b..024e7a3247 100644 --- a/tests/Turbo/RecoConfigDumperTest.php +++ b/tests/Turbo/TurboConfigDumperTest.php @@ -8,11 +8,11 @@ use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer; use PhpCsFixer\Fixer\Casing\LowercaseKeywordsFixer; use Symplify\EasyCodingStandard\Testing\PHPUnit\AbstractTestCase; -use Symplify\EasyCodingStandard\Turbo\RecoConfigDumper; +use Symplify\EasyCodingStandard\Turbo\TurboConfigDumper; -final class RecoConfigDumperTest extends AbstractTestCase +final class TurboConfigDumperTest extends AbstractTestCase { - private RecoConfigDumper $recoConfigDumper; + private TurboConfigDumper $turboConfigDumper; #[Override] protected function setUp(): void @@ -20,19 +20,19 @@ protected function setUp(): void parent::setUp(); $this->createContainerWithConfigs([__DIR__ . '/Source/configured-ecs.php']); - $this->recoConfigDumper = $this->make(RecoConfigDumper::class); + $this->turboConfigDumper = $this->make(TurboConfigDumper::class); } public function testDumpPassesPathsThrough(): void { - $data = $this->recoConfigDumper->dump(['src', 'tests']); + $data = $this->turboConfigDumper->dump(['src', 'tests']); $this->assertSame(['src', 'tests'], $data['paths']); } public function testDumpExtractsConfiguredFixerConfig(): void { - $data = $this->recoConfigDumper->dump([]); + $data = $this->turboConfigDumper->dump([]); $configByClass = []; foreach ($data['rules'] as $rule) { @@ -47,7 +47,7 @@ public function testDumpExtractsConfiguredFixerConfig(): void public function testDumpReportsPathSkipAndClassSkip(): void { - $data = $this->recoConfigDumper->dump([]); + $data = $this->turboConfigDumper->dump([]); $skipPaths = []; $skipClasses = []; diff --git a/tests/Turbo/TurboRunnerTest.php b/tests/Turbo/TurboRunnerTest.php index 8384cf9f22..5d6381d719 100644 --- a/tests/Turbo/TurboRunnerTest.php +++ b/tests/Turbo/TurboRunnerTest.php @@ -5,7 +5,7 @@ namespace Symplify\EasyCodingStandard\Tests\Turbo; use PHPUnit\Framework\TestCase; -use Symplify\EasyCodingStandard\Turbo\RecoBinaryLocator; +use Symplify\EasyCodingStandard\Turbo\EcsGoBinaryLocator; use Symplify\EasyCodingStandard\Turbo\TurboRunner; final class TurboRunnerTest extends TestCase @@ -14,20 +14,20 @@ final class TurboRunnerTest extends TestCase protected function setUp(): void { - $this->turboRunner = new TurboRunner(new RecoBinaryLocator()); + $this->turboRunner = new TurboRunner(new EcsGoBinaryLocator()); } - public function testCreateArgumentsInCheckModeAddsDryRun(): void + public function testCreateArgumentsInCheckModeReportsOnly(): void { - $arguments = $this->turboRunner->createArguments('reco', '/tmp/ecs-turbo.json', false); + $arguments = $this->turboRunner->createArguments('ecs-go', '/tmp/ecs-turbo.json', false); - $this->assertSame(['reco', 'run', '--ecs-config', '/tmp/ecs-turbo.json', '--dry-run'], $arguments); + $this->assertSame(['ecs-go', '--ecs-config', '/tmp/ecs-turbo.json'], $arguments); } public function testCreateArgumentsInFixModeRewritesInPlace(): void { - $arguments = $this->turboRunner->createArguments('reco', '/tmp/ecs-turbo.json', true); + $arguments = $this->turboRunner->createArguments('ecs-go', '/tmp/ecs-turbo.json', true); - $this->assertSame(['reco', 'run', '--ecs-config', '/tmp/ecs-turbo.json'], $arguments); + $this->assertSame(['ecs-go', '--ecs-config', '/tmp/ecs-turbo.json', '--fix'], $arguments); } }