GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( cc39b3...2b121f )
by Anton
04:25 queued 01:05
created

ProgressBar::overwrite()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 27
c 2
b 0
f 0
nc 6
nop 1
dl 0
loc 41
rs 8.0555
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Console\Helper;
13
14
use Symfony\Component\Console\Cursor;
15
use Symfony\Component\Console\Exception\LogicException;
16
use Symfony\Component\Console\Output\ConsoleOutputInterface;
17
use Symfony\Component\Console\Output\ConsoleSectionOutput;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Terminal;
20
21
/**
22
 * The ProgressBar provides helpers to display progress output.
23
 *
24
 * @author Fabien Potencier <[email protected]>
25
 * @author Chris Jones <[email protected]>
26
 */
27
final class ProgressBar
28
{
29
    public const FORMAT_VERBOSE = 'verbose';
30
    public const FORMAT_VERY_VERBOSE = 'very_verbose';
31
    public const FORMAT_DEBUG = 'debug';
32
    public const FORMAT_NORMAL = 'normal';
33
34
    private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
35
    private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
36
    private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
37
    private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
38
39
    private int $barWidth = 28;
40
    private string $barChar;
41
    private string $emptyBarChar = '-';
42
    private string $progressChar = '>';
43
    private ?string $format = null;
44
    private ?string $internalFormat = null;
45
    private ?int $redrawFreq = 1;
46
    private int $writeCount = 0;
47
    private float $lastWriteTime = 0;
48
    private float $minSecondsBetweenRedraws = 0;
49
    private float $maxSecondsBetweenRedraws = 1;
50
    private OutputInterface $output;
51
    private int $step = 0;
52
    private int $startingStep = 0;
53
    private ?int $max = null;
54
    private int $startTime;
55
    private int $stepWidth;
56
    private float $percent = 0.0;
57
    private array $messages = [];
58
    private bool $overwrite = true;
59
    private Terminal $terminal;
60
    private ?string $previousMessage = null;
61
    private Cursor $cursor;
62
    private array $placeholders = [];
63
64
    private static array $formatters;
65
    private static array $formats;
66
67
    /**
68
     * @param int $max Maximum steps (0 if unknown)
69
     */
70
    public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
71
    {
72
        if ($output instanceof ConsoleOutputInterface) {
73
            $output = $output->getErrorOutput();
74
        }
75
76
        $this->output = $output;
77
        $this->setMaxSteps($max);
78
        $this->terminal = new Terminal();
79
80
        if (0 < $minSecondsBetweenRedraws) {
81
            $this->redrawFreq = null;
82
            $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
83
        }
84
85
        if (!$this->output->isDecorated()) {
86
            // disable overwrite when output does not support ANSI codes.
87
            $this->overwrite = false;
88
89
            // set a reasonable redraw frequency so output isn't flooded
90
            $this->redrawFreq = null;
91
        }
92
93
        $this->startTime = time();
94
        $this->cursor = new Cursor($output);
95
    }
96
97
    /**
98
     * Sets a placeholder formatter for a given name, globally for all instances of ProgressBar.
99
     *
100
     * This method also allow you to override an existing placeholder.
101
     *
102
     * @param string                       $name     The placeholder name (including the delimiter char like %)
103
     * @param callable(ProgressBar):string $callable A PHP callable
104
     */
105
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
106
    {
107
        self::$formatters ??= self::initPlaceholderFormatters();
108
109
        self::$formatters[$name] = $callable;
110
    }
111
112
    /**
113
     * Gets the placeholder formatter for a given name.
114
     *
115
     * @param string $name The placeholder name (including the delimiter char like %)
116
     */
117
    public static function getPlaceholderFormatterDefinition(string $name): ?callable
118
    {
119
        self::$formatters ??= self::initPlaceholderFormatters();
120
121
        return self::$formatters[$name] ?? null;
122
    }
123
124
    /**
125
     * Sets a placeholder formatter for a given name, for this instance only.
126
     *
127
     * @param callable(ProgressBar):string $callable A PHP callable
128
     */
129
    public function setPlaceholderFormatter(string $name, callable $callable): void
130
    {
131
        $this->placeholders[$name] = $callable;
132
    }
133
134
    /**
135
     * Gets the placeholder formatter for a given name.
136
     *
137
     * @param string $name The placeholder name (including the delimiter char like %)
138
     */
139
    public function getPlaceholderFormatter(string $name): ?callable
140
    {
141
        return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name);
142
    }
143
144
    /**
145
     * Sets a format for a given name.
146
     *
147
     * This method also allow you to override an existing format.
148
     *
149
     * @param string $name   The format name
150
     * @param string $format A format string
151
     */
152
    public static function setFormatDefinition(string $name, string $format): void
