Browse Source

feat: Introduce percentage bar as new default progress output (#7603)

Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>
Greg Korba 1 year ago
parent
commit
cfdeac13ad

+ 1 - 1
ci-integration.sh

@@ -5,4 +5,4 @@ IFS='
 '
 CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "${COMMIT_RANGE}")
 if ! echo "${CHANGED_FILES}" | grep -qE "^(\\.php-cs-fixer(\\.dist)?\\.php|composer\\.lock)$"; then EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); else EXTRA_ARGS=''; fi
-vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --stop-on-violation --using-cache=no ${EXTRA_ARGS}
+vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --show-progress=dots --stop-on-violation --using-cache=no ${EXTRA_ARGS}

+ 3 - 2
doc/usage.rst

@@ -54,7 +54,7 @@ NOTE: the output for the following formats are generated in accordance with sche
 
 The ``--quiet`` Do not output any message.
 
-The ``--verbose`` option will show the applied rules. When using the ``txt`` format it will also display progress notifications.
+The ``--verbose`` option will show the applied rules. When using the ``txt`` format it will also display progress output (progress bar by default, but can be changed using ``--show-progress`` option).
 
 NOTE: if there is an error like "errors reported during linting after fixing", you can use this to be even more verbose for debugging purpose
 
@@ -109,8 +109,9 @@ The ``--show-progress`` option allows you to choose the way process progress is
 
 * ``none``: disables progress output;
 * ``dots``: multiline progress output with number of files and percentage on each line. Note that with this option, the files list is evaluated before processing to get the total number of files and then kept in memory to avoid using the file iterator twice. This has an impact on memory usage so using this option is not recommended on very large projects;
+* ``bar``: single line progress output with number of files and calculated percentage. Similar to ``dots`` output, it has to evaluate files list twice;
 
-If the option is not provided, it defaults to ``dots`` unless a config file that disables output is used, in which case it defaults to ``none``. This option has no effect if the verbosity of the command is less than ``verbose``.
+If the option is not provided, it defaults to ``bar`` unless a config file that disables output, or non-txt reporter is used, then it defaults to ``none``.
 
 .. code-block:: console
 

+ 1 - 1
phpstan.dist.neon

@@ -21,7 +21,7 @@ parameters:
         -
             message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
             path: tests
-            count: 1035
+            count: 1034
 
         -
             message: '#Call to static method .+ with .+ will always evaluate to true.$#'

+ 2 - 1
src/Console/Command/FixCommand.php

@@ -157,8 +157,9 @@ use Symfony\Component\Stopwatch\Stopwatch;
 
             * <comment>none</comment>: disables progress output;
             * <comment>dots</comment>: multiline progress output with number of files and percentage on each line.
+            * <comment>bar</comment>: single line progress output with number of files and calculated percentage.
 
-            If the option is not provided, it defaults to <comment>dots</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
+            If the option is not provided, it defaults to <comment>bar</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
 
                 <info>$ php %command.full_name% --verbose --show-progress=dots</info>
 

+ 4 - 5
src/Console/ConfigurationResolver.php

@@ -42,7 +42,6 @@ use PhpCsFixer\ToolInfoInterface;
 use PhpCsFixer\Utils;
 use PhpCsFixer\WhitespacesFixerConfig;
 use PhpCsFixer\WordMatcher;
-use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Finder\Finder as SymfonyFinder;
 
