Repository: FriendsOfPHP/PHP-CS-Fixer (php)

Issue:
Provide a command-line argument to list all existing rulesets
In PhpStorm there is a chooser to run a predefined ruleset but available rulesets are hard-coded since there is no easy way to fetch them from CS-Fixer. 
For now, we have the following standards: `"PSR1", "PSR2", "Symfony", "DoctrineAnnotation", "PHP70Migration", "PHP71Migration"` and looking into https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/2.18/src/RuleSet/Sets it looks like the list is far from complete.

Since we support different versions of PHP-CS-Fixer, a command-line option that outputs the available standards will allow us to list all rulesets instead of a limited list supported by 2.8.0 (the earliest version we support).

Pull request description:
Add list-sets command
closes #5621 
example: `php php-cs-fixer list-sets --format json`

Patch:
diff --git a/.gitattributes b/.gitattributes
index e74aa0950..88a99d0ef 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,7 @@
 # @TODO 3.0 replace following `tests/... export-ignore` with single `tests/ export-ignore`
 /tests/**/*Test.php export-ignore
 /tests/AbstractDoctrineAnnotationFixerTestCase.php export-ignore
+/tests/Console/Report/ListSetsReport/AbstractReporterTestCase.php export-ignore
 /tests/Console/TestToolInfo.php export-ignore
 /tests/Differ/AbstractDifferTestCase.php export-ignore
 /tests/Fixtures/ export-ignore
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 409f84c78..95a6407f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,33 +3,6 @@ CHANGELOG for PHP CS Fixer
 
 This file contains changelogs for stable releases only.
 
-Changelog for v2.18.6
----------------------
-
-* bug #5586 Add support for nullsafe object operator ("?->") (kubawerlos)
-* bug #5597 Tokens - fix for checking block edges  (kubawerlos)
-* bug #5604 Custom annotations @type changed into @var (Leprechaunz)
-* bug #5606 DoctrineAnnotationBracesFixer false positive (Leprechaunz)
-* bug #5610 BracesFixer - fix braces of match expression (Leprechaunz)
-* bug #5615 GroupImportFixer severely broken (Leprechaunz)
-* bug #5617 ClassAttributesSeparationFixer - fix for using visibility for class elements (kubawerlos)
-* bug #5618 GroupImportFixer - fix removal of import type when mixing multiple types (Leprechaunz)
-* bug #5622 Exclude Doctrine documents from final fixer (ossinkine)
-* bug #5630 PhpdocTypesOrderFixer - handle complex keys (Leprechaunz)
-* minor #5554 DX: use tmp file in sys_temp_dir for integration tests (keradus)
-* minor #5564 DX: make integration tests matching entries in FixerFactoryTest (kubawerlos)
-* minor #5603 DX: DocumentationGenerator - no need to re-configure Differ (keradus)
-* minor #5612 DX: use ::class whenever possible (kubawerlos)
-* minor #5619 DX: allow XDebugHandler v2 (keradus)
-* minor #5623 DX: when displaying app version, don't put extra space if there is no CODENAME available (keradus)
-* minor #5626 DX: update PHPStan and way of ignoring flickering PHPStan exception (keradus)
-* minor #5629 DX: fix CiIntegrationTest (keradus)
-* minor #5636 DX: remove 'create' method in internal classes (keradus)
-* minor #5637 DX: do not calculate bitmap via helper anymore (keradus)
-* minor #5639 Move fix reports (classes and schemas) (keradus)
-* minor #5640 DX: use constants for PHPUnit version (keradus)
-* minor #5646 Cleanup YodaStyleFixerTest (kubawerlos)
-
 Changelog for v2.18.5
 ---------------------
 
diff --git a/doc/schemas/list-sets/schema.json b/doc/schemas/list-sets/schema.json
new file mode 100644
index 000000000..8869c8ff9
--- /dev/null
+++ b/doc/schemas/list-sets/schema.json
@@ -0,0 +1,31 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "sets": {
+            "type": "object",
+            "additionalProperties": {
+                "type": "object",
+                "properties": {
+                    "description": {
+                        "type": "string"
+                    },
+                    "isRisky": {
+                        "type": "boolean"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "required": [
+                    "description",
+                    "isRisky",
+                    "name"
+                ]
+            }
+        }
+    },
+    "required": [
+        "sets"
+    ]
+}
diff --git a/src/Console/Application.php b/src/Console/Application.php
index b5b7f8e56..a93d8a1a5 100644
--- a/src/Console/Application.php
+++ b/src/Console/Application.php
@@ -16,6 +16,7 @@
 use PhpCsFixer\Console\Command\FixCommand;
 use PhpCsFixer\Console\Command\HelpCommand;
 use PhpCsFixer\Console\Command\ListFilesCommand;
