From 672466d2fa96288ee11de984b080c5cfdace786b Mon Sep 17 00:00:00 2001 From: Sylvain VANEL Date: Thu, 30 Apr 2026 16:05:39 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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', - ], - ] - ]; - } -}