153
    {
154
        self::$formats ??= self::initFormats();
155
156
        self::$formats[$name] = $format;
157
    }
158
159
    /**
160
     * Gets the format for a given name.
161
     *
162
     * @param string $name The format name
163
     */
164
    public static function getFormatDefinition(string $name): ?string
165
    {
166
        self::$formats ??= self::initFormats();
167
168
        return self::$formats[$name] ?? null;
169
    }
170
171
    /**
172
     * Associates a text with a named placeholder.
173
     *
174
     * The text is displayed when the progress bar is rendered but only
175
     * when the corresponding placeholder is part of the custom format line
176
     * (by wrapping the name with %).
177
     *
178
     * @param string $message The text to associate with the placeholder
179
     * @param string $name    The name of the placeholder
180
     */
181
    public function setMessage(string $message, string $name = 'message'): void
182
    {
183
        $this->messages[$name] = $message;
184
    }
185
186
    public function getMessage(string $name = 'message'): ?string
187
    {
188
        return $this->messages[$name] ?? null;
189
    }
190
191
    public function getStartTime(): int
192
    {
193
        return $this->startTime;
194
    }
195
196
    public function getMaxSteps(): int
197
    {
198
        return $this->max ?? 0;
199
    }
200
201
    public function getProgress(): int
202
    {
203
        return $this->step;
204
    }
205
206
    private function getStepWidth(): int
207
    {
208
        return $this->stepWidth;
209
    }
210
211
    public function getProgressPercent(): float
212
    {
213
        return $this->percent;
214
    }
215
216
    public function getBarOffset(): float
217
    {
218
        return floor(null !== $this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
219
    }
220
221
    public function getEstimated(): float
222
    {
223
        if (0 === $this->step || $this->step === $this->startingStep) {
224
            return 0;
225
        }
226
227
        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max);
228
    }
229
230
    public function getRemaining(): float
231
    {
232
        if (!$this->step) {
233
            return 0;
234
        }
235
236
        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
237
    }
238
239
    public function setBarWidth(int $size): void
240
    {
241
        $this->barWidth = max(1, $size);
242
    }
243
244
    public function getBarWidth(): int
245
    {
246
        return $this->barWidth;
247
    }
248
249
    public function setBarCharacter(string $char): void
250
    {
251
        $this->barChar = $char;
252
    }
253
254
    public function getBarCharacter(): string
255
    {
256
        return $this->barChar ?? (null !== $this->max ? '=' : $this->emptyBarChar);
257
    }
258
259
    public function setEmptyBarCharacter(string $char): void
260
    {
261
        $this->emptyBarChar = $char;
262
    }
263
264
    public function getEmptyBarCharacter(): string
265
    {
266
        return $this->emptyBarChar;
267
    }
268
269
    public function setProgressCharacter(string $char): void
270
    {
271
        $this->progressChar = $char;
272
    }
273
274
    public function getProgressCharacter(): string
275
    {
276
        return $this->progressChar;
277
    }
278
279
    public function setFormat(string $format): void
280
    {
281
        $this->format = null;
282
        $this->internalFormat = $format;
283
    }
284
285
    /**
286
     * Sets the redraw frequency.
287
     *
288
     * @param int|null $freq The frequency in steps
289
     */
290
    public function setRedrawFrequency(?int $freq): void
291
    {
292
        $this->redrawFreq = null !== $freq ? max(1, $freq) : null;
293
    }
294
295
    public function minSecondsBetweenRedraws(float $seconds): void
296
    {
297
        $this->minSecondsBetweenRedraws = $seconds;
298
    }
299
300
    public function maxSecondsBetweenRedraws(float $seconds): void
301
    {
302
        $this->maxSecondsBetweenRedraws = $seconds;
303
    }
304
305
    /**
306
     * Returns an iterator that will automatically update the progress bar when iterated.
307
     *
308
     * @template TKey
309
     * @template TValue
310
     *
311
     * @param iterable<TKey, TValue> $iterable
312
     * @param int|null               $max      Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
313
     *
314
     * @return iterable<TKey, TValue>
315
     */
316
    public function iterate(iterable $iterable, ?int $max = null): iterable
317
    {
318
        if (0 === $max) {
319
            $max = null;
320
        }
321
322
        $max ??= is_countable($iterable) ? \count($iterable) : null;
323
324
        if (0 === $max) {
325
            $this->max = 0;
326
            $this->stepWidth = 2;
327
            $this->finish();
328
329
            return;
330
        }
331
332
        $this->start($max);
333
334
        foreach ($iterable as $key => $value) {
335
            yield $key => $value;
336
337
            $this->advance();
338
        }
339
340
        $this->finish();
341
    }
