diff --git a/composer.json b/composer.json index c1ab2875..00d9b0e1 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,8 @@ "sstalle/php7cc": "Lets GrumPHP check PHP 5.3 - 5.6 code compatibility with PHP 7.", "symfony/phpunit-bridge": "Lets GrumPHP run your unit tests with the phpunit-bridge of Symfony.", "symplify/easy-coding-standard": "Lets GrumPHP check coding standard.", - "vimeo/psalm": "Lets GrumPHP discover errors in your code without running it." + "vimeo/psalm": "Lets GrumPHP discover errors in your code without running it.", + "vincentlanglet/twig-cs-fixer": "Lets GrumPHP check and fix twig coding standard." }, "autoload": { "psr-4": { diff --git a/doc/tasks.md b/doc/tasks.md index bc618fad..e2ebba68 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -63,6 +63,7 @@ grumphp: stylelint: ~ tester: ~ twigcs: ~ + twigcsfixer: ~ xmllint: ~ yamllint: ~ ``` @@ -129,6 +130,7 @@ Every task has its own default configuration. It is possible to overwrite the pa - [Stylelint](tasks/stylelint.md) - [Tester](tasks/tester.md) - [TwigCs](tasks/twigcs.md) +- [Twig-CS-Fixer](tasks/twigcsfixer.md) - [XmlLint](tasks/xmllint.md) - [YamlLint](tasks/yamllint.md) diff --git a/doc/tasks/twigcsfixer.md b/doc/tasks/twigcsfixer.md new file mode 100644 index 00000000..f4036aae --- /dev/null +++ b/doc/tasks/twigcsfixer.md @@ -0,0 +1,80 @@ +# Twig-CS-Fixer + +Check and fix Twig coding standard using [VincentLanglet/Twig-CS-Fixer](https://github.com/VincentLanglet/Twig-CS-Fixer). + +***Composer*** + +``` +composer require --dev "vincentlanglet/twig-cs-fixer:>=2" +``` + +***Config*** + +The task lives under the `twigcsfixer` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + twigcsfixer: + paths: [] + level: ~ + config: ~ + report: 'text' + no-cache: false + verbose: false + triggered_by: ['twig'] +``` + +**paths** + +*Default: []* + +By default, current folder will be used. +On precommit only changed files that live in the paths will be passed as arguments. + + +**level** + +*Default: 'notice'* + +The level of the messages to display (possibles values are : 'notice', 'warning', 'error'). + +**config** + +*Default: null* + +Path to a `.twig-cs-fixer.php` config file. If not set, the default config will be used. + +You can check config file [here](https://github.com/VincentLanglet/Twig-CS-Fixer/blob/main/docs/configuration.md). + +**report** + +*Default: 'text'* + +The `--report` option allows to choose the output format for the linter report. + +Supported formats are: +- `text` selected by default. +- `checkstyle` following the common checkstyle XML schema. +- `github` if you want annotations on GitHub actions. +- `junit` following JUnit schema XML from Jenkins. +- `null` if you don't want any reporting. + +**no-cache** + +*Default: false* + +Do not use cache. + +**verbose** + +*Default: false* + +Increase the verbosity of messages. + +**triggered_by** + +*Default: [twig]* + +This option will specify which file extensions will trigger this task. diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index c1d5aafe..318dbe8d 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -401,6 +401,13 @@ services: tags: - {name: grumphp.task, task: twigcs} + GrumPHP\Task\TwigCsFixer: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - {name: grumphp.task, task: twigcsfixer} + GrumPHP\Task\XmlLint: arguments: - '@linter.xmllint' diff --git a/src/Task/TwigCsFixer.php b/src/Task/TwigCsFixer.php new file mode 100644 index 00000000..20bdaa8b --- /dev/null +++ b/src/Task/TwigCsFixer.php @@ -0,0 +1,95 @@ + + */ +class TwigCsFixer extends AbstractExternalTask +{ + public static function getConfigurableOptions(): ConfigOptionsResolver + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'paths' => [], + 'level' => null, + 'config' => null, + 'report' => 'text', + 'no-cache' => false, + 'verbose' => false, + 'triggered_by' => ['twig'], + ]); + + $resolver->addAllowedTypes('paths', ['array']); + $resolver->addAllowedTypes('level', ['null', 'string']); + $resolver->addAllowedTypes('config', ['null', 'string']); + $resolver->addAllowedTypes('report', ['null', 'string']); + $resolver->addAllowedTypes('no-cache', ['bool']); + $resolver->addAllowedTypes('verbose', ['bool']); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function canRunInContext(ContextInterface $context): bool + { + return $context instanceof GitPreCommitContext || $context instanceof RunContext; + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $files = $context->getFiles() + ->extensions($config['triggered_by']) + ->paths($config['paths']); + + if (\count($files) === 0) { + return TaskResult::createSkipped($this, $context); + } + + $arguments = $this->processBuilder->createArgumentsForCommand('twig-cs-fixer'); + $arguments->add('lint'); + + if ($context instanceof GitPreCommitContext) { + $arguments->addFiles($files); + } + + if ($context instanceof RunContext) { + $arguments->addArgumentArray('%s', $config['paths']); + } + + $arguments->addOptionalArgument('--level=%s', $config['level']); + $arguments->addOptionalArgument('--config=%s', $config['config']); + $arguments->addOptionalArgument('--report=%s', $config['report']); + + $arguments->addOptionalArgument('--no-cache', $config['no-cache']); + $arguments->addOptionalArgument('--verbose', $config['verbose']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return FixableProcessResultProvider::provide( + TaskResult::createFailed($this, $context, $this->formatter->format($process)), + function () use ($arguments): Process { + $arguments->add('--fix'); + return $this->processBuilder->buildProcess($arguments); + } + ); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/test/Unit/Task/TwigCsFixerTest.php b/test/Unit/Task/TwigCsFixerTest.php new file mode 100644 index 00000000..3dd084d0 --- /dev/null +++ b/test/Unit/Task/TwigCsFixerTest.php @@ -0,0 +1,242 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'triggered_by' => ['twig'], + 'paths' => [], + 'level' => null, + 'config' => null, + 'report' => 'text', + 'no-cache' => false, + 'verbose' => false, + ] + ]; + } + + public function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + $this->mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + $this->mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + $this->mockContext() + ]; + } + + public function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [], + $this->mockContext(RunContext::class, ['hello.twig']), + function () { + $this->mockProcessBuilder('twig-cs-fixer', $process = $this->mockProcess(1)); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope', + FixableTaskResult::class + ]; + } + + public function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + $this->mockContext(RunContext::class, ['hello.twig']), + function () { + $this->mockProcessBuilder('twig-cs-fixer', $this->mockProcess(0)); + } + ]; + } + + public function provideSkipsOnStuff(): iterable + { + yield 'no-files' => [ + [], + $this->mockContext(RunContext::class), + function () { + } + ]; + yield 'no-files-after-triggered-by' => [ + [], + $this->mockContext(RunContext::class, ['notatwigfile.php']), + function () { + } + ]; + yield 'no-files-in-paths' => [ + ['paths' => ['src']], + $this->mockContext(RunContext::class, ['other/hello.twig']), + function () { + } + ]; + } + + public function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--report=text', + ] + ]; + + yield 'paths' => [ + [ + 'paths' => ['src', 'templates'], + ], + $this->mockContext(RunContext::class, ['templates/hello.twig', 'templates/hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + 'src', + 'templates', + '--report=text', + ] + ]; + + yield 'precommit' => [ + [ + 'paths' => ['templates'], + ], + $this->mockContext(GitPreCommitContext::class, ['templates/hello.twig', 'templates/hello2.twig', 'other/hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + 'templates/hello.twig', + 'templates/hello2.twig', + '--report=text', + ] + ]; + + yield 'level' => [ + [ + 'level' => 'warning', + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--level=warning', + '--report=text', + ] + ]; + + yield 'config' => [ + [ + 'config' => 'twig-cs-fixer.php', + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--config=twig-cs-fixer.php', + '--report=text', + ] + ]; + + yield 'no-cache' => [ + [ + 'no-cache' => true, + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--report=text', + '--no-cache', + ] + ]; + + yield 'verbose' => [ + [ + 'verbose' => true, + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--report=text', + '--verbose', + ] + ]; + + yield 'report' => [ + [ + 'report' => 'json', + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + '--report=json', + ] + ]; + + yield 'default report' => [ + [ + 'report' => null, + ], + $this->mockContext(RunContext::class, ['hello.twig', 'hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + ] + ]; + + yield 'multiple options' => [ + [ + 'paths' => ['src', 'templates'], + 'level' => 'warning', + 'config' => 'twig-cs-fixer.php', + 'no-cache' => true, + 'verbose' => true, + ], + $this->mockContext(RunContext::class, ['templates/hello.twig', 'templates/hello2.twig']), + 'twig-cs-fixer', + [ + 'lint', + 'src', + 'templates', + '--level=warning', + '--config=twig-cs-fixer.php', + '--report=text', + '--no-cache', + '--verbose', + ] + ]; + } +}