+use PhpCsFixer\Console\Command\ListSetsCommand;
 use PhpCsFixer\Console\Command\SelfUpdateCommand;
 use PhpCsFixer\Console\SelfUpdate\GithubClient;
 use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
@@ -57,6 +58,7 @@ public function __construct()
         $this->add(new DescribeCommand());
         $this->add(new FixCommand($this->toolInfo));
         $this->add(new ListFilesCommand($this->toolInfo));
+        $this->add(new ListSetsCommand());
         $this->add(new SelfUpdateCommand(
             new NewVersionChecker(new GithubClient()),
             $this->toolInfo,
diff --git a/src/Console/Command/ListFilesCommand.php b/src/Console/Command/ListFilesCommand.php
index 4913ed188..92721dd5e 100644
--- a/src/Console/Command/ListFilesCommand.php
+++ b/src/Console/Command/ListFilesCommand.php
@@ -74,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
             [
                 'config' => $passedConfig,
             ],
-            getcwd(),
+            $cwd,
             $this->toolInfo
         );
 
diff --git a/src/Console/Command/ListSetsCommand.php b/src/Console/Command/ListSetsCommand.php
new file mode 100644
index 000000000..a2d3223e1
--- /dev/null
+++ b/src/Console/Command/ListSetsCommand.php
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Command;
+
+use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
+use PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory;
+use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
+use PhpCsFixer\Console\Report\ListSetsReport\TextReporter;
+use PhpCsFixer\RuleSet\RuleSets;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Formatter\OutputFormatter;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class ListSetsCommand extends Command
+{
+    protected static $defaultName = 'list-sets';
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setDefinition(
+                [
+                    new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.', (new TextReporter())->getFormat()),
+                ]
+            )
+            ->setDescription('List all available RuleSets.')
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $reporter = $this->resolveReporterWithFactory(
+            $input->getOption('format'),
+            new ReporterFactory()
+        );
+        $reportSummary = new ReportSummary(
+            array_values(RuleSets::getSetDefinitions())
+        );
+        $report = $reporter->generate($reportSummary);
+
+        $output->isDecorated()
+            ? $output->write(OutputFormatter::escape($report))
+            : $output->write($report, false, OutputInterface::OUTPUT_RAW)
+        ;
+
+        return 0;
+    }
+
+    /**
+     * @param string $format
+     */
+    private function resolveReporterWithFactory($format, ReporterFactory $factory)
+    {
+        try {
+            $factory->registerBuiltInReporters();
+            $reporter = $factory->getReporter($format);
+        } catch (\UnexpectedValueException $e) {
+            $formats = $factory->getFormats();
+            sort($formats);
+
+            throw new InvalidConfigurationException(sprintf('The format "%s" is not defined, supported are "%s".', $format, implode('", "', $formats)));
+        }
+
+        return $reporter;
+    }
+}
diff --git a/src/Console/Report/ListSetsReport/JsonReporter.php b/src/Console/Report/ListSetsReport/JsonReporter.php
new file mode 100644
index 000000000..bf868ebfa
--- /dev/null
+++ b/src/Console/Report/ListSetsReport/JsonReporter.php
@@ -0,0 +1,54 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Report\ListSetsReport;
+
+use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class JsonReporter implements ReporterInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormat()
+    {
+        return 'json';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function generate(ReportSummary $reportSummary)
+    {
+        $json = ['sets' => []];
+
+        $sets = $reportSummary->getSets();
+        usort($sets, function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b) {
+            return $a->getName() > $b->getName() ? 1 : -1;
+        });
+
+        foreach ($sets as $set) {
+            $json['sets'][$set->getName()] = [
+                'description' => $set->getDescription(),
+                'isRisky' => $set->isRisky(),
+                'name' => $set->getName(),
+            ];
+        }
+
+        return json_encode($json, JSON_PRETTY_PRINT);
+    }
+}
diff --git a/src/Console/Report/ListSetsReport/ReportSummary.php b/src/Console/Report/ListSetsReport/ReportSummary.php
new file mode 100644
index 000000000..75b852974
--- /dev/null
+++ b/src/Console/Report/ListSetsReport/ReportSummary.php
@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Report\ListSetsReport;
+
+use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class ReportSummary
+{
+    /**
+     * @var RuleSetDescriptionInterface[]
+     */
+    private $sets;
+
+    /**
+     * @param RuleSetDescriptionInterface[] $sets
+     */
+    public function __construct(
+        array $sets
+    ) {
+        $this->sets = $sets;
+    }
+
+    /**
+     * @return RuleSetDescriptionInterface[]
+     */
+    public function getSets()
+    {
+        return $this->sets;
+    }
+}
diff --git a/src/Console/Report/ListSetsReport/ReporterFactory.php b/src/Console/Report/ListSetsReport/ReporterFactory.php
new file mode 100644
index 000000000..c5207c52d
--- /dev/null
+++ b/src/Console/Report/ListSetsReport/ReporterFactory.php
@@ -0,0 +1,95 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Report\ListSetsReport;
+
+use Symfony\Component\Finder\Finder as SymfonyFinder;
+use Symfony\Component\Finder\SplFileInfo;
+
+/**
+ * @author Boris Gorbylev <ekho@ekho.name>
+ *
+ * @internal
+ */
+final class ReporterFactory
+{
+    /** @var ReporterInterface[] */
+    private $reporters = [];
+
+    public function registerBuiltInReporters()
+    {
+        /** @var null|string[] $builtInReporters */
+        static $builtInReporters;
+
+        if (null === $builtInReporters) {
+            $builtInReporters = [];
+
+            /** @var SplFileInfo $file */
+            foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
+                $relativeNamespace = $file->getRelativePath();
+                $builtInReporters[] = sprintf(
+                    '%s\\%s%s',
+                    __NAMESPACE__,
+                    $relativeNamespace ? $relativeNamespace.'\\' : '',
+                    $file->getBasename('.php')
+                );
+            }
+        }
+
+        foreach ($builtInReporters as $reporterClass) {
+            $this->registerReporter(new $reporterClass());
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return $this
+     */
+    public function registerReporter(ReporterInterface $reporter)
+    {
+        $format = $reporter->getFormat();
+
+        if (isset($this->reporters[$format])) {
+            throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is already registered.', $format));
+        }
+
+        $this->reporters[$format] = $reporter;
+
+        return $this;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getFormats()
+    {
+        $formats = array_keys($this->reporters);
+        sort($formats);
+
+        return $formats;
+    }
+
+    /**
+     * @param string $format
+     *
+     * @return ReporterInterface
+     */
+    public function getReporter($format)
+    {
+        if (!isset($this->reporters[$format])) {
+            throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is not registered.', $format));
+        }
+
+        return $this->reporters[$format];
+    }
+}
diff --git a/src/Console/Report/ListSetsReport/ReporterInterface.php b/src/Console/Report/ListSetsReport/ReporterInterface.php
new file mode 100644
index 000000000..b074f50bf
--- /dev/null
+++ b/src/Console/Report/ListSetsReport/ReporterInterface.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Report\ListSetsReport;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+interface ReporterInterface
+{
+    /**
+     * @return string
+     */
+    public function getFormat();
+
+    /**
+     * Process changed files array. Returns generated report.
+     *
+     * @return string
+     */
+    public function generate(ReportSummary $reportSummary);
+}
diff --git a/src/Console/Report/ListSetsReport/TextReporter.php b/src/Console/Report/ListSetsReport/TextReporter.php
new file mode 100644
index 000000000..1eb4a5d60
--- /dev/null
+++ b/src/Console/Report/ListSetsReport/TextReporter.php
@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Console\Report\ListSetsReport;
+
+use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class TextReporter implements ReporterInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormat()
+    {
+        return 'txt';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function generate(ReportSummary $reportSummary)
+    {
+        $output = '';
+        $i = 0;
+
+        $sets = $reportSummary->getSets();
+        usort($sets, function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b) {
+            return $a->getName() > $b->getName() ? 1 : -1;
+        });
+
+        foreach ($sets as $set) {
+            ++$i;
+            $output .= sprintf('%2d) %s', $i, $set->getName()).PHP_EOL.'      '.$set->getDescription().PHP_EOL;
+
+            if ($set->isRisky()) {
+                $output .= '      Set contains risky rules.'.PHP_EOL;
+            }
+        }
+
+        return $output;
+    }
+}


Test patch:
diff --git a/tests/Console/Command/ListFilesCommandTest.php b/tests/Console/Command/ListFilesCommandTest.php
index 3b7f71394..c1308c616 100644
--- a/tests/Console/Command/ListFilesCommandTest.php
+++ b/tests/Console/Command/ListFilesCommandTest.php
@@ -35,6 +35,7 @@ public function testListWithConfig()
         // make the test also work on windows
         $expectedPath = str_replace('/', \DIRECTORY_SEPARATOR, $expectedPath);
 
+        static::assertSame(0, $commandTester->getStatusCode());
         static::assertSame(escapeshellarg($expectedPath).PHP_EOL, $commandTester->getDisplay());
     }
 
