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..1818c37b0 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -32,6 +32,10 @@ grumphp: infection: ~ jsonlint: ~ kahlan: ~ + mago_analyze: ~ + mago_format: ~ + mago_guard: ~ + mago_lint: ~ make: ~ npm_script: ~ paratest: ~ @@ -99,6 +103,11 @@ 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 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 new file mode 100644 index 000000000..8d18c30b0 --- /dev/null +++ b/doc/tasks/mago.md @@ -0,0 +1,8 @@ +# 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 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 d4927541b..1af87b2f5 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -169,6 +169,41 @@ 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' + - '@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 new file mode 100644 index 000000000..a5e30d3be --- /dev/null +++ b/src/Task/Mago.php @@ -0,0 +1,145 @@ + + */ +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, + '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('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']); + } + + 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; + } + + protected function addFixArguments( + ProcessArgumentsCollection $arguments, + array $config, + ?string $fix + ): 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']); + } + + 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'] + ]; + } +}