Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
66 changes: 66 additions & 0 deletions docs/turbo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Turbo mode (experimental)

`--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)
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 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 `ecs-go --ecs-config <config.json>` - reports without writing.
- `--turbo --fix` runs `ecs-go --ecs-config <config.json> --fix` - rewrites in place.

The exit code of `ecs-go` is passed through.

## Dumping the config

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
```

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 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/ecs-go`, if present;
3. `ecs-go` on the `PATH`.

## Prototype caveat

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 ecs-go binary has to be installed alongside ECS. The planned path:

- 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 ecs-go yourself and point `ECS_TURBO_BIN` at it, or put it on your `PATH`.
14 changes: 14 additions & 0 deletions src/Console/Command/CheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter;
use Symplify\EasyCodingStandard\MemoryLimitter;
use Symplify\EasyCodingStandard\Reporter\ProcessedFileReporter;
use Symplify\EasyCodingStandard\Turbo\TurboConfigDumper;
use Symplify\EasyCodingStandard\Turbo\TurboRunner;

final readonly class CheckCommand implements CommandInterface, DefaultCommandInterface
{
Expand All @@ -22,6 +24,8 @@ public function __construct(
private ConfigInitializer $configInitializer,
private EasyCodingStandardApplication $easyCodingStandardApplication,
private ConfigurationFactory $configurationFactory,
private TurboRunner $turboRunner,
private TurboConfigDumper $turboConfigDumper,
) {
}

Expand All @@ -36,6 +40,7 @@ public function getDescription(): string
}

/**
* @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
Expand All @@ -60,6 +65,7 @@ public function run(
bool $noErrorTable = false,
bool $noDiffs = false,
bool $debug = false,
bool $turbo = false,
string $config = '',
string $outputFormat = ConsoleOutputFormatter::NAME,
string $memoryLimit = '',
Expand Down Expand Up @@ -87,6 +93,14 @@ public function run(
$memoryLimit !== '' ? $memoryLimit : null,
$debug,
);

// experimental: hand the resolved config to the ecs-go Go binary and skip the PHP engine
if ($turbo) {
$configData = $this->turboConfigDumper->dump($configuration->getSources());
$turboExitCode = $this->turboRunner->run($configData, $fix);
return $turboExitCode === ExitCode::SUCCESS ? ExitCode::SUCCESS : ExitCode::CHANGED_CODE_OR_FOUND_ERRORS;
}

$this->memoryLimitter->adjust($configuration);

$errorsAndDiffs = $this->easyCodingStandardApplication->run($configuration);
Expand Down
69 changes: 69 additions & 0 deletions src/Console/Command/DumpConfigCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Console\Command;

use Entropy\Console\Contract\CommandInterface;
use Nette\Utils\Json;
use Symplify\EasyCodingStandard\Configuration\ConfigurationFactory;
use Symplify\EasyCodingStandard\Console\ExitCode;
use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter;
use Symplify\EasyCodingStandard\Turbo\TurboConfigDumper;

/**
* Dumps the resolved ecs.php configuration - paths, rules and skips - as JSON,
* 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 TurboConfigDumper $turboConfigDumper,
) {
}

public function getName(): string
{
return 'dump-config';
}

public function getDescription(): string
{
return 'Dump the resolved configuration (paths, rules, skips) as JSON for the ecs-go turbo runner';
}

/**
* @param string $config Path to config file
* @param string ...$paths The path(s) to dump the configuration for.
*
* @option $config
*
* @api invoked via reflection by the Entropy console application
*
* @return ExitCode::*
*/
public function run(string $config = '', string ...$paths): int
{
$configuration = $this->configurationFactory->create(
array_values($paths),
false,
false,
false,
false,
false,
ConsoleOutputFormatter::NAME,
$config !== '' ? $config : null,
'',
'',
null,
false,
);

$data = $this->turboConfigDumper->dump($configuration->getSources());

echo Json::encode($data, Json::PRETTY) . PHP_EOL;

return ExitCode::SUCCESS;
}
}
31 changes: 31 additions & 0 deletions src/Turbo/EcsGoBinaryLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Turbo;

/**
* Resolves the "ecs-go" Go binary that powers the experimental --turbo mode.
*
* @see \Symplify\EasyCodingStandard\Tests\Turbo\EcsGoBinaryLocatorTest
*/
final class EcsGoBinaryLocator
{
private const string ENV_OVERRIDE = 'ECS_TURBO_BIN';

public function locate(): string
{
$envBinary = getenv(self::ENV_OVERRIDE);
if (is_string($envBinary) && $envBinary !== '' && is_file($envBinary)) {
return $envBinary;
}

$vendorBinary = getcwd() . '/vendor/bin/ecs-go';
if (is_file($vendorBinary)) {
return $vendorBinary;
}

// fall back to the binary on PATH
return 'ecs-go';
}
}
126 changes: 126 additions & 0 deletions src/Turbo/TurboConfigDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Turbo;

use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use ReflectionProperty;
use stdClass;
use Symplify\EasyCodingStandard\FixerRunner\Application\FixerFileProcessor;
use Symplify\EasyCodingStandard\Skipper\SkipCriteriaResolver\SkippedClassResolver;
use Symplify\EasyCodingStandard\Skipper\SkipCriteriaResolver\SkippedPathsResolver;
use Symplify\EasyCodingStandard\SniffRunner\Application\SniffFileProcessor;

/**
* 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 TurboConfigDumper
{
public function __construct(
private SniffFileProcessor $sniffFileProcessor,
private FixerFileProcessor $fixerFileProcessor,
private SkippedPathsResolver $skippedPathsResolver,
private SkippedClassResolver $skippedClassResolver,
) {
}

/**
* @param string[] $paths
* @return array{paths: string[], rules: array<array{class: string, config: object|array<string, mixed>}>, skips: array<array{path?: string, class?: string, paths?: string[]}>}
*/
public function dump(array $paths): array
{
return [
'paths' => array_values($paths),
'rules' => $this->dumpRules(),
'skips' => $this->dumpSkips(),
];
}

/**
* @return array<array{class: string, config: object|array<string, mixed>}>
*/
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; ecs-go maps only config-less sniffs
'config' => new stdClass(),
];
}

return $rules;
}

/**
* @return array<array{path?: string, class?: string, paths?: string[]}>
*/
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 ecs-go reads as the config-less form.
*
* @return object|array<string, mixed>
*/
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;
}
}
Loading
Loading