diff --git a/tests/Console/Command/ListSetsCommandTest.php b/tests/Console/Command/ListSetsCommandTest.php
new file mode 100644
index 000000000..2fb6b8c78
--- /dev/null
+++ b/tests/Console/Command/ListSetsCommandTest.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Command;
+
+use PhpCsFixer\Console\Application;
+use PhpCsFixer\Console\Command\ListSetsCommand;
+use PhpCsFixer\Tests\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Command\ListSetsCommand
+ */
+final class ListSetsCommandTest extends TestCase
+{
+    public function testListWithTxtFormat()
+    {
+        $commandTester = $this->doTestExecute([
+            '--format' => 'txt',
+        ]);
+
+        $resultRaw = $commandTester->getDisplay();
+
+        $expectedResultStart = ' 1) @DoctrineAnnotation'.PHP_EOL.'      Rules covering Doctrine annotations';
+        static::assertStringStartsWith($expectedResultStart, $resultRaw);
+        static::assertSame(0, $commandTester->getStatusCode());
+    }
+
+    public function testListWithJsonFormat()
+    {
+        $commandTester = $this->doTestExecute([
+            '--format' => 'json',
+        ]);
+
+        $resultRaw = $commandTester->getDisplay();
+
+        static::assertJson($resultRaw);
+        static::assertSame(0, $commandTester->getStatusCode());
+    }
+
+    /**
+     * @return CommandTester
+     */
+    private function doTestExecute(array $arguments)
+    {
+        $application = new Application();
+        $application->add(new ListSetsCommand());
+
+        $command = $application->find('list-sets');
+        $commandTester = new CommandTester($command);
+
+        $commandTester->execute($arguments);
+
+        return $commandTester;
+    }
+}
diff --git a/tests/Console/Report/ListSetsReport/AbstractReporterTestCase.php b/tests/Console/Report/ListSetsReport/AbstractReporterTestCase.php
new file mode 100644
index 000000000..4a542d834
--- /dev/null
+++ b/tests/Console/Report/ListSetsReport/AbstractReporterTestCase.php
@@ -0,0 +1,101 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Report\ListSetsReport;
+
+use PhpCsFixer\Console\Report\ListSetsReport\ReporterInterface;
+use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
+use PhpCsFixer\RuleSet\Sets\PhpCsFixerSet;
+use PhpCsFixer\RuleSet\Sets\SymfonyRiskySet;
+use PhpCsFixer\Tests\TestCase;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+abstract class AbstractReporterTestCase extends TestCase
+{
+    /**
+     * @var null|ReporterInterface
+     */
+    protected $reporter;
+
+    protected function doSetUp()
+    {
+        parent::doSetUp();
+
+        $this->reporter = $this->createReporter();
+    }
+
+    protected function doTearDown()
+    {
+        parent::doTearDown();
+
+        $this->reporter = null;
+    }
+
+    final public function testGetFormat()
+    {
+        static::assertSame(
+            $this->getFormat(),
+            $this->reporter->getFormat()
+        );
+    }
+
+    /**
+     * @param string $expectedReport
+     *
+     * @dataProvider provideGenerateCases
+     */
+    final public function testGenerate($expectedReport, ReportSummary $reportSummary)
+    {
+        $actualReport = $this->reporter->generate($reportSummary);
+
+        $this->assertFormat($expectedReport, $actualReport);
+    }
+
+    /**
+     * @return iterable
+     */
+    final public function provideGenerateCases()
+    {
+        yield 'example' => [
+            $this->createSimpleReport(),
+            new ReportSummary([
+                new SymfonyRiskySet(),
+                new PhpCsFixerSet(),
+            ]),
+        ];
+    }
+
+    /**
+     * @return ReporterInterface
+     */
+    abstract protected function createReporter();
+
+    /**
+     * @return string
+     */
+    abstract protected function getFormat();
+
+    /**
+     * @param string $expected
+     * @param string $input
+     */
+    abstract protected function assertFormat($expected, $input);
+
+    /**
+     * @return string
+     */
+    abstract protected function createSimpleReport();
+}
diff --git a/tests/Console/Report/ListSetsReport/JsonReporterTest.php b/tests/Console/Report/ListSetsReport/JsonReporterTest.php
new file mode 100644
index 000000000..999fee553
--- /dev/null
+++ b/tests/Console/Report/ListSetsReport/JsonReporterTest.php
@@ -0,0 +1,89 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Report\ListSetsReport;
+
+use PhpCsFixer\Console\Report\ListSetsReport\JsonReporter;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Report\ListSetsReport\JsonReporter
+ */
+final class JsonReporterTest extends AbstractReporterTestCase
+{
+    protected function createReporter()
+    {
+        return new JsonReporter();
+    }
+
+    protected function getFormat()
+    {
+        return 'json';
+    }
+
+    protected function assertFormat($expected, $input)
+    {
+        static::assertJsonSchema($input);
+        static::assertJsonStringEqualsJsonString($expected, $input);
+    }
+
+    /**
+     * @return string
+     */
+    protected function createSimpleReport()
+    {
+        return '{
+    "sets": {
+        "@PhpCsFixer": {
+            "description": "Rule set as used by the PHP-CS-Fixer development team, highly opinionated.",
+            "isRisky": false,
+            "name": "@PhpCsFixer"
+        },
+        "@Symfony:risky": {
+            "description": "Rules that follow the official `Symfony Coding Standards <https:\/\/symfony.com\/doc\/current\/contributing\/code\/standards.html>`_.",
+            "isRisky": true,
+            "name": "@Symfony:risky"
+        }
+    }
+}';
+    }
+
+    /**
+     * @param string $json
+     */
+    private static function assertJsonSchema($json)
+    {
+        $jsonPath = __DIR__.'/../../../../doc/schemas/list-sets/schema.json';
+
+        $data = json_decode($json);
+
+        $validator = new \JsonSchema\Validator();
+        $validator->validate(
+            $data,
+            (object) ['$ref' => 'file://'.realpath($jsonPath)]
+        );
+
+        static::assertTrue(
+            $validator->isValid(),
+            implode(
+                "\n",
+                array_map(
+                    static function (array $item) { return sprintf('Property `%s`: %s.', $item['property'], $item['message']); },
+                    $validator->getErrors()
+                )
+            )
+        );
+    }
+}
diff --git a/tests/Console/Report/ListSetsReport/ReportSummaryTest.php b/tests/Console/Report/ListSetsReport/ReportSummaryTest.php
new file mode 100644
index 000000000..59ca4e514
--- /dev/null
+++ b/tests/Console/Report/ListSetsReport/ReportSummaryTest.php
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Report\ListSetsReport;
+
+use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
+use PhpCsFixer\RuleSet\Sets\PhpCsFixerSet;
+use PhpCsFixer\Tests\TestCase;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Report\ListSetsReport\ReportSummary
+ */
+final class ReportSummaryTest extends TestCase
+{
+    public function testReportSummary()
+    {
+        $sets = [
+            new PhpCsFixerSet(),
+        ];
+        $reportSummary = new ReportSummary(
+            $sets
+        );
+
+        static::assertSame($sets, $reportSummary->getSets());
+    }
+}
diff --git a/tests/Console/Report/ListSetsReport/ReporterFactoryTest.php b/tests/Console/Report/ListSetsReport/ReporterFactoryTest.php
new file mode 100644
index 000000000..4f0ed3635
--- /dev/null
+++ b/tests/Console/Report/ListSetsReport/ReporterFactoryTest.php
@@ -0,0 +1,115 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Report\ListSetsReport;
+
+use PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory;
+use PhpCsFixer\Tests\TestCase;
+
+/**
+ * @author Boris Gorbylev <ekho@ekho.name>
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory
+ */
+final class ReporterFactoryTest extends TestCase
+{
+    public function testInterfaceIsFluent()
+    {
+        $builder = new ReporterFactory();
+
+        $testInstance = $builder->registerBuiltInReporters();
+        static::assertSame($builder, $testInstance);
+
+        $double = $this->createReporterDouble('r1');
+        $testInstance = $builder->registerReporter($double);
+        static::assertSame($builder, $testInstance);
+    }
+
+    public function testRegisterBuiltInReports()
+    {
+        $builder = new ReporterFactory();
+
+        static::assertCount(0, $builder->getFormats());
+
+        $builder->registerBuiltInReporters();
+        static::assertSame(
+            ['json', 'txt'],
+            $builder->getFormats()
+        );
+    }
+
+    public function testThatCanRegisterAndGetReports()
+    {
+        $builder = new ReporterFactory();
+
+        $r1 = $this->createReporterDouble('r1');
+        $r2 = $this->createReporterDouble('r2');
+        $r3 = $this->createReporterDouble('r3');
+
+        $builder->registerReporter($r1);
+        $builder->registerReporter($r2);
+        $builder->registerReporter($r3);
+
+        static::assertSame($r1, $builder->getReporter('r1'));
+        static::assertSame($r2, $builder->getReporter('r2'));
+        static::assertSame($r3, $builder->getReporter('r3'));
+    }
+
+    public function testGetFormats()
+    {
+        $builder = new ReporterFactory();
+
+        $r1 = $this->createReporterDouble('r1');
+        $r2 = $this->createReporterDouble('r2');
+        $r3 = $this->createReporterDouble('r3');
+
+        $builder->registerReporter($r1);
+        $builder->registerReporter($r2);
+        $builder->registerReporter($r3);
+
+        static::assertSame(['r1', 'r2', 'r3'], $builder->getFormats());
+    }
+
+    public function testRegisterReportWithOccupiedFormat()
+    {
+        $this->expectException(\UnexpectedValueException::class);
+        $this->expectExceptionMessage('Reporter for format "non_unique_name" is already registered.');
+
+        $factory = new ReporterFactory();
+
+        $r1 = $this->createReporterDouble('non_unique_name');
+        $r2 = $this->createReporterDouble('non_unique_name');
+        $factory->registerReporter($r1);
+        $factory->registerReporter($r2);
+    }
+
+    public function testGetNonRegisteredReport()
+    {
+        $this->expectException(\UnexpectedValueException::class);
+        $this->expectExceptionMessage('Reporter for format "non_registered_format" is not registered.');
+
+        $builder = new ReporterFactory();
+
+        $builder->getReporter('non_registered_format');
+    }
+
+    private function createReporterDouble($format)
+    {
+        $reporter = $this->prophesize(\PhpCsFixer\Console\Report\ListSetsReport\ReporterInterface::class);
+        $reporter->getFormat()->willReturn($format);
+
+        return $reporter->reveal();
+    }
+}
diff --git a/tests/Console/Report/ListSetsReport/TextReporterTest.php b/tests/Console/Report/ListSetsReport/TextReporterTest.php
new file mode 100644
index 000000000..780432d68
--- /dev/null
+++ b/tests/Console/Report/ListSetsReport/TextReporterTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Console\Report\ListSetsReport;
+
+use PhpCsFixer\Console\Report\ListSetsReport\TextReporter;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Report\ListSetsReport\TextReporter
+ */
+final class TextReporterTest extends AbstractReporterTestCase
+{
+    protected function createReporter()
+    {
+        return new TextReporter();
+    }
+
+    protected function getFormat()
+    {
+        return 'txt';
+    }
+
+    protected function assertFormat($expected, $input)
+    {
+        static::assertSame($expected, $input);
+    }
+
+    /**
+     * @return string
+     */
+    protected function createSimpleReport()
+    {
+        return str_replace("\n", PHP_EOL, ' 1) @PhpCsFixer
+      Rule set as used by the PHP-CS-Fixer development team, highly opinionated.
+ 2) @Symfony:risky
+      Rules that follow the official `Symfony Coding Standards <https://symfony.com/doc/current/contributing/code/standards.html>`_.
+      Set contains risky rules.
+');
+    }
+}