From 593be4f6ec41c6dc80b6e6889dd43ebc78bd1b6f Mon Sep 17 00:00:00 2001 From: guvra Date: Wed, 25 Mar 2026 16:28:38 +0100 Subject: [PATCH] Add phpmd3 task --- doc/tasks/phpmd3.md | 68 +++++++++++++ resources/config/tasks.yml | 7 ++ src/Task/PhpMd3.php | 97 +++++++++++++++++++ test/Unit/Task/PhpMdTest3.php | 173 ++++++++++++++++++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 doc/tasks/phpmd3.md create mode 100644 src/Task/PhpMd3.php create mode 100644 test/Unit/Task/PhpMdTest3.php diff --git a/doc/tasks/phpmd3.md b/doc/tasks/phpmd3.md new file mode 100644 index 000000000..26c57dc95 --- /dev/null +++ b/doc/tasks/phpmd3.md @@ -0,0 +1,68 @@ +# PhpMd + +The PhpMd3 task will sniff your code for bad coding standards. + +It is the same as the `phpmd` task, but provides compatibility with the `3.x-dev` branch. +It will be merged into the `phpmd` task when the 3.x becomes stable. + +***Composer*** + +``` +composer require --dev phpmd/phpmd:3.x-dev +``` + +***Config*** + +The task lives under the `phpmd3` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + phpmd3: + whitelist_patterns: [] + exclude: [] + report_format: text + ruleset: ['phpmd.xml.dist'] + triggered_by: ['php'] +``` + +**whitelist_patterns** + +*Default: []* + +This is a list of regex patterns that will filter files to validate. With this option you can skip files like tests. This option is used in relation with the parameter `triggered_by`. +For example: whitelist files in `src/FolderA/` and `src/FolderB/` you can use +```yaml +whitelist_patterns: + - /^src\/FolderA\/(.*)/ + - /^src\/FolderB\/(.*)/ +``` + +**exclude** + +*Default: []* + +This is a list of patterns that will be ignored by phpmd. With this option you can skip directories like tests. Leave this option blank to run phpmd for every php file. + +**report_format** + +*Default: text* + +This sets the output [renderer](https://phpmd.org/documentation/#renderers) of phpmd. +Available formats: ansi, text. + +**ruleset** + +*Default: [phpmd.xml.dist]* + +With this parameter you will be able to configure the rule/rulesets you want to use. You can use the standard +sets provided by PhpMd or you can configure your own xml configuration as described in the [PhpMd Documentation](https://phpmd.org/documentation/creating-a-ruleset.html) + +The full list of rules/rulesets can be found at [PhpMd Rules](https://phpmd.org/rules/index.html) + +**triggered_by** + +*Default: [php]* + +This is a list of extensions to be sniffed. diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index d4927541b..87cd2e8d1 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -261,6 +261,13 @@ services: tags: - {name: grumphp.task, task: phpmd} + GrumPHP\Task\PhpMd3: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - {name: grumphp.task, task: phpmd3} + GrumPHP\Task\PhpMnd: arguments: - '@process_builder' diff --git a/src/Task/PhpMd3.php b/src/Task/PhpMd3.php new file mode 100644 index 000000000..579491d36 --- /dev/null +++ b/src/Task/PhpMd3.php @@ -0,0 +1,97 @@ + + */ +class PhpMd3 extends AbstractExternalTask +{ + public static function getConfigurableOptions(): ConfigOptionsResolver + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'whitelist_patterns' => [], + 'exclude' => [], + 'report_format' => 'text', + 'ruleset' => ['phpmd.xml.dist'], + 'triggered_by' => ['php'], + ]); + + $resolver->addAllowedTypes('whitelist_patterns', ['array']); + $resolver->addAllowedTypes('exclude', ['array']); + $resolver->addAllowedTypes('report_format', ['string']); + $resolver->addAllowedValues('report_format', ['text', 'ansi']); + $resolver->addAllowedTypes('ruleset', ['array']); + $resolver->addAllowedTypes('triggered_by', ['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(); + + $whitelistPatterns = $config['whitelist_patterns']; + $extensions = $config['triggered_by']; + + $files = $context->getFiles(); + if (\count($whitelistPatterns)) { + $files = $files->paths($whitelistPatterns); + } + $files = $files->extensions($extensions); + + if (0 === \count($files)) { + return TaskResult::createSkipped($this, $context); + } + + $arguments = $this->processBuilder->createArgumentsForCommand('phpmd'); + $arguments->add('analyze'); + $arguments->addOptionalArgument('--format=%s', $config['report_format']); + + foreach ($config['ruleset'] as $ruleset) { + $arguments->addOptionalArgument('--ruleset=%s', $ruleset); + } + + foreach ($config['exclude'] as $exclude) { + $arguments->addOptionalArgument('--exclude=%s', $exclude); + } + + foreach ($extensions as $extension) { + $arguments->addOptionalArgument('--suffixes=%s', $extension); + } + + $arguments->addFiles($files); + + $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/PhpMdTest3.php b/test/Unit/Task/PhpMdTest3.php new file mode 100644 index 000000000..4f712c018 --- /dev/null +++ b/test/Unit/Task/PhpMdTest3.php @@ -0,0 +1,173 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public static function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'whitelist_patterns' => [], + 'exclude' => [], + 'report_format' => 'text', + 'ruleset' => ['phpmd.xml.dist'], + 'triggered_by' => ['php'], + ] + ]; + + yield 'invalidcase' => [ + [ + 'whitelist_patterns' => 'thisisnotanarray' + ], + 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, ['hello.php']), + function () { + $this->mockProcessBuilder('phpmd', $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('phpmd', self::mockProcess(0)); + } + ]; + } + + public static function provideSkipsOnStuff(): iterable + { + yield 'no-files' => [ + [], + self::mockContext(RunContext::class), + function () {} + ]; + yield 'no-files-after-triggered-by' => [ + [], + self::mockContext(RunContext::class, ['notaphpfile.txt']), + function () {} + ]; + yield 'no-files-after-whitelist' => [ + [ + 'whitelist_patterns' => ['src/'], + ], + self::mockContext(RunContext::class, ['test/file.php']), + function () {} + ]; + } + + public static function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'phpmd', + [ + 'analyze', + '--format=text', + '--ruleset=phpmd.xml.dist', + '--suffixes=php', + 'hello.php', + 'hello2.php', + ] + ]; + + yield 'excludes' => [ + [ + 'exclude' => ['hello.php', 'hello2.php'], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'phpmd', + [ + 'analyze', + '--format=text', + '--ruleset=phpmd.xml.dist', + '--exclude=hello.php', + '--exclude=hello2.php', + '--suffixes=php', + 'hello.php', + 'hello2.php', + ] + ]; + + yield 'rulesets' => [ + [ + 'ruleset' => ['cleancode'], + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'phpmd', + [ + 'analyze', + '--format=text', + '--ruleset=cleancode', + '--suffixes=php', + 'hello.php', + 'hello2.php', + ] + ]; + + yield 'report_formats' => [ + [ + 'report_format' => 'ansi', + ], + self::mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'phpmd', + [ + 'analyze', + '--format=ansi', + '--ruleset=phpmd.xml.dist', + '--suffixes=php', + 'hello.php', + 'hello2.php', + ] + ]; + } +}