342
343
    /**
344
     * Starts the progress output.
345
     *
346
     * @param int|null $max     Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
347
     * @param int      $startAt The starting point of the bar (useful e.g. when resuming a previously started bar)
348
     */
349
    public function start(?int $max = null, int $startAt = 0): void
350
    {
351
        $this->startTime = time();
352
        $this->step = $startAt;
353
        $this->startingStep = $startAt;
354
355
        $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;
356
357
        if (null !== $max) {
358
            $this->setMaxSteps($max);
359
        }
360
361
        $this->display();
362
    }
363
364
    /**
365
     * Advances the progress output X steps.
366
     *
367
     * @param int $step Number of steps to advance
368
     */
369
    public function advance(int $step = 1): void
370
    {
371
        $this->setProgress($this->step + $step);
372
    }
373
374
    /**
375
     * Sets whether to overwrite the progressbar, false for new line.
376
     */
377
    public function setOverwrite(bool $overwrite): void
378
    {
379
        $this->overwrite = $overwrite;
380
    }
381
382
    public function setProgress(int $step): void
383
    {
384
        if ($this->max && $step > $this->max) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
385
            $this->max = $step;
386
        } elseif ($step < 0) {
387
            $step = 0;
388
        }
389
390
        $redrawFreq = $this->redrawFreq ?? (($this->max ?? 10) / 10);
391
        $prevPeriod = $redrawFreq ? (int) ($this->step / $redrawFreq) : 0;
392
        $currPeriod = $redrawFreq ? (int) ($step / $redrawFreq) : 0;
393
        $this->step = $step;
394
        $this->percent = match ($this->max) {
395
            null => 0,
396
            0 => 1,
397
            default => (float) $this->step / $this->max,
398
        };
399
        $timeInterval = microtime(true) - $this->lastWriteTime;
400
401
        // Draw regardless of other limits
402
        if ($this->max === $step) {
403
            $this->display();
404
405
            return;
406
        }
407
408
        // Throttling
409
        if ($timeInterval < $this->minSecondsBetweenRedraws) {
410
            return;
411
        }
412
413
        // Draw each step period, but not too late
414
        if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
415
            $this->display();
416
        }
417
    }
418
419
    public function setMaxSteps(?int $max): void
420
    {
421
        if (0 === $max) {
422
            $max = null;
423
        }
424
425
        $this->format = null;
426
        if (null === $max) {
427
            $this->max = null;
428
            $this->stepWidth = 4;
429
        } else {
430
            $this->max = max(0, $max);
431
            $this->stepWidth = Helper::width((string) $this->max);
432
        }
433
    }
434
435
    /**
436
     * Finishes the progress output.
437
     */
438
    public function finish(): void
439
    {
440
        if (null === $this->max) {
441
            $this->max = $this->step;
442
        }
443
444
        if (($this->step === $this->max || null === $this->max) && !$this->overwrite) {
445
            // prevent double 100% output
446
            return;
447
        }
448
449
        $this->setProgress($this->max ?? $this->step);
450
    }
451
452
    /**
453
     * Outputs the current progress string.
454
     */
455
    public function display(): void
456
    {
457
        if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
458
            return;
459
        }
460
461
        if (null === $this->format) {
462
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
463
        }
464
465
        $this->overwrite($this->buildLine());
466
    }
467
468
    /**
469
     * Removes the progress bar from the current line.
470
     *
471
     * This is useful if you wish to write some output
472
     * while a progress bar is running.
473
     * Call display() to show the progress bar again.
474
     */
475
    public function clear(): void
476
    {
477
        if (!$this->overwrite) {
478
            return;
479
        }
480
481
        if (null === $this->format) {
482
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
483
        }
484
485
        $this->overwrite('');
486
    }
487
488
    private function setRealFormat(string $format): void
489
    {
490
        // try to use the _nomax variant if available
491
        if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
492
            $this->format = self::getFormatDefinition($format.'_nomax');
493
        } elseif (null !== self::getFormatDefinition($format)) {
494
            $this->format = self::getFormatDefinition($format);
495
        } else {
496
            $this->format = $format;
497
        }
498
    }
499
500
    /**
501
     * Overwrites a previous message to the output.
502
     */
503
    private function overwrite(string $message): void
