From 672466d2fa96288ee11de984b080c5cfdace786b Mon Sep 17 00:00:00 2001 From: Sylvain VANEL Date: Thu, 30 Apr 2026 16:05:39 +0200 Subject: [PATCH 1/4] Add Mago as task --- composer.json | 1 + doc/tasks.md | 2 + doc/tasks/mago.md | 91 ++++++++++++ resources/config/tasks.yml | 7 + src/Task/Mago.php | 94 ++++++++++++ test/Unit/Task/MagoTest.php | 276 ++++++++++++++++++++++++++++++++++++ 6 files changed, 471 insertions(+) create mode 100644 doc/tasks/mago.md create mode 100644 src/Task/Mago.php create mode 100644 test/Unit/Task/MagoTest.php diff --git a/composer.json b/composer.json index 61530d36e..0d145f906 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ "atoum/atoum": "Lets GrumPHP run your unit tests.", "behat/behat": "Lets GrumPHP validate your project features.", "brianium/paratest": "Lets GrumPHP run PHPUnit in parallel.", + "carthage-software/mago": "Lets GrumPHP help you write better PHP code.", "codeception/codeception": "Lets GrumPHP run your project's full stack tests", "consolidation/robo": "Lets GrumPHP run your automated PHP tasks.", "designsecurity/progpilot": "Lets GrumPHP be sure that there are no vulnerabilities in your code.", diff --git a/doc/tasks.md b/doc/tasks.md index d9cdbcf1d..1a904bae0 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -32,6 +32,7 @@ grumphp: infection: ~ jsonlint: ~ kahlan: ~ + mago: ~ make: ~ npm_script: ~ paratest: ~ @@ -99,6 +100,7 @@ Every task has its own default configuration. It is possible to overwrite the pa - [Infection](tasks/infection.md) - [JsonLint](tasks/jsonlint.md) - [Kahlan](tasks/kahlan.md) +- [Mago](tasks/mago.md) - [Make](tasks/make.md) - [NPM script](tasks/npm_script.md) - [Paratest](tasks/paratest.md) diff --git a/doc/tasks/mago.md b/doc/tasks/mago.md new file mode 100644 index 000000000..e7653153d --- /dev/null +++ b/doc/tasks/mago.md @@ -0,0 +1,91 @@ +# Mago + +The Mago task runs the Mago's toolchain. + +***Composer*** + +``` +composer require --dev carthage-software/mago +``` + +***Config*** + +The task lives under the `mago` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + mago: + formatter: ~ + formatter_options: ~ + linter: ~ + linter_options: ~ + analyzer: ~ + analyzer_options: ~ + guard: ~ + guard_options: ~ +``` + +**formatter** + +*Default: `true`* + +Enable the Mago's formatter. + + +**formatter_options** + +*Default: `['--staged']`* + +[Options](https://mago.carthage.software/tools/formatter/command-reference#options) for the `mago format` command. +Each option must be an array's element. +If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. + + +**linter** + +*Default: `true`* + +Enable the Mago's linter. + + +**linter_options** + +*Default: `['--staged']`* + +[Options](https://mago.carthage.software/tools/linter/command-reference#options) for the `mago lint` command. +Each option must be an array's element. +If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. + + +**analyzer** + +*Default: `true`* + +Enable the Mago's analyzer. + + +**analyzer_options** + +*Default: `['--staged']`* + +[Options](https://mago.carthage.software/tools/analyzer/command-reference#options) for the `mago analyze` command. +Each option must be an array's element. +If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. + + +**guard** + +*Default: `false`* + +Enable the architectural guard. + + +**guard_options** + +*Default: `[]`* + +[Options](https://mago.carthage.software/tools/guard/command-reference#options) for the `mago guard` command. +Each option must be an array's element. +If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index d4927541b..99b1bb274 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -169,6 +169,13 @@ services: tags: - {name: grumphp.task, task: kahlan} + GrumPHP\Task\Mago: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - {name: grumphp.task, task: mago} + GrumPHP\Task\Make: arguments: - '@process_builder' diff --git a/src/Task/Mago.php b/src/Task/Mago.php new file mode 100644 index 000000000..0fcb4f3c0 --- /dev/null +++ b/src/Task/Mago.php @@ -0,0 +1,94 @@ + + */ +class Mago extends AbstractExternalTask +{ + + public static function getConfigurableOptions(): ConfigOptionsResolver + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'formatter' => true, + 'formatter_options' => ['--staged'], + 'linter' => true, + 'linter_options' => ['--staged'], + 'analyzer' => true, + 'analyzer_options' => ['--staged'], + 'guard' => false, + 'guard_options' => [], + ]); + + $resolver->addAllowedTypes('formatter', ['bool']); + $resolver->addAllowedTypes('formatter_options', ['array']); + $resolver->addAllowedTypes('linter', ['bool']); + $resolver->addAllowedTypes('linter_options', ['array']); + $resolver->addAllowedTypes('analyzer', ['bool']); + $resolver->addAllowedTypes('analyzer_options', ['array']); + $resolver->addAllowedTypes('guard', ['bool']); + $resolver->addAllowedTypes('guard_options', ['array']); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + /** + * {@inheritdoc} + */ + public function canRunInContext(ContextInterface $context): bool + { + return $context instanceof GitPreCommitContext || $context instanceof RunContext; + } + + /** + * {@inheritdoc} + */ + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + + if ($config['formatter'] === false && $config['linter'] === false && $config['analyzer'] === false && $config['guard'] === false) { + return TaskResult::createSkipped($this, $context); + } + + $commandMap = [ + 'formatter' => 'fmt', + 'linter' => 'lint', + 'analyzer' => 'analyze', + 'guard' => 'guard', + ]; + + foreach ($commandMap as $configKey => $command) { + if ($config[$configKey] !== true) { + continue; + } + + $arguments = $this->processBuilder->createArgumentsForCommand('mago'); + $arguments->add($command); + + $arguments->addArgumentArray('%s', $config[$configKey . '_options']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/test/Unit/Task/MagoTest.php b/test/Unit/Task/MagoTest.php new file mode 100644 index 000000000..610f0626f --- /dev/null +++ b/test/Unit/Task/MagoTest.php @@ -0,0 +1,276 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'formatter' => true, + 'formatter_options' => ['--staged'], + 'linter' => true, + 'linter_options' => ['--staged'], + 'analyzer' => true, + 'analyzer_options' => ['--staged'], + 'guard' => false, + 'guard_options' => [], + ] + ]; + } + + public static function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + self::mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + self::mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + self::mockContext() + ]; + } + + public static function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + self::mockContext(RunContext::class, ['hello.php']), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope' + ]; + } + + public static function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + self::mockContext(RunContext::class, ['hello.php']), + function () { + $this->mockProcessBuilder('mago', self::mockProcess(0)); + } + ]; + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-commands' => [ + [ + 'formatter' => false, + 'linter' => false, + 'analyzer' => false, + 'guard' => false, + ], + self::mockContext(RunContext::class, ['file.php']), + function () {} + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'formatter' => [ + [ + 'formatter' => true, + 'formatter_options' => ['--check'], + 'linter' => false, + 'linter_options' => [], + 'analyzer' => false, + 'analyzer_options' => [], + 'guard' => false, + 'guard_options' => [], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'mago', + [ + 'fmt', + '--check', + ] + ]; + + yield 'linter' => [ + [ + 'formatter' => false, + 'formatter_options' => [], + 'linter' => true, + 'linter_options' => ['--semantics'], + 'analyzer' => false, + 'analyzer_options' => [], + 'guard' => false, + 'guard_options' => [], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'mago', + [ + 'lint', + '--semantics', + ] + ]; + + yield 'analyzer' => [ + [ + 'formatter' => false, + 'formatter_options' => [], + 'linter' => false, + 'linter_options' => [], + 'analyzer' => true, + 'analyzer_options' => ['--no-stubs'], + 'guard' => false, + 'guard_options' => [], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'mago', + [ + 'analyze', + '--no-stubs', + ] + ]; + + yield 'architectural-guard' => [ + [ + 'formatter' => false, + 'formatter_options' => [], + 'linter' => false, + 'linter_options' => [], + 'analyzer' => false, + 'analyzer_options' => [], + 'guard' => true, + 'guard_options' => ['--structural'], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'mago', + [ + 'guard', + '--structural', + ] + ]; + } + + #[DataProvider('provideMultipleExternalTaskRuns')] + #[Test] + public function it_runs_multiple_external_commands( + array $config, + ContextInterface $context, + string $taskName, + array $expectedCommands + ): void { + $task = $this->configureTask($config); + + $this->processBuilder->createArgumentsForCommand($taskName)->will(function () { + return new \GrumPHP\Collection\ProcessArgumentsCollection(); + }); + + $count = 0; + $this->processBuilder->buildProcess(Argument::any()) + ->shouldBeCalledTimes(count($expectedCommands)) + ->will(function ($parameters) use ($expectedCommands, &$count) { + $cliArguments = $expectedCommands[$count++]; + $processArguments = $parameters[0]->getValues(); + \PHPUnit\Framework\Assert::assertSame($cliArguments, $processArguments); + + return MagoTest::mockProcess(0); + }); + + $result = $task->run($context); + self::assertInstanceOf(\GrumPHP\Runner\TaskResultInterface::class, $result); + self::assertTrue($result->isPassed()); + } + + public static function provideMultipleExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class, ['hello.php']), + 'mago', + [ + ['fmt', '--staged'], + ['lint', '--staged'], + ['analyze', '--staged'], + ] + ]; + + yield 'formatter-and-linter' => [ + [ + 'formatter' => true, + 'formatter_options' => ['--staged'], + 'linter' => true, + 'linter_options' => ['--staged'], + 'analyzer' => false, + 'analyzer_options' => [], + 'guard' => false, + 'guard_options' => [], + ], + self::mockContext(RunContext::class, ['hello.php']), + 'mago', + [ + ['fmt', '--staged'], + ['lint', '--staged'], + ] + ]; + + yield 'all-commands' => [ + [ + 'formatter' => true, + 'formatter_options' => ['--staged'], + 'linter' => true, + 'linter_options' => ['--staged', '--semantics', '--minimum-report-level=warning'], + 'analyzer' => true, + 'analyzer_options' => ['--staged'], + 'guard' => true, + 'guard_options' => ['--structural'], + ], + self::mockContext(RunContext::class, ['hello.php']), + 'mago', + [ + [ + 'fmt', + '--staged', + ], + [ + 'lint', + '--staged', + '--semantics', + '--minimum-report-level=warning', + ], + [ + 'analyze', + '--staged', + ], + [ + 'guard', + '--structural', + ], + ] + ]; + } +} From 239952bce2b5745640204cd814f448afc308c900 Mon Sep 17 00:00:00 2001 From: Sylvain VANEL Date: Fri, 1 May 2026 11:00:35 +0200 Subject: [PATCH 2/4] Fix phpcs on Mago task --- src/Task/Mago.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Task/Mago.php b/src/Task/Mago.php index 0fcb4f3c0..2027939c0 100644 --- a/src/Task/Mago.php +++ b/src/Task/Mago.php @@ -60,7 +60,10 @@ public function run(ContextInterface $context): TaskResultInterface { $config = $this->getConfig()->getOptions(); - if ($config['formatter'] === false && $config['linter'] === false && $config['analyzer'] === false && $config['guard'] === false) { + if ($config['formatter'] === false + && $config['linter'] === false + && $config['analyzer'] === false + && $config['guard'] === false) { return TaskResult::createSkipped($this, $context); } From 76c3bab60e0602355c1648a5e22735cda6a655f7 Mon Sep 17 00:00:00 2001 From: Sylvain VANEL Date: Fri, 8 May 2026 11:43:57 +0200 Subject: [PATCH 3/4] Refactor Mago task into 4 subtasks --- doc/tasks.md | 9 +- doc/tasks/mago.md | 93 +------- doc/tasks/mago/analyzer.md | 119 ++++++++++ doc/tasks/mago/formatter.md | 32 +++ doc/tasks/mago/guard.md | 123 +++++++++++ doc/tasks/mago/linter.md | 135 ++++++++++++ resources/config/tasks.yml | 28 +++ src/Task/Mago.php | 160 +++++++++----- src/Task/MagoAnalyzer.php | 59 +++++ src/Task/MagoFormatter.php | 48 ++++ src/Task/MagoGuard.php | 61 ++++++ src/Task/MagoLinter.php | 65 ++++++ test/Unit/Task/MagoAnalyzerTest.php | 265 ++++++++++++++++++++++ test/Unit/Task/MagoFormatterTest.php | 158 +++++++++++++ test/Unit/Task/MagoGuardTest.php | 317 +++++++++++++++++++++++++++ test/Unit/Task/MagoLinterTest.php | 282 ++++++++++++++++++++++++ test/Unit/Task/MagoTest.php | 276 ----------------------- 17 files changed, 1809 insertions(+), 421 deletions(-) create mode 100644 doc/tasks/mago/analyzer.md create mode 100644 doc/tasks/mago/formatter.md create mode 100644 doc/tasks/mago/guard.md create mode 100644 doc/tasks/mago/linter.md create mode 100644 src/Task/MagoAnalyzer.php create mode 100644 src/Task/MagoFormatter.php create mode 100644 src/Task/MagoGuard.php create mode 100644 src/Task/MagoLinter.php create mode 100644 test/Unit/Task/MagoAnalyzerTest.php create mode 100644 test/Unit/Task/MagoFormatterTest.php create mode 100644 test/Unit/Task/MagoGuardTest.php create mode 100644 test/Unit/Task/MagoLinterTest.php delete mode 100644 test/Unit/Task/MagoTest.php diff --git a/doc/tasks.md b/doc/tasks.md index 1a904bae0..1818c37b0 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -32,7 +32,10 @@ grumphp: infection: ~ jsonlint: ~ kahlan: ~ - mago: ~ + mago_analyze: ~ + mago_format: ~ + mago_guard: ~ + mago_lint: ~ make: ~ npm_script: ~ paratest: ~ @@ -101,6 +104,10 @@ Every task has its own default configuration. It is possible to overwrite the pa - [JsonLint](tasks/jsonlint.md) - [Kahlan](tasks/kahlan.md) - [Mago](tasks/mago.md) + - [Mago Analyzer](tasks/mago/analyzer.md) + - [Mago Formatter](tasks/mago/formatter.md) + - [Mago Guard](tasks/mago/guard.md) + - [Mago Linter](tasks/mago/linter.md) - [Make](tasks/make.md) - [NPM script](tasks/npm_script.md) - [Paratest](tasks/paratest.md) diff --git a/doc/tasks/mago.md b/doc/tasks/mago.md index e7653153d..8d18c30b0 100644 --- a/doc/tasks/mago.md +++ b/doc/tasks/mago.md @@ -1,91 +1,8 @@ # Mago -The Mago task runs the Mago's toolchain. +The Mago's toolchain. -***Composer*** - -``` -composer require --dev carthage-software/mago -``` - -***Config*** - -The task lives under the `mago` namespace and has following configurable parameters: - -```yaml -# grumphp.yml -grumphp: - tasks: - mago: - formatter: ~ - formatter_options: ~ - linter: ~ - linter_options: ~ - analyzer: ~ - analyzer_options: ~ - guard: ~ - guard_options: ~ -``` - -**formatter** - -*Default: `true`* - -Enable the Mago's formatter. - - -**formatter_options** - -*Default: `['--staged']`* - -[Options](https://mago.carthage.software/tools/formatter/command-reference#options) for the `mago format` command. -Each option must be an array's element. -If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. - - -**linter** - -*Default: `true`* - -Enable the Mago's linter. - - -**linter_options** - -*Default: `['--staged']`* - -[Options](https://mago.carthage.software/tools/linter/command-reference#options) for the `mago lint` command. -Each option must be an array's element. -If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. - - -**analyzer** - -*Default: `true`* - -Enable the Mago's analyzer. - - -**analyzer_options** - -*Default: `['--staged']`* - -[Options](https://mago.carthage.software/tools/analyzer/command-reference#options) for the `mago analyze` command. -Each option must be an array's element. -If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. - - -**guard** - -*Default: `false`* - -Enable the architectural guard. - - -**guard_options** - -*Default: `[]`* - -[Options](https://mago.carthage.software/tools/guard/command-reference#options) for the `mago guard` command. -Each option must be an array's element. -If the option needs a value, add it after the option name with an equal sign like this: `--option=value`. +- [mago_analyze](mago/analyzer.md) +- [mago_format](mago/formatter.md) +- [mago_guard](mago/guard.md) +- [mago_lint](mago/linter.md) diff --git a/doc/tasks/mago/analyzer.md b/doc/tasks/mago/analyzer.md new file mode 100644 index 000000000..063371707 --- /dev/null +++ b/doc/tasks/mago/analyzer.md @@ -0,0 +1,119 @@ +# Mago Analyzer + +Perform deep static analysis on PHP code including type checking, control flow analysis, and detection of logical errors. + +## Composer + +```bash +composer require --dev carthage-software/mago +``` + +## Config + +The task lives under the `mago_analyze` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + mago_analyze: + no-stubs: ~ + staged: ~ + retain-codes: [] + ignore-baseline: ~ + fix: ~ + fail-on-remaining: ~ + sort: ~ + fixable-only: ~ + reporting-format: ~ + reporting-target: ~ + minimum-report-level: ~ + minimum-fail-level: ~ + dry-run: ~ +``` + +**no-stubs** + +*Type: bool* + +Disable built-in PHP and library stubs for analysis. By default, the analyzer uses stubs for built-in PHP functions and popular libraries to provide accurate type information. Disabling this may result in more reported issues when external symbols can't be resolved. + +**staged** + +*Type: bool* + +Only analyze files that are staged in git. Designed for git pre-commit hooks. Fails if not in a git repository. + +**retain-codes** + +*Type: string[] — Default: []* + +Reporting filter: only display issues matching the specified rule codes (e.g. `invalid-argument`, `semantics`). All rules still run; only the output is filtered. Can be specified multiple times. + +**ignore-baseline** + +*Type: bool* + +Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago analyze --generate-baseline`. + +**fix** + +*Default: null* + +Apply automatic fixes to the source code. Accepted values: + +- `safe` — apply only safe fixes (default fix mode) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior + +Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. + +**fail-on-remaining** + +*Type: bool* + +Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. + +**sort** + +*Type: bool* + +Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. + +**fixable-only** + +*Type: bool* + +Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. + +**reporting-format** + +*Default: null (mago default: medium)* + +Output format for issue reports. Not available when using `fix`. Possible values: + +`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` + +**reporting-target** + +*Default: null (mago default: stdout)* + +Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` + +**minimum-report-level** + +*Default: null (mago default: all levels)* + +Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` + +**minimum-fail-level** + +*Default: null (mago default: error)* + +Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` + +**dry-run** + +*Type: bool* + +Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/doc/tasks/mago/formatter.md b/doc/tasks/mago/formatter.md new file mode 100644 index 000000000..9e51ebeca --- /dev/null +++ b/doc/tasks/mago/formatter.md @@ -0,0 +1,32 @@ +# Mago Formatter + +Automatically format PHP code to match the configured style preferences. + +## Composer + +```bash +composer require --dev carthage-software/mago +``` + +## Config + +The task lives under the `mago_format` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + mago_format: + type: default +``` + +**type** + +*Default: default — Possible values: `default`, `dry-run`, `check`, `staged`* + +Controls how the formatter runs: + +- `default` — apply formatting changes in-place +- `dry-run` — print a diff of changes without modifying any files +- `check` — exit with failure if any file would be changed, without modifying files. Ideal for CI environments +- `staged` — format files currently staged in git. Designed for git pre-commit hooks. Fails if not in a git repository diff --git a/doc/tasks/mago/guard.md b/doc/tasks/mago/guard.md new file mode 100644 index 000000000..f9ca33738 --- /dev/null +++ b/doc/tasks/mago/guard.md @@ -0,0 +1,123 @@ +# Mago Guard + +Enforce architectural rules and layer dependencies. Checks that code follows defined architectural constraints, such as ensuring that certain layers don't depend on others. + +## Composer + +```bash +composer require --dev carthage-software/mago +``` + +## Config + +The task lives under the `mago_guard` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + mago_guard: + no-stubs: ~ + checks: all + retain-codes: [] + ignore-baseline: ~ + fix: ~ + fail-on-remaining: ~ + sort: ~ + fixable-only: ~ + reporting-format: ~ + reporting-target: ~ + minimum-report-level: ~ + minimum-fail-level: ~ + dry-run: ~ +``` + +**no-stubs** + +*Type: bool* + +Disable built-in PHP and library stubs. By default, guard uses stubs for built-in PHP functions and popular libraries to provide accurate symbol information. Disabling this may result in more warnings when external symbols can't be resolved. + +**checks** + +*Default: all — Possible values: `all`, `structural`, `perimeter`* + +Controls which checks are run: + +- `all` — run both structural and perimeter checks +- `structural` — run only structural checks (naming conventions, modifiers, inheritance constraints) +- `perimeter` — run only perimeter checks (dependency boundaries, layer restrictions) + +**retain-codes** + +*Type: string[] — Default: []* + +Reporting filter: only display issues matching the specified rule codes. All rules still run; only the output is filtered. Can be specified multiple times. + +**ignore-baseline** + +*Type: bool* + +Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago guard --generate-baseline`. + +**fix** + +*Default: null* + +Apply automatic fixes to the source code. Accepted values: + +- `safe` — apply only safe fixes (default fix mode) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior + +Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. + +**fail-on-remaining** + +*Type: bool* + +Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. + +**sort** + +*Type: bool* + +Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. + +**fixable-only** + +*Type: bool* + +Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. + +**reporting-format** + +*Default: null (mago default: medium)* + +Output format for issue reports. Not available when using `fix`. Possible values: + +`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` + +**reporting-target** + +*Default: null (mago default: stdout)* + +Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` + +**minimum-report-level** + +*Default: null (mago default: all levels)* + +Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` + +**minimum-fail-level** + +*Default: null (mago default: error)* + +Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` + +**dry-run** + +*Type: bool* + +Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/doc/tasks/mago/linter.md b/doc/tasks/mago/linter.md new file mode 100644 index 000000000..d9a46d591 --- /dev/null +++ b/doc/tasks/mago/linter.md @@ -0,0 +1,135 @@ +# Mago Linter + +Run linting rules on PHP code to identify style violations, code smells, and potential bugs. + +## Composer + +```bash +composer require --dev carthage-software/mago +``` + +## Config + +The task lives under the `mago_lint` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + mago_lint: + semantics: ~ + pedantic: ~ + only: [] + staged: ~ + retain-codes: [] + ignore-baseline: ~ + fix: ~ + fail-on-remaining: ~ + sort: ~ + fixable-only: ~ + reporting-format: ~ + reporting-target: ~ + minimum-report-level: ~ + minimum-fail-level: ~ + dry-run: ~ +``` + +**semantics** + +*Type: bool* + +Skip linter rules and only perform basic syntax and semantic validation. Checks that your PHP code parses correctly and has valid semantic structure, without applying any style or quality rules. Useful for quick syntax validation. + +**pedantic** + +*Type: bool* + +Enable every available linter rule for maximum thoroughness. Overrides your configuration and enables all rules, including those disabled by default. The output will be extremely verbose and is not recommended for regular use. Useful for comprehensive code audits. + +**only** + +*Type: string[] — Default: []* + +Run only the specified rules, ignoring the configuration file. Provide a list of rule codes (e.g. `invalid-argument`, `semantics`). Overrides your `mago.toml` configuration and is useful for targeted analysis. + +**staged** + +*Type: bool* + +Only lint files that are staged in git. Designed for git pre-commit hooks. Fails if not in a git repository. + +**retain-codes** + +*Type: string[] — Default: []* + +Reporting filter: only display issues matching the specified rule codes (e.g. `invalid-argument`, `semantics`). All rules still run; only the output is filtered. Can be specified multiple times. + +Note: this differs from `only`, which restricts which rules are executed. + +**ignore-baseline** + +*Type: bool* + +Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago lint --generate-baseline`. + +**fix** + +*Default: null* + +Apply automatic fixes to the source code. Accepted values: + +- `safe` — apply only safe fixes (default fix mode) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior + +Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. + +**fail-on-remaining** + +*Type: bool* + +Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. + +**sort** + +*Type: bool* + +Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. + +**fixable-only** + +*Type: bool* + +Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. + +**reporting-format** + +*Default: null (mago default: medium)* + +Output format for issue reports. Not available when using `fix`. Possible values: + +`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` + +**reporting-target** + +*Default: null (mago default: stdout)* + +Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` + +**minimum-report-level** + +*Default: null (mago default: all levels)* + +Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` + +**minimum-fail-level** + +*Default: null (mago default: error)* + +Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` + +**dry-run** + +*Type: bool* + +Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index 99b1bb274..1af87b2f5 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -176,6 +176,34 @@ services: tags: - {name: grumphp.task, task: mago} + GrumPHP\Task\MagoAnalyzer: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - { name: grumphp.task, task: mago_analyze } + + GrumPHP\Task\MagoFormatter: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - { name: grumphp.task, task: mago_format } + + GrumPHP\Task\MagoGuard: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - { name: grumphp.task, task: mago_guard } + + GrumPHP\Task\MagoLinter: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - { name: grumphp.task, task: mago_lint } + GrumPHP\Task\Make: arguments: - '@process_builder' diff --git a/src/Task/Mago.php b/src/Task/Mago.php index 2027939c0..a5e30d3be 100644 --- a/src/Task/Mago.php +++ b/src/Task/Mago.php @@ -4,6 +4,7 @@ namespace GrumPHP\Task; +use GrumPHP\Collection\ProcessArgumentsCollection; use GrumPHP\Formatter\ProcessFormatterInterface; use GrumPHP\Runner\TaskResult; use GrumPHP\Runner\TaskResultInterface; @@ -21,77 +22,124 @@ class Mago extends AbstractExternalTask public static function getConfigurableOptions(): ConfigOptionsResolver { - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'formatter' => true, - 'formatter_options' => ['--staged'], - 'linter' => true, - 'linter_options' => ['--staged'], - 'analyzer' => true, - 'analyzer_options' => ['--staged'], - 'guard' => false, - 'guard_options' => [], - ]); - - $resolver->addAllowedTypes('formatter', ['bool']); - $resolver->addAllowedTypes('formatter_options', ['array']); - $resolver->addAllowedTypes('linter', ['bool']); - $resolver->addAllowedTypes('linter_options', ['array']); - $resolver->addAllowedTypes('analyzer', ['bool']); - $resolver->addAllowedTypes('analyzer_options', ['array']); - $resolver->addAllowedTypes('guard', ['bool']); - $resolver->addAllowedTypes('guard_options', ['array']); - - return ConfigOptionsResolver::fromOptionsResolver($resolver); + return ConfigOptionsResolver::fromOptionsResolver(new OptionsResolver()); } - /** - * {@inheritdoc} - */ public function canRunInContext(ContextInterface $context): bool { return $context instanceof GitPreCommitContext || $context instanceof RunContext; } - /** - * {@inheritdoc} - */ public function run(ContextInterface $context): TaskResultInterface { - $config = $this->getConfig()->getOptions(); - - if ($config['formatter'] === false - && $config['linter'] === false - && $config['analyzer'] === false - && $config['guard'] === false) { - return TaskResult::createSkipped($this, $context); - } - - $commandMap = [ - 'formatter' => 'fmt', - 'linter' => 'lint', - 'analyzer' => 'analyze', - 'guard' => 'guard', - ]; + return TaskResult::createFailed( + $this, + $context, + 'The mago task is split into 4 distinct tasks.'.PHP_EOL + . 'Please use the following tasks instead:'.PHP_EOL.PHP_EOL + . '- mago_analyze ' + . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/analyzer.md)'.PHP_EOL + . '- mago_format ' + . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/formatter.md)'.PHP_EOL + . '- mago_guard ' + . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/guard.md)'.PHP_EOL + . '- mago_lint ' + . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/linter.md)'.PHP_EOL + ); + } - foreach ($commandMap as $configKey => $command) { - if ($config[$configKey] !== true) { - continue; - } + protected static function configureSharedOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ]); - $arguments = $this->processBuilder->createArgumentsForCommand('mago'); - $arguments->add($command); + $resolver->addAllowedTypes('retain-codes', ['array']); + $resolver->addAllowedTypes('ignore-baseline', ['null', 'bool']); + $resolver->addAllowedTypes('fix', ['null', 'string']); + $resolver->addAllowedTypes('fail-on-remaining', ['null', 'bool']); + $resolver->addAllowedTypes('sort', ['null', 'bool']); + $resolver->addAllowedTypes('fixable-only', ['null', 'bool']); + $resolver->addAllowedTypes('reporting-format', ['null', 'string']); + $resolver->addAllowedTypes('reporting-target', ['null', 'string']); + $resolver->addAllowedTypes('minimum-report-level', ['null', 'string']); + $resolver->addAllowedTypes('minimum-fail-level', ['null', 'string']); + $resolver->addAllowedTypes('dry-run', ['null', 'bool']); + + $resolver->addAllowedValues('fix', [null, 'safe', 'potentially-unsafe', 'unsafe']); + $resolver->addAllowedValues('reporting-format', [ + null, 'rich', 'medium', 'short', 'ariadne', 'github', 'gitlab', + 'json', 'count', 'code-count', 'checkstyle', 'emacs', 'sarif', + ]); + $resolver->addAllowedValues('reporting-target', [null, 'stdout', 'stderr']); + $resolver->addAllowedValues('minimum-report-level', [null, 'note', 'help', 'warning', 'error']); + $resolver->addAllowedValues('minimum-fail-level', [null, 'note', 'help', 'warning', 'error']); + } - $arguments->addArgumentArray('%s', $config[$configKey . '_options']); + protected function resolveFixOption(array $config): ?string + { + return $config['fix']; + } - $process = $this->processBuilder->buildProcess($arguments); - $process->run(); + protected function validateFixCompatibility( + array $config, + ?string $fix, + ContextInterface $context + ): ?TaskResultInterface { + $error = match (true) { + null === $fix && $config['fail-on-remaining'] + => 'Fail on remaining option is only supported with fix option.', + null === $fix && $config['dry-run'] + => 'Dry run option is only supported with fix option.', + null !== $fix && $config['fixable-only'] + => 'Fixable-only option is not supported with fix option.', + null !== $fix && $config['reporting-format'] + => 'Reporting format option is not supported with fix option.', + null !== $fix && $config['reporting-target'] + => 'Reporting target option is not supported with fix option.', + default => null, + }; + + return null !== $error ? TaskResult::createFailed($this, $context, $error) : null; + } - if (!$process->isSuccessful()) { - return TaskResult::createFailed($this, $context, $this->formatter->format($process)); - } + protected function addFixArguments( + ProcessArgumentsCollection $arguments, + array $config, + ?string $fix + ): void { + if (null === $fix) { + return; } - return TaskResult::createPassed($this, $context); + $arguments->add('--fix'); + $arguments->addOptionalArgument('--potentially-unsafe', 'potentially-unsafe' === $fix); + $arguments->addOptionalArgument('--unsafe', 'unsafe' === $fix); + $arguments->addOptionalArgument('--fail-on-remaining', $config['fail-on-remaining']); + $arguments->addOptionalArgument('--dry-run', $config['dry-run']); + } + + protected function addSharedArguments( + ProcessArgumentsCollection $arguments, + array $config + ): void { + $arguments->addOptionalArgument('--fixable-only', $config['fixable-only']); + $arguments->addOptionalArgumentWithSeparatedValue('--reporting-format', $config['reporting-format']); + $arguments->addOptionalArgumentWithSeparatedValue('--reporting-target', $config['reporting-target']); + $arguments->addArgumentArrayWithSeparatedValue('--retain-code', $config['retain-codes']); + $arguments->addOptionalArgumentWithSeparatedValue('--minimum-report-level', $config['minimum-report-level']); + $arguments->addOptionalArgumentWithSeparatedValue('--minimum-fail-level', $config['minimum-fail-level']); + $arguments->addOptionalArgument('--ignore-baseline', $config['ignore-baseline']); + $arguments->addOptionalArgument('--sort', $config['sort']); } } diff --git a/src/Task/MagoAnalyzer.php b/src/Task/MagoAnalyzer.php new file mode 100644 index 000000000..ae943ae8b --- /dev/null +++ b/src/Task/MagoAnalyzer.php @@ -0,0 +1,59 @@ +setDefaults([ + 'no-stubs' => null, + 'staged' => null, + ]); + + $resolver->addAllowedTypes('no-stubs', ['null', 'bool']); + $resolver->addAllowedTypes('staged', ['null', 'bool']); + + self::configureSharedOptions($resolver); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $fix = $this->resolveFixOption($config); + + if ($error = $this->validateFixCompatibility($config, $fix, $context)) { + return $error; + } + + $arguments = $this->processBuilder->createArgumentsForCommand('mago'); + $arguments->add('analyze'); + + $this->addFixArguments($arguments, $config, $fix); + $this->addSharedArguments($arguments, $config); + + $arguments->addOptionalArgument('--no-stubs', $config['no-stubs']); + $arguments->addOptionalArgument('--staged', $config['staged']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/src/Task/MagoFormatter.php b/src/Task/MagoFormatter.php new file mode 100644 index 000000000..f008f8a82 --- /dev/null +++ b/src/Task/MagoFormatter.php @@ -0,0 +1,48 @@ +setDefaults([ + 'type' => 'default', + ]); + + $resolver->addAllowedTypes('type', ['string']); + $resolver->addAllowedValues('type', ['default', 'dry-run', 'check', 'staged']); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $arguments = $this->processBuilder->createArgumentsForCommand('mago'); + $arguments->add('format'); + + $arguments->addOptionalArgument('--dry-run', 'dry-run' === $config['type']); + $arguments->addOptionalArgument('--check', 'check' === $config['type']); + $arguments->addOptionalArgument('--staged', 'staged' === $config['type']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/src/Task/MagoGuard.php b/src/Task/MagoGuard.php new file mode 100644 index 000000000..c7e7a1a80 --- /dev/null +++ b/src/Task/MagoGuard.php @@ -0,0 +1,61 @@ +setDefaults([ + 'no-stubs' => null, + 'checks' => 'all', + ]); + + $resolver->addAllowedTypes('no-stubs', ['null', 'bool']); + $resolver->addAllowedTypes('checks', ['string']); + $resolver->addAllowedValues('checks', ['all', 'structural', 'perimeter']); + + self::configureSharedOptions($resolver); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $fix = $this->resolveFixOption($config); + + if ($error = $this->validateFixCompatibility($config, $fix, $context)) { + return $error; + } + + $arguments = $this->processBuilder->createArgumentsForCommand('mago'); + $arguments->add('guard'); + + $this->addFixArguments($arguments, $config, $fix); + $this->addSharedArguments($arguments, $config); + + $arguments->addOptionalArgument('--no-stubs', $config['no-stubs']); + $arguments->addOptionalArgument('--structural', 'structural' === $config['checks']); + $arguments->addOptionalArgument('--perimeter', 'perimeter' === $config['checks']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/src/Task/MagoLinter.php b/src/Task/MagoLinter.php new file mode 100644 index 000000000..8b3eed3b9 --- /dev/null +++ b/src/Task/MagoLinter.php @@ -0,0 +1,65 @@ +setDefaults([ + 'semantics' => null, + 'pedantic' => null, + 'only' => [], + 'staged' => null, + ]); + + $resolver->addAllowedTypes('semantics', ['null', 'bool']); + $resolver->addAllowedTypes('pedantic', ['null', 'bool']); + $resolver->addAllowedTypes('only', ['array']); + $resolver->addAllowedTypes('staged', ['null', 'bool']); + + self::configureSharedOptions($resolver); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $fix = $this->resolveFixOption($config); + + if ($error = $this->validateFixCompatibility($config, $fix, $context)) { + return $error; + } + + $arguments = $this->processBuilder->createArgumentsForCommand('mago'); + $arguments->add('lint'); + + $this->addFixArguments($arguments, $config, $fix); + $this->addSharedArguments($arguments, $config); + + $arguments->addOptionalCommaSeparatedArgument('--only=%s', $config['only']); + $arguments->addOptionalArgument('--semantics', $config['semantics']); + $arguments->addOptionalArgument('--pedantic', $config['pedantic']); + $arguments->addOptionalArgument('--staged', $config['staged']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/test/Unit/Task/MagoAnalyzerTest.php b/test/Unit/Task/MagoAnalyzerTest.php new file mode 100644 index 000000000..eb3e08227 --- /dev/null +++ b/test/Unit/Task/MagoAnalyzerTest.php @@ -0,0 +1,265 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'no-stubs' => null, + 'staged' => null, + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ] + ]; + + yield 'invalid-fix' => [['fix' => 'invalid'], null]; + yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; + yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; + yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; + } + + public static function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + self::mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + self::mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + self::mockContext() + ]; + } + + public static function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + ]; + + yield 'fail-on-remaining-without-fix' => [ + ['fail-on-remaining' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fail on remaining option is only supported with fix option.', + ]; + + yield 'dry-run-without-fix' => [ + ['dry-run' => true], + self::mockContext(RunContext::class), + function () {}, + 'Dry run option is only supported with fix option.', + ]; + + yield 'fixable-only-with-fix' => [ + ['fix' => 'safe', 'fixable-only' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fixable-only option is not supported with fix option.', + ]; + + yield 'reporting-format-with-fix' => [ + ['fix' => 'safe', 'reporting-format' => 'json'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting format option is not supported with fix option.', + ]; + + yield 'reporting-target-with-fix' => [ + ['fix' => 'safe', 'reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting target option is not supported with fix option.', + ]; + } + + public static function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', self::mockProcess(0)); + } + ]; + } + + #[Test] + #[DataProvider('provideSkipsOnStuff')] + public function it_skips_on_stuff( + array $config, + ContextInterface $context, + callable $configurator + ): void + { + self::markTestSkipped('No skip scenarios defined yet'); + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-skip-scenarios' => [ + [], + self::mockContext(RunContext::class), + function () {} + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class), + 'mago', + ['analyze'] + ]; + + yield 'no-stubs' => [ + ['no-stubs' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--no-stubs'] + ]; + + yield 'fix-safe' => [ + ['fix' => 'safe'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fix'] + ]; + + yield 'fix-potentially-unsafe' => [ + ['fix' => 'potentially-unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fix', '--potentially-unsafe'] + ]; + + yield 'fix-unsafe' => [ + ['fix' => 'unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fix', '--unsafe'] + ]; + + yield 'fix-with-fail-on-remaining' => [ + ['fix' => 'safe', 'fail-on-remaining' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fix', '--fail-on-remaining'] + ]; + + yield 'fix-with-dry-run' => [ + ['fix' => 'safe', 'dry-run' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fix', '--dry-run'] + ]; + + yield 'fixable-only' => [ + ['fixable-only' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--fixable-only'] + ]; + + yield 'reporting-format' => [ + ['reporting-format' => 'json'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--reporting-format', 'json'] + ]; + + yield 'reporting-target' => [ + ['reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--reporting-target', 'stderr'] + ]; + + yield 'retain-codes' => [ + ['retain-codes' => ['invalid-argument', 'semantics']], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] + ]; + + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--minimum-report-level', 'warning'] + ]; + + yield 'minimum-fail-level' => [ + ['minimum-fail-level' => 'error'], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--minimum-fail-level', 'error'] + ]; + + yield 'ignore-baseline' => [ + ['ignore-baseline' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--ignore-baseline'] + ]; + + yield 'sort' => [ + ['sort' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--sort'] + ]; + + yield 'staged' => [ + ['staged' => true], + self::mockContext(RunContext::class), + 'mago', + ['analyze', '--staged'] + ]; + } +} diff --git a/test/Unit/Task/MagoFormatterTest.php b/test/Unit/Task/MagoFormatterTest.php new file mode 100644 index 000000000..7eea87a79 --- /dev/null +++ b/test/Unit/Task/MagoFormatterTest.php @@ -0,0 +1,158 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + ['type' => 'default'] + ]; + + yield 'type-default' => [ + ['type' => 'default'], + ['type' => 'default'] + ]; + + yield 'type-dry-run' => [ + ['type' => 'dry-run'], + ['type' => 'dry-run'] + ]; + + yield 'type-check' => [ + ['type' => 'check'], + ['type' => 'check'] + ]; + + yield 'type-staged' => [ + ['type' => 'staged'], + ['type' => 'staged'] + ]; + + yield 'invalid-type' => [ + ['type' => 'invalid'], + null + ]; + } + + public static function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + self::mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + self::mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + self::mockContext() + ]; + } + + public static function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + ]; + } + + public static function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', self::mockProcess(0)); + } + ]; + } + + #[Test] + #[DataProvider('provideSkipsOnStuff')] + public function it_skips_on_stuff( + array $config, + ContextInterface $context, + callable $configurator + ): void + { + self::markTestSkipped('No skip scenarios defined yet'); + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-skip-scenarios' => [ + [], + self::mockContext(RunContext::class), + function () {} + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class), + 'mago', + ['format'] + ]; + + yield 'type-default' => [ + ['type' => 'default'], + self::mockContext(RunContext::class), + 'mago', + ['format'] + ]; + + yield 'type-dry-run' => [ + ['type' => 'dry-run'], + self::mockContext(RunContext::class), + 'mago', + ['format', '--dry-run'] + ]; + + yield 'type-check' => [ + ['type' => 'check'], + self::mockContext(RunContext::class), + 'mago', + ['format', '--check'] + ]; + + yield 'type-staged' => [ + ['type' => 'staged'], + self::mockContext(RunContext::class), + 'mago', + ['format', '--staged'] + ]; + } +} diff --git a/test/Unit/Task/MagoGuardTest.php b/test/Unit/Task/MagoGuardTest.php new file mode 100644 index 000000000..b5951974c --- /dev/null +++ b/test/Unit/Task/MagoGuardTest.php @@ -0,0 +1,317 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'no-stubs' => null, + 'checks' => 'all', + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ] + ]; + + yield 'checks-structural' => [ + ['checks' => 'structural'], + [ + 'no-stubs' => null, + 'checks' => 'structural', + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ] + ]; + + yield 'checks-perimeter' => [ + ['checks' => 'perimeter'], + [ + 'no-stubs' => null, + 'checks' => 'perimeter', + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ] + ]; + yield 'invalid-checks' => [['checks' => 'invalid'], null]; + yield 'invalid-fix' => [['fix' => 'invalid'], null]; + yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; + yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; + yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; + } + + public static function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + self::mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + self::mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + self::mockContext() + ]; + } + + public static function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + ]; + + yield 'fail-on-remaining-without-fix' => [ + ['fail-on-remaining' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fail on remaining option is only supported with fix option.', + ]; + + yield 'dry-run-without-fix' => [ + ['dry-run' => true], + self::mockContext(RunContext::class), + function () {}, + 'Dry run option is only supported with fix option.', + ]; + + yield 'fixable-only-with-fix' => [ + ['fix' => 'safe', 'fixable-only' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fixable-only option is not supported with fix option.', + ]; + + yield 'reporting-format-with-fix' => [ + ['fix' => 'safe', 'reporting-format' => 'json'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting format option is not supported with fix option.', + ]; + + yield 'reporting-target-with-fix' => [ + ['fix' => 'safe', 'reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting target option is not supported with fix option.', + ]; + } + + public static function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', self::mockProcess(0)); + } + ]; + } + + #[Test] + #[DataProvider('provideSkipsOnStuff')] + public function it_skips_on_stuff( + array $config, + ContextInterface $context, + callable $configurator + ): void + { + self::markTestSkipped('No skip scenarios defined yet'); + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-skip-scenarios' => [ + [], + self::mockContext(RunContext::class), + function () {} + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class), + 'mago', + ['guard'] + ]; + + yield 'no-stubs' => [ + ['no-stubs' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--no-stubs'] + ]; + + yield 'checks-structural' => [ + ['checks' => 'structural'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--structural'] + ]; + + yield 'checks-perimeter' => [ + ['checks' => 'perimeter'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--perimeter'] + ]; + + yield 'checks-all' => [ + ['checks' => 'all'], + self::mockContext(RunContext::class), + 'mago', + ['guard'] + ]; + + yield 'fix-safe' => [ + ['fix' => 'safe'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fix'] + ]; + + yield 'fix-potentially-unsafe' => [ + ['fix' => 'potentially-unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fix', '--potentially-unsafe'] + ]; + + yield 'fix-unsafe' => [ + ['fix' => 'unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fix', '--unsafe'] + ]; + + yield 'fix-with-fail-on-remaining' => [ + ['fix' => 'safe', 'fail-on-remaining' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fix', '--fail-on-remaining'] + ]; + + yield 'fix-with-dry-run' => [ + ['fix' => 'safe', 'dry-run' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fix', '--dry-run'] + ]; + + yield 'fixable-only' => [ + ['fixable-only' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--fixable-only'] + ]; + + yield 'reporting-format' => [ + ['reporting-format' => 'json'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--reporting-format', 'json'] + ]; + + yield 'reporting-target' => [ + ['reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--reporting-target', 'stderr'] + ]; + + yield 'retain-codes' => [ + ['retain-codes' => ['invalid-argument', 'semantics']], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] + ]; + + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--minimum-report-level', 'warning'] + ]; + + yield 'minimum-fail-level' => [ + ['minimum-fail-level' => 'error'], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--minimum-fail-level', 'error'] + ]; + + yield 'ignore-baseline' => [ + ['ignore-baseline' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--ignore-baseline'] + ]; + + yield 'sort' => [ + ['sort' => true], + self::mockContext(RunContext::class), + 'mago', + ['guard', '--sort'] + ]; + } +} diff --git a/test/Unit/Task/MagoLinterTest.php b/test/Unit/Task/MagoLinterTest.php new file mode 100644 index 000000000..9ef284754 --- /dev/null +++ b/test/Unit/Task/MagoLinterTest.php @@ -0,0 +1,282 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'semantics' => null, + 'pedantic' => null, + 'only' => [], + 'staged' => null, + 'retain-codes' => [], + 'ignore-baseline' => null, + 'fix' => null, + 'fail-on-remaining' => null, + 'sort' => null, + 'fixable-only' => null, + 'reporting-format' => null, + 'reporting-target' => null, + 'minimum-report-level' => null, + 'minimum-fail-level' => null, + 'dry-run' => null, + ] + ]; + + yield 'invalid-fix' => [['fix' => 'invalid'], null]; + yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; + yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; + yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; + } + + public static function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + self::mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + self::mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + self::mockContext() + ]; + } + + public static function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + ]; + + yield 'fail-on-remaining-without-fix' => [ + ['fail-on-remaining' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fail on remaining option is only supported with fix option.', + ]; + + yield 'dry-run-without-fix' => [ + ['dry-run' => true], + self::mockContext(RunContext::class), + function () {}, + 'Dry run option is only supported with fix option.', + ]; + + yield 'fixable-only-with-fix' => [ + ['fix' => 'safe', 'fixable-only' => true], + self::mockContext(RunContext::class), + function () {}, + 'Fixable-only option is not supported with fix option.', + ]; + + yield 'reporting-format-with-fix' => [ + ['fix' => 'safe', 'reporting-format' => 'json'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting format option is not supported with fix option.', + ]; + + yield 'reporting-target-with-fix' => [ + ['fix' => 'safe', 'reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + function () {}, + 'Reporting target option is not supported with fix option.', + ]; + } + + public static function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + self::mockContext(RunContext::class), + function () { + $this->mockProcessBuilder('mago', self::mockProcess(0)); + } + ]; + } + + #[Test] + #[DataProvider('provideSkipsOnStuff')] + public function it_skips_on_stuff( + array $config, + ContextInterface $context, + callable $configurator + ): void + { + self::markTestSkipped('No skip scenarios defined yet'); + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-skip-scenarios' => [ + [], + self::mockContext(RunContext::class), + function () { + } + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class), + 'mago', + ['lint'] + ]; + + yield 'fix-safe' => [ + ['fix' => 'safe'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fix'] + ]; + + yield 'fix-potentially-unsafe' => [ + ['fix' => 'potentially-unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fix', '--potentially-unsafe'] + ]; + + yield 'fix-unsafe' => [ + ['fix' => 'unsafe'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fix', '--unsafe'] + ]; + + yield 'fix-with-fail-on-remaining' => [ + ['fix' => 'safe', 'fail-on-remaining' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fix', '--fail-on-remaining'] + ]; + + yield 'fix-with-dry-run' => [ + ['fix' => 'safe', 'dry-run' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fix', '--dry-run'] + ]; + + yield 'fixable-only' => [ + ['fixable-only' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--fixable-only'] + ]; + + yield 'reporting-format' => [ + ['reporting-format' => 'json'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--reporting-format', 'json'] + ]; + + yield 'reporting-target' => [ + ['reporting-target' => 'stderr'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--reporting-target', 'stderr'] + ]; + + yield 'only' => [ + ['only' => ['invalid-argument', 'semantics']], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--only=invalid-argument,semantics'] + ]; + + yield 'retain-codes' => [ + ['retain-codes' => ['invalid-argument', 'semantics']], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] + ]; + + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--minimum-report-level', 'warning'] + ]; + + yield 'minimum-fail-level' => [ + ['minimum-fail-level' => 'error'], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--minimum-fail-level', 'error'] + ]; + + yield 'semantics' => [ + ['semantics' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--semantics'] + ]; + + yield 'pedantic' => [ + ['pedantic' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--pedantic'] + ]; + + yield 'ignore-baseline' => [ + ['ignore-baseline' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--ignore-baseline'] + ]; + + yield 'sort' => [ + ['sort' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--sort'] + ]; + + yield 'staged' => [ + ['staged' => true], + self::mockContext(RunContext::class), + 'mago', + ['lint', '--staged'] + ]; + } +} diff --git a/test/Unit/Task/MagoTest.php b/test/Unit/Task/MagoTest.php deleted file mode 100644 index 610f0626f..000000000 --- a/test/Unit/Task/MagoTest.php +++ /dev/null @@ -1,276 +0,0 @@ -processBuilder->reveal(), - $this->formatter->reveal() - ); - } - - public static function provideConfigurableOptions(): iterable - { - yield 'defaults' => [ - [], - [ - 'formatter' => true, - 'formatter_options' => ['--staged'], - 'linter' => true, - 'linter_options' => ['--staged'], - 'analyzer' => true, - 'analyzer_options' => ['--staged'], - 'guard' => false, - 'guard_options' => [], - ] - ]; - } - - public static function provideRunContexts(): iterable - { - yield 'run-context' => [ - true, - self::mockContext(RunContext::class) - ]; - - yield 'pre-commit-context' => [ - true, - self::mockContext(GitPreCommitContext::class) - ]; - - yield 'other' => [ - false, - self::mockContext() - ]; - } - - public static function provideFailsOnStuff(): iterable - { - yield 'exitCode1' => [ - [], - self::mockContext(RunContext::class, ['hello.php']), - function () { - $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); - $this->formatter->format($process)->willReturn('nope'); - }, - 'nope' - ]; - } - - public static function providePassesOnStuff(): iterable - { - yield 'exitCode0' => [ - [], - self::mockContext(RunContext::class, ['hello.php']), - function () { - $this->mockProcessBuilder('mago', self::mockProcess(0)); - } - ]; - } - - public static function provideSkipsOnStuff(): iterable - { - yield 'no-commands' => [ - [ - 'formatter' => false, - 'linter' => false, - 'analyzer' => false, - 'guard' => false, - ], - self::mockContext(RunContext::class, ['file.php']), - function () {} - ]; - } - - public static function provideExternalTaskRuns(): iterable - { - yield 'formatter' => [ - [ - 'formatter' => true, - 'formatter_options' => ['--check'], - 'linter' => false, - 'linter_options' => [], - 'analyzer' => false, - 'analyzer_options' => [], - 'guard' => false, - 'guard_options' => [], - ], - self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), - 'mago', - [ - 'fmt', - '--check', - ] - ]; - - yield 'linter' => [ - [ - 'formatter' => false, - 'formatter_options' => [], - 'linter' => true, - 'linter_options' => ['--semantics'], - 'analyzer' => false, - 'analyzer_options' => [], - 'guard' => false, - 'guard_options' => [], - ], - self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), - 'mago', - [ - 'lint', - '--semantics', - ] - ]; - - yield 'analyzer' => [ - [ - 'formatter' => false, - 'formatter_options' => [], - 'linter' => false, - 'linter_options' => [], - 'analyzer' => true, - 'analyzer_options' => ['--no-stubs'], - 'guard' => false, - 'guard_options' => [], - ], - self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), - 'mago', - [ - 'analyze', - '--no-stubs', - ] - ]; - - yield 'architectural-guard' => [ - [ - 'formatter' => false, - 'formatter_options' => [], - 'linter' => false, - 'linter_options' => [], - 'analyzer' => false, - 'analyzer_options' => [], - 'guard' => true, - 'guard_options' => ['--structural'], - ], - self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), - 'mago', - [ - 'guard', - '--structural', - ] - ]; - } - - #[DataProvider('provideMultipleExternalTaskRuns')] - #[Test] - public function it_runs_multiple_external_commands( - array $config, - ContextInterface $context, - string $taskName, - array $expectedCommands - ): void { - $task = $this->configureTask($config); - - $this->processBuilder->createArgumentsForCommand($taskName)->will(function () { - return new \GrumPHP\Collection\ProcessArgumentsCollection(); - }); - - $count = 0; - $this->processBuilder->buildProcess(Argument::any()) - ->shouldBeCalledTimes(count($expectedCommands)) - ->will(function ($parameters) use ($expectedCommands, &$count) { - $cliArguments = $expectedCommands[$count++]; - $processArguments = $parameters[0]->getValues(); - \PHPUnit\Framework\Assert::assertSame($cliArguments, $processArguments); - - return MagoTest::mockProcess(0); - }); - - $result = $task->run($context); - self::assertInstanceOf(\GrumPHP\Runner\TaskResultInterface::class, $result); - self::assertTrue($result->isPassed()); - } - - public static function provideMultipleExternalTaskRuns(): iterable - { - yield 'defaults' => [ - [], - self::mockContext(RunContext::class, ['hello.php']), - 'mago', - [ - ['fmt', '--staged'], - ['lint', '--staged'], - ['analyze', '--staged'], - ] - ]; - - yield 'formatter-and-linter' => [ - [ - 'formatter' => true, - 'formatter_options' => ['--staged'], - 'linter' => true, - 'linter_options' => ['--staged'], - 'analyzer' => false, - 'analyzer_options' => [], - 'guard' => false, - 'guard_options' => [], - ], - self::mockContext(RunContext::class, ['hello.php']), - 'mago', - [ - ['fmt', '--staged'], - ['lint', '--staged'], - ] - ]; - - yield 'all-commands' => [ - [ - 'formatter' => true, - 'formatter_options' => ['--staged'], - 'linter' => true, - 'linter_options' => ['--staged', '--semantics', '--minimum-report-level=warning'], - 'analyzer' => true, - 'analyzer_options' => ['--staged'], - 'guard' => true, - 'guard_options' => ['--structural'], - ], - self::mockContext(RunContext::class, ['hello.php']), - 'mago', - [ - [ - 'fmt', - '--staged', - ], - [ - 'lint', - '--staged', - '--semantics', - '--minimum-report-level=warning', - ], - [ - 'analyze', - '--staged', - ], - [ - 'guard', - '--structural', - ], - ] - ]; - } -} From 815490caacff28a37b02ba940d07c50ed4aa01ae Mon Sep 17 00:00:00 2001 From: Sylvain VANEL Date: Sat, 13 Jun 2026 18:06:06 +0200 Subject: [PATCH 4/4] Refactor Mago tasks based on threads --- doc/tasks.md | 2 +- doc/tasks/mago.md | 8 -- doc/tasks/mago/analyzer.md | 72 ++-------- doc/tasks/mago/formatter.md | 22 ++-- doc/tasks/mago/guard.md | 82 ++++-------- doc/tasks/mago/linter.md | 72 ++-------- resources/config/tasks.yml | 7 - src/Task/Mago.php | 148 ++++++++------------- src/Task/MagoAnalyzer.php | 34 +++-- src/Task/MagoFormatter.php | 35 +++-- src/Task/MagoGuard.php | 19 +-- src/Task/MagoLinter.php | 13 +- test/Unit/Task/MagoAnalyzerTest.php | 134 ++----------------- test/Unit/Task/MagoFormatterTest.php | 73 +++-------- test/Unit/Task/MagoGuardTest.php | 189 ++++++--------------------- test/Unit/Task/MagoLinterTest.php | 154 ++++------------------ 16 files changed, 263 insertions(+), 801 deletions(-) delete mode 100644 doc/tasks/mago.md diff --git a/doc/tasks.md b/doc/tasks.md index 1818c37b0..fd720deb4 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -103,7 +103,7 @@ Every task has its own default configuration. It is possible to overwrite the pa - [Infection](tasks/infection.md) - [JsonLint](tasks/jsonlint.md) - [Kahlan](tasks/kahlan.md) -- [Mago](tasks/mago.md) +- Mago - [Mago Analyzer](tasks/mago/analyzer.md) - [Mago Formatter](tasks/mago/formatter.md) - [Mago Guard](tasks/mago/guard.md) diff --git a/doc/tasks/mago.md b/doc/tasks/mago.md deleted file mode 100644 index 8d18c30b0..000000000 --- a/doc/tasks/mago.md +++ /dev/null @@ -1,8 +0,0 @@ -# Mago - -The Mago's toolchain. - -- [mago_analyze](mago/analyzer.md) -- [mago_format](mago/formatter.md) -- [mago_guard](mago/guard.md) -- [mago_lint](mago/linter.md) diff --git a/doc/tasks/mago/analyzer.md b/doc/tasks/mago/analyzer.md index 063371707..6218682f7 100644 --- a/doc/tasks/mago/analyzer.md +++ b/doc/tasks/mago/analyzer.md @@ -8,6 +8,12 @@ Perform deep static analysis on PHP code including type checking, control flow a composer require --dev carthage-software/mago ``` +## Behavior + +The task runs `mago analyze` directly to get full diagnostic output. When running in a `git pre-commit` context, only staged files are analyzed (`--staged`). In a `run` context, all files are analyzed. + +If the task fails, GrumPHP will offer to re-run with `--fix` applied. The fix mode can be configured via `fix-mode`. + ## Config The task lives under the `mago_analyze` namespace and has following configurable parameters: @@ -18,18 +24,11 @@ grumphp: tasks: mago_analyze: no-stubs: ~ - staged: ~ retain-codes: [] ignore-baseline: ~ - fix: ~ - fail-on-remaining: ~ sort: ~ - fixable-only: ~ - reporting-format: ~ - reporting-target: ~ + fix-mode: safe minimum-report-level: ~ - minimum-fail-level: ~ - dry-run: ~ ``` **no-stubs** @@ -38,12 +37,6 @@ grumphp: Disable built-in PHP and library stubs for analysis. By default, the analyzer uses stubs for built-in PHP functions and popular libraries to provide accurate type information. Disabling this may result in more reported issues when external symbols can't be resolved. -**staged** - -*Type: bool* - -Only analyze files that are staged in git. Designed for git pre-commit hooks. Fails if not in a git repository. - **retain-codes** *Type: string[] — Default: []* @@ -56,49 +49,21 @@ Reporting filter: only display issues matching the specified rule codes (e.g. `i Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago analyze --generate-baseline`. -**fix** - -*Default: null* - -Apply automatic fixes to the source code. Accepted values: - -- `safe` — apply only safe fixes (default fix mode) -- `potentially-unsafe` — also apply fixes that may require manual review -- `unsafe` — also apply fixes that might change code behavior - -Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. - -**fail-on-remaining** - -*Type: bool* - -Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. - **sort** *Type: bool* Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. -**fixable-only** - -*Type: bool* - -Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. - -**reporting-format** - -*Default: null (mago default: medium)* - -Output format for issue reports. Not available when using `fix`. Possible values: - -`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` +**fix-mode** -**reporting-target** +*Default: safe — Possible values: `safe`, `potentially-unsafe`, `unsafe`* -*Default: null (mago default: stdout)* +Controls which fixes are applied when GrumPHP offers to auto-fix: -Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` +- `safe` — apply only safe fixes (default) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior **minimum-report-level** @@ -106,14 +71,3 @@ Where to send the output. Not available when using `fix`. Possible values: `stdo Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` -**minimum-fail-level** - -*Default: null (mago default: error)* - -Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` - -**dry-run** - -*Type: bool* - -Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/doc/tasks/mago/formatter.md b/doc/tasks/mago/formatter.md index 9e51ebeca..881b36f6f 100644 --- a/doc/tasks/mago/formatter.md +++ b/doc/tasks/mago/formatter.md @@ -8,25 +8,19 @@ Automatically format PHP code to match the configured style preferences. composer require --dev carthage-software/mago ``` +## Behavior + +The task always runs in `--dry-run` mode: it previews formatting changes without modifying any files, and fails if any file would be changed. + +If the task fails, GrumPHP will offer to re-run without `--dry-run` to apply the formatting in-place. + ## Config -The task lives under the `mago_format` namespace and has following configurable parameters: +The task lives under the `mago_format` namespace and has no configurable parameters: ```yaml # grumphp.yml grumphp: tasks: - mago_format: - type: default + mago_format: ~ ``` - -**type** - -*Default: default — Possible values: `default`, `dry-run`, `check`, `staged`* - -Controls how the formatter runs: - -- `default` — apply formatting changes in-place -- `dry-run` — print a diff of changes without modifying any files -- `check` — exit with failure if any file would be changed, without modifying files. Ideal for CI environments -- `staged` — format files currently staged in git. Designed for git pre-commit hooks. Fails if not in a git repository diff --git a/doc/tasks/mago/guard.md b/doc/tasks/mago/guard.md index f9ca33738..bbc8177a9 100644 --- a/doc/tasks/mago/guard.md +++ b/doc/tasks/mago/guard.md @@ -8,6 +8,12 @@ Enforce architectural rules and layer dependencies. Checks that code follows def composer require --dev carthage-software/mago ``` +## Behavior + +The task always runs in `--fix --dry-run` mode: it previews what automatic fixes would be applied without modifying any files, and fails if issues are found. The task runs on all files in both `git pre-commit` and `run` contexts. + +If the task fails, GrumPHP will offer to re-run with `--fix` applied. The fix mode can be configured via `fix-mode`. + ## Config The task lives under the `mago_guard` namespace and has following configurable parameters: @@ -18,18 +24,13 @@ grumphp: tasks: mago_guard: no-stubs: ~ - checks: all + structural: ~ + perimeter: ~ retain-codes: [] ignore-baseline: ~ - fix: ~ - fail-on-remaining: ~ sort: ~ - fixable-only: ~ - reporting-format: ~ - reporting-target: ~ + fix-mode: safe minimum-report-level: ~ - minimum-fail-level: ~ - dry-run: ~ ``` **no-stubs** @@ -38,15 +39,17 @@ grumphp: Disable built-in PHP and library stubs. By default, guard uses stubs for built-in PHP functions and popular libraries to provide accurate symbol information. Disabling this may result in more warnings when external symbols can't be resolved. -**checks** +**structural** -*Default: all — Possible values: `all`, `structural`, `perimeter`* +*Type: bool* -Controls which checks are run: +Run only structural checks (naming conventions, modifiers, inheritance constraints). Can be combined with `perimeter`. When neither is set, both check types run. -- `all` — run both structural and perimeter checks -- `structural` — run only structural checks (naming conventions, modifiers, inheritance constraints) -- `perimeter` — run only perimeter checks (dependency boundaries, layer restrictions) +**perimeter** + +*Type: bool* + +Run only perimeter checks (dependency boundaries, layer restrictions). Can be combined with `structural`. When neither is set, both check types run. **retain-codes** @@ -60,49 +63,21 @@ Reporting filter: only display issues matching the specified rule codes. All rul Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago guard --generate-baseline`. -**fix** - -*Default: null* - -Apply automatic fixes to the source code. Accepted values: - -- `safe` — apply only safe fixes (default fix mode) -- `potentially-unsafe` — also apply fixes that may require manual review -- `unsafe` — also apply fixes that might change code behavior - -Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. - -**fail-on-remaining** - -*Type: bool* - -Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. - **sort** *Type: bool* Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. -**fixable-only** - -*Type: bool* - -Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. - -**reporting-format** - -*Default: null (mago default: medium)* - -Output format for issue reports. Not available when using `fix`. Possible values: - -`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` +**fix-mode** -**reporting-target** +*Default: safe — Possible values: `safe`, `potentially-unsafe`, `unsafe`* -*Default: null (mago default: stdout)* +Controls which fixes are applied when GrumPHP offers to auto-fix: -Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` +- `safe` — apply only safe fixes (default) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior **minimum-report-level** @@ -110,14 +85,3 @@ Where to send the output. Not available when using `fix`. Possible values: `stdo Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` -**minimum-fail-level** - -*Default: null (mago default: error)* - -Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` - -**dry-run** - -*Type: bool* - -Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/doc/tasks/mago/linter.md b/doc/tasks/mago/linter.md index d9a46d591..b77711cc6 100644 --- a/doc/tasks/mago/linter.md +++ b/doc/tasks/mago/linter.md @@ -8,6 +8,12 @@ Run linting rules on PHP code to identify style violations, code smells, and pot composer require --dev carthage-software/mago ``` +## Behavior + +The task always runs in `--fix --dry-run` mode: it previews what automatic fixes would be applied without modifying any files, and fails if issues are found. When running in a `git pre-commit` context, only staged files are linted (`--staged`). In a `run` context, all files are linted. + +If the task fails, GrumPHP will offer to re-run with `--fix` applied. The fix mode can be configured via `fix-mode`. + ## Config The task lives under the `mago_lint` namespace and has following configurable parameters: @@ -20,18 +26,11 @@ grumphp: semantics: ~ pedantic: ~ only: [] - staged: ~ retain-codes: [] ignore-baseline: ~ - fix: ~ - fail-on-remaining: ~ sort: ~ - fixable-only: ~ - reporting-format: ~ - reporting-target: ~ + fix-mode: safe minimum-report-level: ~ - minimum-fail-level: ~ - dry-run: ~ ``` **semantics** @@ -52,12 +51,6 @@ Enable every available linter rule for maximum thoroughness. Overrides your conf Run only the specified rules, ignoring the configuration file. Provide a list of rule codes (e.g. `invalid-argument`, `semantics`). Overrides your `mago.toml` configuration and is useful for targeted analysis. -**staged** - -*Type: bool* - -Only lint files that are staged in git. Designed for git pre-commit hooks. Fails if not in a git repository. - **retain-codes** *Type: string[] — Default: []* @@ -72,49 +65,21 @@ Note: this differs from `only`, which restricts which rules are executed. Ignore the baseline file and report all issues, including those currently suppressed. The baseline file must be generated manually via `mago lint --generate-baseline`. -**fix** - -*Default: null* - -Apply automatic fixes to the source code. Accepted values: - -- `safe` — apply only safe fixes (default fix mode) -- `potentially-unsafe` — also apply fixes that may require manual review -- `unsafe` — also apply fixes that might change code behavior - -Cannot be used together with `fixable-only`, `reporting-format`, or `reporting-target`. - -**fail-on-remaining** - -*Type: bool* - -Exit with a non-zero status if there are issues remaining after fixing. Useful in CI/CD pipelines to ensure all issues are addressed. Requires `fix` to be set. - **sort** *Type: bool* Sort reported issues by severity level, rule code, and file location. By default, issues are reported in the order they appear in files. -**fixable-only** - -*Type: bool* - -Filter output to only show issues that can be automatically fixed. Cannot be used together with `fix`. - -**reporting-format** - -*Default: null (mago default: medium)* - -Output format for issue reports. Not available when using `fix`. Possible values: - -`rich`, `medium`, `short`, `ariadne`, `github`, `gitlab`, `json`, `count`, `code-count`, `checkstyle`, `emacs`, `sarif` +**fix-mode** -**reporting-target** +*Default: safe — Possible values: `safe`, `potentially-unsafe`, `unsafe`* -*Default: null (mago default: stdout)* +Controls which fixes are applied when GrumPHP offers to auto-fix: -Where to send the output. Not available when using `fix`. Possible values: `stdout`, `stderr` +- `safe` — apply only safe fixes (default) +- `potentially-unsafe` — also apply fixes that may require manual review +- `unsafe` — also apply fixes that might change code behavior **minimum-report-level** @@ -122,14 +87,3 @@ Where to send the output. Not available when using `fix`. Possible values: `stdo Minimum severity level to display in the report. Issues below this level are not shown. Possible values: `note`, `help`, `warning`, `error` -**minimum-fail-level** - -*Default: null (mago default: error)* - -Minimum severity level that causes the command to fail. For example, setting this to `warning` means the command fails on warnings and errors, but not on notes or help suggestions. Possible values: `note`, `help`, `warning`, `error` - -**dry-run** - -*Type: bool* - -Preview fixes without writing any changes to disk. Shows what changes would be made without modifying any files. Requires `fix` to be set. diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index 1af87b2f5..513ca84aa 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -169,13 +169,6 @@ services: tags: - {name: grumphp.task, task: kahlan} - GrumPHP\Task\Mago: - arguments: - - '@process_builder' - - '@formatter.raw_process' - tags: - - {name: grumphp.task, task: mago} - GrumPHP\Task\MagoAnalyzer: arguments: - '@process_builder' diff --git a/src/Task/Mago.php b/src/Task/Mago.php index a5e30d3be..56bfad1e3 100644 --- a/src/Task/Mago.php +++ b/src/Task/Mago.php @@ -5,141 +5,105 @@ namespace GrumPHP\Task; use GrumPHP\Collection\ProcessArgumentsCollection; +use GrumPHP\Fixer\Provider\FixableProcessResultProvider; use GrumPHP\Formatter\ProcessFormatterInterface; use GrumPHP\Runner\TaskResult; use GrumPHP\Runner\TaskResultInterface; -use GrumPHP\Task\Config\ConfigOptionsResolver; use GrumPHP\Task\Context\ContextInterface; use GrumPHP\Task\Context\GitPreCommitContext; use GrumPHP\Task\Context\RunContext; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Process\Process; /** * @extends AbstractExternalTask */ -class Mago extends AbstractExternalTask +abstract class Mago extends AbstractExternalTask { - public static function getConfigurableOptions(): ConfigOptionsResolver - { - return ConfigOptionsResolver::fromOptionsResolver(new OptionsResolver()); - } - public function canRunInContext(ContextInterface $context): bool { return $context instanceof GitPreCommitContext || $context instanceof RunContext; } - public function run(ContextInterface $context): TaskResultInterface - { - return TaskResult::createFailed( - $this, - $context, - 'The mago task is split into 4 distinct tasks.'.PHP_EOL - . 'Please use the following tasks instead:'.PHP_EOL.PHP_EOL - . '- mago_analyze ' - . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/analyzer.md)'.PHP_EOL - . '- mago_format ' - . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/formatter.md)'.PHP_EOL - . '- mago_guard ' - . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/guard.md)'.PHP_EOL - . '- mago_lint ' - . '(https://github.com/phpro/grumphp/blob/master/doc/tasks/mago/linter.md)'.PHP_EOL - ); - } - protected static function configureSharedOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ]); $resolver->addAllowedTypes('retain-codes', ['array']); $resolver->addAllowedTypes('ignore-baseline', ['null', 'bool']); - $resolver->addAllowedTypes('fix', ['null', 'string']); - $resolver->addAllowedTypes('fail-on-remaining', ['null', 'bool']); $resolver->addAllowedTypes('sort', ['null', 'bool']); - $resolver->addAllowedTypes('fixable-only', ['null', 'bool']); - $resolver->addAllowedTypes('reporting-format', ['null', 'string']); - $resolver->addAllowedTypes('reporting-target', ['null', 'string']); + $resolver->addAllowedTypes('fix-mode', ['string']); $resolver->addAllowedTypes('minimum-report-level', ['null', 'string']); - $resolver->addAllowedTypes('minimum-fail-level', ['null', 'string']); - $resolver->addAllowedTypes('dry-run', ['null', 'bool']); - $resolver->addAllowedValues('fix', [null, 'safe', 'potentially-unsafe', 'unsafe']); - $resolver->addAllowedValues('reporting-format', [ - null, 'rich', 'medium', 'short', 'ariadne', 'github', 'gitlab', - 'json', 'count', 'code-count', 'checkstyle', 'emacs', 'sarif', - ]); - $resolver->addAllowedValues('reporting-target', [null, 'stdout', 'stderr']); + $resolver->addAllowedValues('fix-mode', ['safe', 'potentially-unsafe', 'unsafe']); $resolver->addAllowedValues('minimum-report-level', [null, 'note', 'help', 'warning', 'error']); - $resolver->addAllowedValues('minimum-fail-level', [null, 'note', 'help', 'warning', 'error']); } - protected function resolveFixOption(array $config): ?string - { - return $config['fix']; - } - - protected function validateFixCompatibility( - array $config, - ?string $fix, - ContextInterface $context - ): ?TaskResultInterface { - $error = match (true) { - null === $fix && $config['fail-on-remaining'] - => 'Fail on remaining option is only supported with fix option.', - null === $fix && $config['dry-run'] - => 'Dry run option is only supported with fix option.', - null !== $fix && $config['fixable-only'] - => 'Fixable-only option is not supported with fix option.', - null !== $fix && $config['reporting-format'] - => 'Reporting format option is not supported with fix option.', - null !== $fix && $config['reporting-target'] - => 'Reporting target option is not supported with fix option.', - default => null, - }; - - return null !== $error ? TaskResult::createFailed($this, $context, $error) : null; + /** + * @param array{ + * retain-codes: array, + * ignore-baseline: bool|null, + * sort: bool|null, + * minimum-report-level: string|null, + * } $config + */ + protected function addCommonArguments( + ProcessArgumentsCollection $arguments, + array $config + ): void { + $arguments->addArgumentArrayWithSeparatedValue('--retain-code', $config['retain-codes']); + $arguments->addOptionalArgument('--ignore-baseline', $config['ignore-baseline']); + $arguments->addOptionalArgument('--sort', $config['sort']); + $arguments->addOptionalArgumentWithSeparatedValue('--minimum-report-level', $config['minimum-report-level']); } - protected function addFixArguments( + /** + * @param array{ + * retain-codes: array, + * ignore-baseline: bool|null, + * sort: bool|null, + * fix-mode: string, + * minimum-report-level: string|null, + * } $config + */ + protected function addSharedArguments( ProcessArgumentsCollection $arguments, - array $config, - ?string $fix + array $config ): void { - if (null === $fix) { - return; - } - $arguments->add('--fix'); - $arguments->addOptionalArgument('--potentially-unsafe', 'potentially-unsafe' === $fix); - $arguments->addOptionalArgument('--unsafe', 'unsafe' === $fix); - $arguments->addOptionalArgument('--fail-on-remaining', $config['fail-on-remaining']); - $arguments->addOptionalArgument('--dry-run', $config['dry-run']); + $arguments->add('--dry-run'); + $arguments->add('--fail-on-remaining'); + $this->addCommonArguments($arguments, $config); } - protected function addSharedArguments( + /** + * @param array{'fix-mode': string} $config + */ + protected function createFailedWithFix( + ContextInterface $context, ProcessArgumentsCollection $arguments, + string $message, array $config - ): void { - $arguments->addOptionalArgument('--fixable-only', $config['fixable-only']); - $arguments->addOptionalArgumentWithSeparatedValue('--reporting-format', $config['reporting-format']); - $arguments->addOptionalArgumentWithSeparatedValue('--reporting-target', $config['reporting-target']); - $arguments->addArgumentArrayWithSeparatedValue('--retain-code', $config['retain-codes']); - $arguments->addOptionalArgumentWithSeparatedValue('--minimum-report-level', $config['minimum-report-level']); - $arguments->addOptionalArgumentWithSeparatedValue('--minimum-fail-level', $config['minimum-fail-level']); - $arguments->addOptionalArgument('--ignore-baseline', $config['ignore-baseline']); - $arguments->addOptionalArgument('--sort', $config['sort']); + ): TaskResultInterface { + return FixableProcessResultProvider::provide( + TaskResult::createFailed($this, $context, $message), + function () use ($arguments, $config): Process { + $arguments->removeElement('--dry-run'); + $arguments->removeElement('--fail-on-remaining'); + match ($config['fix-mode']) { + 'potentially-unsafe' => $arguments->add('--potentially-unsafe'), + 'unsafe' => $arguments->add('--unsafe'), + default => null, + }; + return $this->processBuilder->buildProcess($arguments); + } + ); } } diff --git a/src/Task/MagoAnalyzer.php b/src/Task/MagoAnalyzer.php index ae943ae8b..967111e8f 100644 --- a/src/Task/MagoAnalyzer.php +++ b/src/Task/MagoAnalyzer.php @@ -4,11 +4,14 @@ namespace GrumPHP\Task; +use GrumPHP\Fixer\Provider\FixableProcessResultProvider; use GrumPHP\Runner\TaskResult; use GrumPHP\Runner\TaskResultInterface; use GrumPHP\Task\Config\ConfigOptionsResolver; use GrumPHP\Task\Context\ContextInterface; +use GrumPHP\Task\Context\GitPreCommitContext; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Process\Process; class MagoAnalyzer extends Mago { @@ -18,11 +21,9 @@ public static function getConfigurableOptions(): ConfigOptionsResolver $resolver = new OptionsResolver(); $resolver->setDefaults([ 'no-stubs' => null, - 'staged' => null, ]); $resolver->addAllowedTypes('no-stubs', ['null', 'bool']); - $resolver->addAllowedTypes('staged', ['null', 'bool']); self::configureSharedOptions($resolver); @@ -32,26 +33,33 @@ public static function getConfigurableOptions(): ConfigOptionsResolver public function run(ContextInterface $context): TaskResultInterface { $config = $this->getConfig()->getOptions(); - $fix = $this->resolveFixOption($config); - - if ($error = $this->validateFixCompatibility($config, $fix, $context)) { - return $error; - } $arguments = $this->processBuilder->createArgumentsForCommand('mago'); $arguments->add('analyze'); - - $this->addFixArguments($arguments, $config, $fix); - $this->addSharedArguments($arguments, $config); - + $this->addCommonArguments($arguments, $config); $arguments->addOptionalArgument('--no-stubs', $config['no-stubs']); - $arguments->addOptionalArgument('--staged', $config['staged']); + $arguments->addOptionalArgument('--staged', $context instanceof GitPreCommitContext ?: null); $process = $this->processBuilder->buildProcess($arguments); $process->run(); if (!$process->isSuccessful()) { - return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + return FixableProcessResultProvider::provide( + TaskResult::createFailed($this, $context, $this->formatter->format($process)), + function () use ($config): Process { + $fixArguments = $this->processBuilder->createArgumentsForCommand('mago'); + $fixArguments->add('analyze'); + $fixArguments->add('--fix'); + $this->addCommonArguments($fixArguments, $config); + $fixArguments->addOptionalArgument('--no-stubs', $config['no-stubs']); + match ($config['fix-mode']) { + 'potentially-unsafe' => $fixArguments->add('--potentially-unsafe'), + 'unsafe' => $fixArguments->add('--unsafe'), + default => null, + }; + return $this->processBuilder->buildProcess($fixArguments); + } + ); } return TaskResult::createPassed($this, $context); diff --git a/src/Task/MagoFormatter.php b/src/Task/MagoFormatter.php index f008f8a82..18b1a3348 100644 --- a/src/Task/MagoFormatter.php +++ b/src/Task/MagoFormatter.php @@ -4,43 +4,52 @@ namespace GrumPHP\Task; +use GrumPHP\Fixer\Provider\FixableProcessResultProvider; +use GrumPHP\Formatter\ProcessFormatterInterface; use GrumPHP\Runner\TaskResult; use GrumPHP\Runner\TaskResultInterface; use GrumPHP\Task\Config\ConfigOptionsResolver; use GrumPHP\Task\Context\ContextInterface; +use GrumPHP\Task\Context\GitPreCommitContext; +use GrumPHP\Task\Context\RunContext; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Process\Process; -class MagoFormatter extends Mago +/** + * @extends AbstractExternalTask + */ +class MagoFormatter extends AbstractExternalTask { + public function canRunInContext(ContextInterface $context): bool + { + return $context instanceof GitPreCommitContext || $context instanceof RunContext; + } + public static function getConfigurableOptions(): ConfigOptionsResolver { $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'type' => 'default', - ]); - - $resolver->addAllowedTypes('type', ['string']); - $resolver->addAllowedValues('type', ['default', 'dry-run', 'check', 'staged']); return ConfigOptionsResolver::fromOptionsResolver($resolver); } public function run(ContextInterface $context): TaskResultInterface { - $config = $this->getConfig()->getOptions(); $arguments = $this->processBuilder->createArgumentsForCommand('mago'); $arguments->add('format'); - - $arguments->addOptionalArgument('--dry-run', 'dry-run' === $config['type']); - $arguments->addOptionalArgument('--check', 'check' === $config['type']); - $arguments->addOptionalArgument('--staged', 'staged' === $config['type']); + $arguments->add('--dry-run'); $process = $this->processBuilder->buildProcess($arguments); $process->run(); if (!$process->isSuccessful()) { - return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + return FixableProcessResultProvider::provide( + TaskResult::createFailed($this, $context, $this->formatter->format($process)), + function () use ($arguments): Process { + $arguments->removeElement('--dry-run'); + return $this->processBuilder->buildProcess($arguments); + } + ); } return TaskResult::createPassed($this, $context); diff --git a/src/Task/MagoGuard.php b/src/Task/MagoGuard.php index c7e7a1a80..f5d5dd7fa 100644 --- a/src/Task/MagoGuard.php +++ b/src/Task/MagoGuard.php @@ -18,12 +18,13 @@ public static function getConfigurableOptions(): ConfigOptionsResolver $resolver = new OptionsResolver(); $resolver->setDefaults([ 'no-stubs' => null, - 'checks' => 'all', + 'structural' => null, + 'perimeter' => null, ]); $resolver->addAllowedTypes('no-stubs', ['null', 'bool']); - $resolver->addAllowedTypes('checks', ['string']); - $resolver->addAllowedValues('checks', ['all', 'structural', 'perimeter']); + $resolver->addAllowedTypes('structural', ['null', 'bool']); + $resolver->addAllowedTypes('perimeter', ['null', 'bool']); self::configureSharedOptions($resolver); @@ -33,27 +34,21 @@ public static function getConfigurableOptions(): ConfigOptionsResolver public function run(ContextInterface $context): TaskResultInterface { $config = $this->getConfig()->getOptions(); - $fix = $this->resolveFixOption($config); - - if ($error = $this->validateFixCompatibility($config, $fix, $context)) { - return $error; - } $arguments = $this->processBuilder->createArgumentsForCommand('mago'); $arguments->add('guard'); - $this->addFixArguments($arguments, $config, $fix); $this->addSharedArguments($arguments, $config); $arguments->addOptionalArgument('--no-stubs', $config['no-stubs']); - $arguments->addOptionalArgument('--structural', 'structural' === $config['checks']); - $arguments->addOptionalArgument('--perimeter', 'perimeter' === $config['checks']); + $arguments->addOptionalArgument('--structural', $config['structural']); + $arguments->addOptionalArgument('--perimeter', $config['perimeter']); $process = $this->processBuilder->buildProcess($arguments); $process->run(); if (!$process->isSuccessful()) { - return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + return $this->createFailedWithFix($context, $arguments, $this->formatter->format($process), $config); } return TaskResult::createPassed($this, $context); diff --git a/src/Task/MagoLinter.php b/src/Task/MagoLinter.php index 8b3eed3b9..fb5e7d619 100644 --- a/src/Task/MagoLinter.php +++ b/src/Task/MagoLinter.php @@ -8,6 +8,7 @@ use GrumPHP\Runner\TaskResultInterface; use GrumPHP\Task\Config\ConfigOptionsResolver; use GrumPHP\Task\Context\ContextInterface; +use GrumPHP\Task\Context\GitPreCommitContext; use Symfony\Component\OptionsResolver\OptionsResolver; class MagoLinter extends Mago @@ -20,13 +21,11 @@ public static function getConfigurableOptions(): ConfigOptionsResolver 'semantics' => null, 'pedantic' => null, 'only' => [], - 'staged' => null, ]); $resolver->addAllowedTypes('semantics', ['null', 'bool']); $resolver->addAllowedTypes('pedantic', ['null', 'bool']); $resolver->addAllowedTypes('only', ['array']); - $resolver->addAllowedTypes('staged', ['null', 'bool']); self::configureSharedOptions($resolver); @@ -36,28 +35,22 @@ public static function getConfigurableOptions(): ConfigOptionsResolver public function run(ContextInterface $context): TaskResultInterface { $config = $this->getConfig()->getOptions(); - $fix = $this->resolveFixOption($config); - - if ($error = $this->validateFixCompatibility($config, $fix, $context)) { - return $error; - } $arguments = $this->processBuilder->createArgumentsForCommand('mago'); $arguments->add('lint'); - $this->addFixArguments($arguments, $config, $fix); $this->addSharedArguments($arguments, $config); $arguments->addOptionalCommaSeparatedArgument('--only=%s', $config['only']); $arguments->addOptionalArgument('--semantics', $config['semantics']); $arguments->addOptionalArgument('--pedantic', $config['pedantic']); - $arguments->addOptionalArgument('--staged', $config['staged']); + $arguments->addOptionalArgument('--staged', $context instanceof GitPreCommitContext ?: null); $process = $this->processBuilder->buildProcess($arguments); $process->run(); if (!$process->isSuccessful()) { - return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + return $this->createFailedWithFix($context, $arguments, $this->formatter->format($process), $config); } return TaskResult::createPassed($this, $context); diff --git a/test/Unit/Task/MagoAnalyzerTest.php b/test/Unit/Task/MagoAnalyzerTest.php index eb3e08227..a4ed90273 100644 --- a/test/Unit/Task/MagoAnalyzerTest.php +++ b/test/Unit/Task/MagoAnalyzerTest.php @@ -4,6 +4,7 @@ namespace GrumPHPTest\Unit\Task; +use GrumPHP\Runner\FixableTaskResult; use GrumPHP\Task\Context\ContextInterface; use GrumPHP\Task\Context\GitPreCommitContext; use GrumPHP\Task\Context\RunContext; @@ -29,26 +30,16 @@ public static function provideConfigurableOptions(): iterable [], [ 'no-stubs' => null, - 'staged' => null, 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ] ]; - yield 'invalid-fix' => [['fix' => 'invalid'], null]; - yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; - yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + yield 'invalid-fix-mode' => [['fix-mode' => 'invalid'], null]; yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; - yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; } public static function provideRunContexts(): iterable @@ -79,41 +70,7 @@ function () { $this->formatter->format($process)->willReturn('nope'); }, 'nope', - ]; - - yield 'fail-on-remaining-without-fix' => [ - ['fail-on-remaining' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fail on remaining option is only supported with fix option.', - ]; - - yield 'dry-run-without-fix' => [ - ['dry-run' => true], - self::mockContext(RunContext::class), - function () {}, - 'Dry run option is only supported with fix option.', - ]; - - yield 'fixable-only-with-fix' => [ - ['fix' => 'safe', 'fixable-only' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fixable-only option is not supported with fix option.', - ]; - - yield 'reporting-format-with-fix' => [ - ['fix' => 'safe', 'reporting-format' => 'json'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting format option is not supported with fix option.', - ]; - - yield 'reporting-target-with-fix' => [ - ['fix' => 'safe', 'reporting-target' => 'stderr'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting target option is not supported with fix option.', + FixableTaskResult::class, ]; } @@ -157,67 +114,18 @@ public static function provideExternalTaskRuns(): iterable ['analyze'] ]; - yield 'no-stubs' => [ - ['no-stubs' => true], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--no-stubs'] - ]; - - yield 'fix-safe' => [ - ['fix' => 'safe'], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fix'] - ]; - - yield 'fix-potentially-unsafe' => [ - ['fix' => 'potentially-unsafe'], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fix', '--potentially-unsafe'] - ]; - - yield 'fix-unsafe' => [ - ['fix' => 'unsafe'], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fix', '--unsafe'] - ]; - - yield 'fix-with-fail-on-remaining' => [ - ['fix' => 'safe', 'fail-on-remaining' => true], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fix', '--fail-on-remaining'] - ]; - - yield 'fix-with-dry-run' => [ - ['fix' => 'safe', 'dry-run' => true], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fix', '--dry-run'] - ]; - - yield 'fixable-only' => [ - ['fixable-only' => true], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--fixable-only'] - ]; - - yield 'reporting-format' => [ - ['reporting-format' => 'json'], - self::mockContext(RunContext::class), + yield 'pre-commit-staged' => [ + [], + self::mockContext(GitPreCommitContext::class), 'mago', - ['analyze', '--reporting-format', 'json'] + ['analyze', '--staged'] ]; - yield 'reporting-target' => [ - ['reporting-target' => 'stderr'], + yield 'no-stubs' => [ + ['no-stubs' => true], self::mockContext(RunContext::class), 'mago', - ['analyze', '--reporting-target', 'stderr'] + ['analyze', '--no-stubs'] ]; yield 'retain-codes' => [ @@ -227,20 +135,6 @@ public static function provideExternalTaskRuns(): iterable ['analyze', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] ]; - yield 'minimum-report-level' => [ - ['minimum-report-level' => 'warning'], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--minimum-report-level', 'warning'] - ]; - - yield 'minimum-fail-level' => [ - ['minimum-fail-level' => 'error'], - self::mockContext(RunContext::class), - 'mago', - ['analyze', '--minimum-fail-level', 'error'] - ]; - yield 'ignore-baseline' => [ ['ignore-baseline' => true], self::mockContext(RunContext::class), @@ -255,11 +149,11 @@ public static function provideExternalTaskRuns(): iterable ['analyze', '--sort'] ]; - yield 'staged' => [ - ['staged' => true], + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], self::mockContext(RunContext::class), 'mago', - ['analyze', '--staged'] + ['analyze', '--minimum-report-level', 'warning'] ]; } } diff --git a/test/Unit/Task/MagoFormatterTest.php b/test/Unit/Task/MagoFormatterTest.php index 7eea87a79..ef9a0110d 100644 --- a/test/Unit/Task/MagoFormatterTest.php +++ b/test/Unit/Task/MagoFormatterTest.php @@ -4,6 +4,7 @@ namespace GrumPHPTest\Unit\Task; +use GrumPHP\Runner\FixableTaskResult; use GrumPHP\Task\Context\ContextInterface; use GrumPHP\Task\Context\GitPreCommitContext; use GrumPHP\Task\Context\RunContext; @@ -25,33 +26,8 @@ protected function provideTask(): TaskInterface public static function provideConfigurableOptions(): iterable { - yield 'defaults' => [ - [], - ['type' => 'default'] - ]; - - yield 'type-default' => [ - ['type' => 'default'], - ['type' => 'default'] - ]; - - yield 'type-dry-run' => [ - ['type' => 'dry-run'], - ['type' => 'dry-run'] - ]; - - yield 'type-check' => [ - ['type' => 'check'], - ['type' => 'check'] - ]; - - yield 'type-staged' => [ - ['type' => 'staged'], - ['type' => 'staged'] - ]; - - yield 'invalid-type' => [ - ['type' => 'invalid'], + yield 'unknown-option' => [ + ['unknown' => true], null ]; } @@ -76,7 +52,7 @@ public static function provideRunContexts(): iterable public static function provideFailsOnStuff(): iterable { - yield 'exitCode1' => [ + yield 'exitCode1-run' => [ [], self::mockContext(RunContext::class), function () { @@ -84,6 +60,18 @@ function () { $this->formatter->format($process)->willReturn('nope'); }, 'nope', + FixableTaskResult::class, + ]; + + yield 'exitCode1-pre-commit' => [ + [], + self::mockContext(GitPreCommitContext::class), + function () { + $this->mockProcessBuilder('mago', $process = self::mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + FixableTaskResult::class, ]; } @@ -124,35 +112,14 @@ public static function provideExternalTaskRuns(): iterable [], self::mockContext(RunContext::class), 'mago', - ['format'] - ]; - - yield 'type-default' => [ - ['type' => 'default'], - self::mockContext(RunContext::class), - 'mago', - ['format'] - ]; - - yield 'type-dry-run' => [ - ['type' => 'dry-run'], - self::mockContext(RunContext::class), - 'mago', ['format', '--dry-run'] ]; - yield 'type-check' => [ - ['type' => 'check'], - self::mockContext(RunContext::class), - 'mago', - ['format', '--check'] - ]; - - yield 'type-staged' => [ - ['type' => 'staged'], - self::mockContext(RunContext::class), + yield 'pre-commit' => [ + [], + self::mockContext(GitPreCommitContext::class), 'mago', - ['format', '--staged'] + ['format', '--dry-run'] ]; } } diff --git a/test/Unit/Task/MagoGuardTest.php b/test/Unit/Task/MagoGuardTest.php index b5951974c..6223ef031 100644 --- a/test/Unit/Task/MagoGuardTest.php +++ b/test/Unit/Task/MagoGuardTest.php @@ -4,6 +4,7 @@ namespace GrumPHPTest\Unit\Task; +use GrumPHP\Runner\FixableTaskResult; use GrumPHP\Task\Context\ContextInterface; use GrumPHP\Task\Context\GitPreCommitContext; use GrumPHP\Task\Context\RunContext; @@ -29,64 +30,46 @@ public static function provideConfigurableOptions(): iterable [], [ 'no-stubs' => null, - 'checks' => 'all', + 'structural' => null, + 'perimeter' => null, 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ] ]; - yield 'checks-structural' => [ - ['checks' => 'structural'], + yield 'structural' => [ + ['structural' => true], [ 'no-stubs' => null, - 'checks' => 'structural', + 'structural' => true, + 'perimeter' => null, 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ] ]; - yield 'checks-perimeter' => [ - ['checks' => 'perimeter'], + yield 'perimeter' => [ + ['perimeter' => true], [ 'no-stubs' => null, - 'checks' => 'perimeter', + 'structural' => null, + 'perimeter' => true, 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ] ]; - yield 'invalid-checks' => [['checks' => 'invalid'], null]; - yield 'invalid-fix' => [['fix' => 'invalid'], null]; - yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; - yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + + yield 'invalid-fix-mode' => [['fix-mode' => 'invalid'], null]; yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; - yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; } public static function provideRunContexts(): iterable @@ -117,41 +100,7 @@ function () { $this->formatter->format($process)->willReturn('nope'); }, 'nope', - ]; - - yield 'fail-on-remaining-without-fix' => [ - ['fail-on-remaining' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fail on remaining option is only supported with fix option.', - ]; - - yield 'dry-run-without-fix' => [ - ['dry-run' => true], - self::mockContext(RunContext::class), - function () {}, - 'Dry run option is only supported with fix option.', - ]; - - yield 'fixable-only-with-fix' => [ - ['fix' => 'safe', 'fixable-only' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fixable-only option is not supported with fix option.', - ]; - - yield 'reporting-format-with-fix' => [ - ['fix' => 'safe', 'reporting-format' => 'json'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting format option is not supported with fix option.', - ]; - - yield 'reporting-target-with-fix' => [ - ['fix' => 'safe', 'reporting-target' => 'stderr'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting target option is not supported with fix option.', + FixableTaskResult::class, ]; } @@ -192,126 +141,64 @@ public static function provideExternalTaskRuns(): iterable [], self::mockContext(RunContext::class), 'mago', - ['guard'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining'] ]; yield 'no-stubs' => [ ['no-stubs' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--no-stubs'] - ]; - - yield 'checks-structural' => [ - ['checks' => 'structural'], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--structural'] - ]; - - yield 'checks-perimeter' => [ - ['checks' => 'perimeter'], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--perimeter'] - ]; - - yield 'checks-all' => [ - ['checks' => 'all'], - self::mockContext(RunContext::class), - 'mago', - ['guard'] - ]; - - yield 'fix-safe' => [ - ['fix' => 'safe'], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--fix'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--no-stubs'] ]; - yield 'fix-potentially-unsafe' => [ - ['fix' => 'potentially-unsafe'], + yield 'structural' => [ + ['structural' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--fix', '--potentially-unsafe'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--structural'] ]; - yield 'fix-unsafe' => [ - ['fix' => 'unsafe'], + yield 'perimeter' => [ + ['perimeter' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--fix', '--unsafe'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--perimeter'] ]; - yield 'fix-with-fail-on-remaining' => [ - ['fix' => 'safe', 'fail-on-remaining' => true], + yield 'structural-and-perimeter' => [ + ['structural' => true, 'perimeter' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--fix', '--fail-on-remaining'] - ]; - - yield 'fix-with-dry-run' => [ - ['fix' => 'safe', 'dry-run' => true], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--fix', '--dry-run'] - ]; - - yield 'fixable-only' => [ - ['fixable-only' => true], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--fixable-only'] - ]; - - yield 'reporting-format' => [ - ['reporting-format' => 'json'], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--reporting-format', 'json'] - ]; - - yield 'reporting-target' => [ - ['reporting-target' => 'stderr'], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--reporting-target', 'stderr'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--structural', '--perimeter'] ]; yield 'retain-codes' => [ ['retain-codes' => ['invalid-argument', 'semantics']], self::mockContext(RunContext::class), 'mago', - ['guard', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] ]; - yield 'minimum-report-level' => [ - ['minimum-report-level' => 'warning'], + yield 'ignore-baseline' => [ + ['ignore-baseline' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--minimum-report-level', 'warning'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--ignore-baseline'] ]; - yield 'minimum-fail-level' => [ - ['minimum-fail-level' => 'error'], + yield 'sort' => [ + ['sort' => true], self::mockContext(RunContext::class), 'mago', - ['guard', '--minimum-fail-level', 'error'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--sort'] ]; - yield 'ignore-baseline' => [ - ['ignore-baseline' => true], + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], self::mockContext(RunContext::class), 'mago', - ['guard', '--ignore-baseline'] + ['guard', '--fix', '--dry-run', '--fail-on-remaining', '--minimum-report-level', 'warning'] ]; - yield 'sort' => [ - ['sort' => true], - self::mockContext(RunContext::class), - 'mago', - ['guard', '--sort'] - ]; } } diff --git a/test/Unit/Task/MagoLinterTest.php b/test/Unit/Task/MagoLinterTest.php index 9ef284754..0e81b931b 100644 --- a/test/Unit/Task/MagoLinterTest.php +++ b/test/Unit/Task/MagoLinterTest.php @@ -4,6 +4,7 @@ namespace GrumPHPTest\Unit\Task; +use GrumPHP\Runner\FixableTaskResult; use GrumPHP\Task\Context\ContextInterface; use GrumPHP\Task\Context\GitPreCommitContext; use GrumPHP\Task\Context\RunContext; @@ -31,26 +32,16 @@ public static function provideConfigurableOptions(): iterable 'semantics' => null, 'pedantic' => null, 'only' => [], - 'staged' => null, 'retain-codes' => [], 'ignore-baseline' => null, - 'fix' => null, - 'fail-on-remaining' => null, 'sort' => null, - 'fixable-only' => null, - 'reporting-format' => null, - 'reporting-target' => null, + 'fix-mode' => 'safe', 'minimum-report-level' => null, - 'minimum-fail-level' => null, - 'dry-run' => null, ] ]; - yield 'invalid-fix' => [['fix' => 'invalid'], null]; - yield 'invalid-reporting-format' => [['reporting-format' => 'invalid'], null]; - yield 'invalid-reporting-target' => [['reporting-target' => 'invalid'], null]; + yield 'invalid-fix-mode' => [['fix-mode' => 'invalid'], null]; yield 'invalid-minimum-report-level' => [['minimum-report-level' => 'invalid'], null]; - yield 'invalid-minimum-fail-level' => [['minimum-fail-level' => 'invalid'], null]; } public static function provideRunContexts(): iterable @@ -81,41 +72,7 @@ function () { $this->formatter->format($process)->willReturn('nope'); }, 'nope', - ]; - - yield 'fail-on-remaining-without-fix' => [ - ['fail-on-remaining' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fail on remaining option is only supported with fix option.', - ]; - - yield 'dry-run-without-fix' => [ - ['dry-run' => true], - self::mockContext(RunContext::class), - function () {}, - 'Dry run option is only supported with fix option.', - ]; - - yield 'fixable-only-with-fix' => [ - ['fix' => 'safe', 'fixable-only' => true], - self::mockContext(RunContext::class), - function () {}, - 'Fixable-only option is not supported with fix option.', - ]; - - yield 'reporting-format-with-fix' => [ - ['fix' => 'safe', 'reporting-format' => 'json'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting format option is not supported with fix option.', - ]; - - yield 'reporting-target-with-fix' => [ - ['fix' => 'safe', 'reporting-target' => 'stderr'], - self::mockContext(RunContext::class), - function () {}, - 'Reporting target option is not supported with fix option.', + FixableTaskResult::class, ]; } @@ -146,8 +103,7 @@ public static function provideSkipsOnStuff(): iterable yield 'no-skip-scenarios' => [ [], self::mockContext(RunContext::class), - function () { - } + function () {} ]; } @@ -157,126 +113,64 @@ public static function provideExternalTaskRuns(): iterable [], self::mockContext(RunContext::class), 'mago', - ['lint'] - ]; - - yield 'fix-safe' => [ - ['fix' => 'safe'], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--fix'] - ]; - - yield 'fix-potentially-unsafe' => [ - ['fix' => 'potentially-unsafe'], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--fix', '--potentially-unsafe'] - ]; - - yield 'fix-unsafe' => [ - ['fix' => 'unsafe'], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--fix', '--unsafe'] - ]; - - yield 'fix-with-fail-on-remaining' => [ - ['fix' => 'safe', 'fail-on-remaining' => true], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--fix', '--fail-on-remaining'] - ]; - - yield 'fix-with-dry-run' => [ - ['fix' => 'safe', 'dry-run' => true], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--fix', '--dry-run'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining'] ]; - yield 'fixable-only' => [ - ['fixable-only' => true], - self::mockContext(RunContext::class), + yield 'pre-commit-staged' => [ + [], + self::mockContext(GitPreCommitContext::class), 'mago', - ['lint', '--fixable-only'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--staged'] ]; - yield 'reporting-format' => [ - ['reporting-format' => 'json'], + yield 'semantics' => [ + ['semantics' => true], self::mockContext(RunContext::class), 'mago', - ['lint', '--reporting-format', 'json'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--semantics'] ]; - yield 'reporting-target' => [ - ['reporting-target' => 'stderr'], + yield 'pedantic' => [ + ['pedantic' => true], self::mockContext(RunContext::class), 'mago', - ['lint', '--reporting-target', 'stderr'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--pedantic'] ]; yield 'only' => [ ['only' => ['invalid-argument', 'semantics']], self::mockContext(RunContext::class), 'mago', - ['lint', '--only=invalid-argument,semantics'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--only=invalid-argument,semantics'] ]; yield 'retain-codes' => [ ['retain-codes' => ['invalid-argument', 'semantics']], self::mockContext(RunContext::class), 'mago', - ['lint', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] - ]; - - yield 'minimum-report-level' => [ - ['minimum-report-level' => 'warning'], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--minimum-report-level', 'warning'] - ]; - - yield 'minimum-fail-level' => [ - ['minimum-fail-level' => 'error'], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--minimum-fail-level', 'error'] - ]; - - yield 'semantics' => [ - ['semantics' => true], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--semantics'] - ]; - - yield 'pedantic' => [ - ['pedantic' => true], - self::mockContext(RunContext::class), - 'mago', - ['lint', '--pedantic'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--retain-code', 'invalid-argument', '--retain-code', 'semantics'] ]; yield 'ignore-baseline' => [ ['ignore-baseline' => true], self::mockContext(RunContext::class), 'mago', - ['lint', '--ignore-baseline'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--ignore-baseline'] ]; yield 'sort' => [ ['sort' => true], self::mockContext(RunContext::class), 'mago', - ['lint', '--sort'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--sort'] ]; - yield 'staged' => [ - ['staged' => true], + yield 'minimum-report-level' => [ + ['minimum-report-level' => 'warning'], self::mockContext(RunContext::class), 'mago', - ['lint', '--staged'] + ['lint', '--fix', '--dry-run', '--fail-on-remaining', '--minimum-report-level', 'warning'] ]; + } }