@@ -402,18 +401,18 @@ final class ConfigurationResolver
     public function getProgressType(): string
     {
         if (null === $this->progress) {
-            if (OutputInterface::VERBOSITY_VERBOSE <= $this->options['verbosity'] && 'txt' === $this->getFormat()) {
+            if ('txt' === $this->getFormat()) {
                 $progressType = $this->options['show-progress'];
 
                 if (null === $progressType) {
                     $progressType = $this->getConfig()->getHideProgress()
                         ? ProgressOutputType::NONE
-                        : ProgressOutputType::DOTS;
-                } elseif (!\in_array($progressType, ProgressOutputType::AVAILABLE, true)) {
+                        : ProgressOutputType::BAR;
+                } elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
                     throw new InvalidConfigurationException(sprintf(
                         'The progress type "%s" is not defined, supported are %s.',
                         $progressType,
-                        Utils::naturalLanguageJoin(ProgressOutputType::AVAILABLE)
+                        Utils::naturalLanguageJoin(ProgressOutputType::all())
                     ));
                 }
 

+ 76 - 0
src/Console/Output/Progress/PercentageBarOutput.php

@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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\Output\Progress;
+
+use PhpCsFixer\Console\Output\OutputContext;
+use PhpCsFixer\FixerFileProcessedEvent;
+use Symfony\Component\Console\Helper\ProgressBar;
+
+/**
+ * Output writer to show the progress of a FixCommand using progress bar (percentage).
+ *
+ * @internal
+ */
+final class PercentageBarOutput implements ProgressOutputInterface
+{
+    /** @readonly */
+    private OutputContext $context;
+
+    private ProgressBar $progressBar;
+
+    public function __construct(OutputContext $context)
+    {
+        $this->context = $context;
+
+        $this->progressBar = new ProgressBar($context->getOutput(), $this->context->getFilesCount());
+        $this->progressBar->setBarCharacter('█');
+        $this->progressBar->setEmptyBarCharacter('░');
+        $this->progressBar->setProgressCharacter('░');
+        $this->progressBar->setFormat('normal');
+
+        $this->progressBar->start();
+    }
+
+    /**
+     * This class is not intended to be serialized,
+     * and cannot be deserialized (see __wakeup method).
+     */
+    public function __sleep(): array
+    {
+        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+    }
+
+    /**
+     * Disable the deserialization of the class to prevent attacker executing
+     * code by leveraging the __destruct method.
+     *
+     * @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
+     */
+    public function __wakeup(): void
+    {
+        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+    }
+
+    public function onFixerFileProcessed(FixerFileProcessedEvent $event): void
+    {
+        $this->progressBar->advance(1);
+
+        if ($this->progressBar->getProgress() === $this->progressBar->getMaxSteps()) {
+            $this->context->getOutput()->write("\n\n");
+        }
+    }
+
+    public function printLegend(): void {}
+}

+ 11 - 7
src/Console/Output/Progress/ProgressOutputFactory.php

@@ -21,6 +21,15 @@ use PhpCsFixer\Console\Output\OutputContext;
  */
 final class ProgressOutputFactory
 {
+    /**
+     * @var array<string, class-string<ProgressOutputInterface>>
+     */
+    private static array $outputTypeMap = [
+        ProgressOutputType::NONE => NullOutput::class,
+        ProgressOutputType::DOTS => DotsOutput::class,
+        ProgressOutputType::BAR => PercentageBarOutput::class,
+    ];
+
     public function create(string $outputType, OutputContext $context): ProgressOutputInterface
     {
         if (null === $context->getOutput()) {
@@ -36,16 +45,11 @@ final class ProgressOutputFactory
             );
         }
 
-        return ProgressOutputType::NONE === $outputType
-            ? new NullOutput()
-            : new DotsOutput($context);
+        return new self::$outputTypeMap[$outputType]($context);
     }
 
     private function isBuiltInType(string $outputType): bool
     {
-        return \in_array($outputType, [
-            ProgressOutputType::NONE,
-            ProgressOutputType::DOTS,
-        ], true);
+        return \in_array($outputType, ProgressOutputType::all(), true);
     }
 }

+ 12 - 4
src/Console/Output/Progress/ProgressOutputType.php

@@ -21,9 +21,17 @@ final class ProgressOutputType
 {
     public const NONE = 'none';
     public const DOTS = 'dots';
+    public const BAR = 'bar';
 
-    public const AVAILABLE = [
-        self::NONE,
-        self::DOTS,
-    ];
+    /**
+     * @return list<ProgressOutputType::*>
+     */
+    public static function all(): array
+    {
+        return [
+            self::BAR,
+            self::DOTS,
+            self::NONE,
+        ];
+    }
 }

+ 9 - 4
tests/Console/ConfigurationResolverTest.php

@@ -89,7 +89,7 @@ final class ConfigurationResolverTest extends TestCase
             'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
         ], $config);
 
-        self::assertSame('dots', $resolver->getProgressType());
+        self::assertSame('bar', $resolver->getProgressType());
     }
 
     public function testResolveProgressWithNegativeConfigAndNegativeOption(): void
@@ -102,7 +102,7 @@ final class ConfigurationResolverTest extends TestCase
             'verbosity' => OutputInterface::VERBOSITY_NORMAL,
         ], $config);
 