504
    {
505
        if ($this->previousMessage === $message) {
506
            return;
507
        }
508
509
        $originalMessage = $message;
510
511
        if ($this->overwrite) {
512
            if (null !== $this->previousMessage) {
513
                if ($this->output instanceof ConsoleSectionOutput) {
514
                    $messageLines = explode("\n", $this->previousMessage);
515
                    $lineCount = \count($messageLines);
516
                    foreach ($messageLines as $messageLine) {
517
                        $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
518
                        if ($messageLineLength > $this->terminal->getWidth()) {
519
                            $lineCount += floor($messageLineLength / $this->terminal->getWidth());
520
                        }
521
                    }
522
                    $this->output->clear($lineCount);
0 ignored issues
show
Bug introduced by
The method clear() does not exist on Symfony\Component\Console\Output\OutputInterface. It seems like you code against a sub-type of Symfony\Component\Console\Output\OutputInterface such as Symfony\Component\Consol...ut\ConsoleSectionOutput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

522
                    $this->output->/** @scrutinizer ignore-call */ 
523
                                   clear($lineCount);
Loading history...
523
                } else {
524
                    $lineCount = substr_count($this->previousMessage, "\n");
525
                    for ($i = 0; $i < $lineCount; ++$i) {
526
                        $this->cursor->moveToColumn(1);
527
                        $this->cursor->clearLine();
528
                        $this->cursor->moveUp();
529
                    }
530
531
                    $this->cursor->moveToColumn(1);
532
                    $this->cursor->clearLine();
533
                }
534
            }
535
        } elseif ($this->step > 0) {
536
            $message = \PHP_EOL.$message;
537
        }
538
539
        $this->previousMessage = $originalMessage;
540
        $this->lastWriteTime = microtime(true);
541
542
        $this->output->write($message);
543
        ++$this->writeCount;
544
    }
545
546
    private function determineBestFormat(): string
547
    {
548
        return match ($this->output->getVerbosity()) {
549
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
550
            OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX,
551
            OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX,
552
            OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX,
553
            default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX,
554
        };
555
    }
556
557
    private static function initPlaceholderFormatters(): array
558
    {
559
        return [
560
            'bar' => function (self $bar, OutputInterface $output) {
561
                $completeBars = $bar->getBarOffset();
562
                $display = str_repeat($bar->getBarCharacter(), $completeBars);
0 ignored issues
show
Bug introduced by
$completeBars of type double is incompatible with the type integer expected by parameter $times of str_repeat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

562
                $display = str_repeat($bar->getBarCharacter(), /** @scrutinizer ignore-type */ $completeBars);
Loading history...
563
                if ($completeBars < $bar->getBarWidth()) {
564
                    $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
565
                    $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
566
                }
567
568
                return $display;
569
            },
570
            'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2),
571
            'remaining' => function (self $bar) {
572
                if (null === $bar->getMaxSteps()) {
0 ignored issues
show
introduced by
The condition null === $bar->getMaxSteps() is always false.
Loading history...
573
                    throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
574
                }
575
576
                return Helper::formatTime($bar->getRemaining(), 2);
577
            },
578
            'estimated' => function (self $bar) {
579
                if (null === $bar->getMaxSteps()) {
0 ignored issues
show
introduced by
The condition null === $bar->getMaxSteps() is always false.
Loading history...
580
                    throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
581
                }
582
583
                return Helper::formatTime($bar->getEstimated(), 2);
584
            },
585
            'memory' => fn (self $bar) => Helper::formatMemory(memory_get_usage(true)),
0 ignored issues
show
Unused Code introduced by
The parameter $bar is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

585
            'memory' => fn (/** @scrutinizer ignore-unused */ self $bar) => Helper::formatMemory(memory_get_usage(true)),

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
586
            'current' => fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT),
587
            'max' => fn (self $bar) => $bar->getMaxSteps(),
588
            'percent' => fn (self $bar) => floor($bar->getProgressPercent() * 100),
589
        ];
590
    }
591
592
    private static function initFormats(): array
593
    {
594
        return [
595
            self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
596
            self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',
597
598
            self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
599
            self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
600
601
            self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
602
            self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
603
604
            self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
605
            self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
606
        ];
607
    }
608
609
    private function buildLine(): string
610
    {
611
        \assert(null !== $this->format);
612
613
        $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
614
        $callback = function ($matches) {
615
            if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
616
                $text = $formatter($this, $this->output);
617
            } elseif (isset($this->messages[$matches[1]])) {
618
                $text = $this->messages[$matches[1]];
619
            } else {
620
                return $matches[0];
621
            }
622
623
            if (isset($matches[2])) {
624
                $text = sprintf('%'.$matches[2], $text);
625
            }
626
627
            return $text;
628
        };
629
        $line = preg_replace_callback($regex, $callback, $this->format);
630
631
        // gets string length for each sub line with multiline format
632
        $linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line));
633
634
        $linesWidth = max($linesLength);
635
636
        $terminalWidth = $this->terminal->getWidth();
637
        if ($linesWidth <= $terminalWidth) {
638
            return $line;
639
        }
640
641
        $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth);
642
643
        return preg_replace_callback($regex, $callback, $this->format);
644
    }
645
}
646