-        self::assertSame('none', $resolver->getProgressType());
+        self::assertSame('bar', $resolver->getProgressType());
     }
 
     /**
@@ -139,9 +139,12 @@ final class ConfigurationResolverTest extends TestCase
         self::assertSame($progressType, $resolver->getProgressType());
     }
 
+    /**
+     * @return iterable<string, array{0: ProgressOutputType::*}>
+     */
     public static function provideProgressTypeCases(): iterable
     {
-        foreach (ProgressOutputType::AVAILABLE as $outputType) {
+        foreach (ProgressOutputType::all() as $outputType) {
             yield $outputType => [$outputType];
         }
     }
@@ -155,7 +158,7 @@ final class ConfigurationResolverTest extends TestCase
         ]);
 
         $this->expectException(InvalidConfigurationException::class);
-        $this->expectExceptionMessage('The progress type "foo" is not defined, supported are "none" and "dots".');
+        $this->expectExceptionMessage('The progress type "foo" is not defined, supported are "bar", "dots" and "none".');
 
         $resolver->getProgressType();
     }
@@ -1088,6 +1091,7 @@ For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/b
         self::assertNull($resolver->getCacheFile());
         self::assertInstanceOf(\PhpCsFixer\Differ\UnifiedDiffer::class, $resolver->getDiffer());
         self::assertSame('json', $resolver->getReporter()->getFormat());
+        self::assertSame('none', $resolver->getProgressType());
     }
 
     /**
@@ -1133,6 +1137,7 @@ For more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/b
         self::assertFalse($resolver->getUsingCache());
         self::assertNull($resolver->getCacheFile());
         self::assertSame('xml', $resolver->getReporter()->getFormat());
+        self::assertSame('none', $resolver->getProgressType());
     }
 
     public function testDeprecationOfPassingOtherThanNoOrYes(): void

+ 83 - 0
tests/Console/Output/Progress/PercentageBarOutputTest.php

@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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\Output\Progress;
+
+use PhpCsFixer\Console\Output\OutputContext;
+use PhpCsFixer\Console\Output\Progress\PercentageBarOutput;
+use PhpCsFixer\FixerFileProcessedEvent;
+use PhpCsFixer\Tests\TestCase;
+use Symfony\Component\Console\Output\BufferedOutput;
+
+/**
+ * @internal
+ *
+ * @covers \PhpCsFixer\Console\Output\Progress\PercentageBarOutput
+ */
+final class PercentageBarOutputTest extends TestCase
+{
+    /**
+     * @param list<array{0: FixerFileProcessedEvent::STATUS_*, 1?: int}> $statuses
+     *
+     * @dataProvider providePercentageBarProgressOutputCases
+     */
+    public function testPercentageBarProgressOutput(array $statuses, string $expectedOutput, int $width): void
+    {
+        $nbFiles = 0;
+        $this->foreachStatus($statuses, static function () use (&$nbFiles): void {
+            ++$nbFiles;
+        });
+
+        $output = new BufferedOutput();
+
+        $processOutput = new PercentageBarOutput(new OutputContext($output, $width, $nbFiles));
+
+        $this->foreachStatus($statuses, static function (int $status) use ($processOutput): void {
+            $processOutput->onFixerFileProcessed(new FixerFileProcessedEvent($status));
+        });
+
+        self::assertSame($expectedOutput, rtrim($output->fetch()));
+    }
+
+    /**
+     * @return iterable<int|string, array{0: list<array{0: FixerFileProcessedEvent::STATUS_*, 1?: int}>, 1: string, 2: int}>
+     */
+    public static function providePercentageBarProgressOutputCases(): iterable
+    {
+        yield [
+            [
+                [FixerFileProcessedEvent::STATUS_NO_CHANGES, 100],
+            ],
+            '   0/100 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%'.PHP_EOL.
+            ' 100/100 [████████████████████████████] 100%',
+            80,
+        ];
+    }
+
+    /**
+     * @param list<array{0: FixerFileProcessedEvent::STATUS_*, 1?: int}> $statuses
+     * @param \Closure(FixerFileProcessedEvent::STATUS_*): void          $action
+     */
+    private function foreachStatus(array $statuses, \Closure $action): void
+    {
+        foreach ($statuses as $status) {
+            $multiplier = $status[1] ?? 1;
+            $status = $status[0];
+
+            for ($i = 0; $i < $multiplier; ++$i) {
+                $action($status);
+            }
+        }
+    }
+}

Some files were not shown because too many files changed in this diff