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

AbstractString::wordwrap()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 26
c 2
b 0
f 0
nc 72
nop 3
dl 0
loc 44
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\String;
13
14
use Symfony\Component\String\Exception\ExceptionInterface;
15
use Symfony\Component\String\Exception\InvalidArgumentException;
16
use Symfony\Component\String\Exception\RuntimeException;
17
18
/**
19
 * Represents a string of abstract characters.
20
 *
21
 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
22
 * This class is the abstract type to use as a type-hint when the logic you want to
23
 * implement doesn't care about the exact variant it deals with.
24
 *
25
 * @author Nicolas Grekas <[email protected]>
26
 * @author Hugo Hamon <[email protected]>
27
 *
28
 * @throws ExceptionInterface
29
 */
30
abstract class AbstractString implements \Stringable, \JsonSerializable
31
{
32
    public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER;
33
    public const PREG_SET_ORDER = \PREG_SET_ORDER;
34
    public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE;
35
    public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL;
36
37
    public const PREG_SPLIT = 0;
38
    public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY;
39
    public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE;
40
    public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE;
41
42
    protected string $string = '';
43
    protected ?bool $ignoreCase = false;
44
45
    abstract public function __construct(string $string = '');
46
47
    /**
48
     * Unwraps instances of AbstractString back to strings.
49
     *
50
     * @return string[]|array
51
     */
52
    public static function unwrap(array $values): array
53
    {
54
        foreach ($values as $k => $v) {
55
            if ($v instanceof self) {
56
                $values[$k] = $v->__toString();
57
            } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) {
58
                $values[$k] = $v;
59
            }
60
        }
61
62
        return $values;
63
    }
64
65
    /**
66
     * Wraps (and normalizes) strings in instances of AbstractString.
67
     *
68
     * @return static[]|array
69
     */
70
    public static function wrap(array $values): array
71
    {
72
        $i = 0;
73
        $keys = null;
74
75
        foreach ($values as $k => $v) {
76
            if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) {
77
                $keys ??= array_keys($values);
78
                $keys[$i] = $j;
79
            }
80
81
            if (\is_string($v)) {
82
                $values[$k] = new static($v);
83
            } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) {
84
                $values[$k] = $v;
85
            }
86
87
            ++$i;
88
        }
89
90
        return null !== $keys ? array_combine($keys, $values) : $values;
91
    }
92
93
    /**
94
     * @param string|string[] $needle
95
     */
96
    public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
97
    {
98
        $str = clone $this;
99
        $i = \PHP_INT_MAX;
100
101
        if (\is_string($needle)) {
102
            $needle = [$needle];
103
        }
104
105
        foreach ($needle as $n) {
106
            $n = (string) $n;
107
            $j = $this->indexOf($n, $offset);
108
109
            if (null !== $j && $j < $i) {
110
                $i = $j;
111
                $str->string = $n;
112
            }
113
        }
114
115
        if (\PHP_INT_MAX === $i) {
116
            return $str;
117
        }
118
119
        if (!$includeNeedle) {
120
            $i += $str->length();
121
        }
122
123
        return $this->slice($i);
124
    }
125
126
    /**
127
     * @param string|string[] $needle
128
     */
129
    public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
130
    {
131
        $str = clone $this;
132
        $i = null;
133
134
        if (\is_string($needle)) {
135
            $needle = [$needle];
136
        }
137
138
        foreach ($needle as $n) {
139
            $n = (string) $n;
140
            $j = $this->indexOfLast($n, $offset);
141
142
            if (null !== $j && $j >= $i) {
143
                $i = $offset = $j;
144
                $str->string = $n;
145
            }
146
        }
147
148
        if (null === $i) {
149
            return $str;
150
        }
151
152
        if (!$includeNeedle) {
153
            $i += $str->length();
154
        }
155
156
        return $this->slice($i);
157
    }
158
159
    abstract public function append(string ...$suffix): static;
160
161
    /**
162
     * @param string|string[] $needle
163
     */
164
    public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
165
    {
166
        $str = clone $this;
167
        $i = \PHP_INT_MAX;
168
169
        if (\is_string($needle)) {
170
            $needle = [$needle];
171
        }
172
173
        foreach ($needle as $n) {
174
            $n = (string) $n;
175
            $j = $this->indexOf($n, $offset);
176
177
            if (null !== $j && $j < $i) {
178
                $i = $j;
179
                $str->string = $n;
180
            }
181
        }
182
183
        if (\PHP_INT_MAX === $i) {
184
            return $str;
185
        }
186
187
        if ($includeNeedle) {
188
            $i += $str->length();
189
        }
190
191
        return $this->slice(0, $i);
192
    }
193
194
    /**
195
     * @param string|string[] $needle
196
     */
197
    public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
198
    {
199
        $str = clone $this;
200
        $i = null;
201
202
        if (\is_string($needle)) {
203
            $needle = [$needle];
204
        }
205
206
        foreach ($needle as $n) {
207
            $n = (string) $n;
208
            $j = $this->indexOfLast($n, $offset);
209
210
            if (null !== $j && $j >= $i) {
211
                $i = $offset = $j;
212
                $str->string = $n;
213
            }
214
        }
215
216
        if (null === $i) {
217
            return $str;
218
        }
219
220
        if ($includeNeedle) {
221
            $i += $str->length();
222
        }
223
224
        return $this->slice(0, $i);
225
    }
226
227
    /**
228
     * @return int[]
229
     */
230
    public function bytesAt(int $offset): array
231
    {
232
        $str = $this->slice($offset, 1);
233
234
        return '' === $str->string ? [] : array_values(unpack('C*', $str->string));
235
    }
236
237
    abstract public function camel(): static;
238
239
    /**
240
     * @return static[]
241
     */
242
    abstract public function chunk(int $length = 1): array;
243
244
    public function collapseWhitespace(): static
245
    {
246
        $str = clone $this;
247
        $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C");
248
249
        return $str;
250
    }
251
252
    /**
253
     * @param string|string[] $needle
254
     */
255
    public function containsAny(string|iterable $needle): bool
256
    {
257
        return null !== $this->indexOf($needle);
258
    }
259
260
    /**
261
     * @param string|string[] $suffix
262
     */
263
    public function endsWith(string|iterable $suffix): bool
264
    {
265
        if (\is_string($suffix)) {
266
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
267
        }
268
269
        foreach ($suffix as $s) {
270
            if ($this->endsWith((string) $s)) {
271
                return true;
272
            }
273
        }
274
275
        return false;
276
    }
277
278
    public function ensureEnd(string $suffix): static
279
    {
280
        if (!$this->endsWith($suffix)) {
281
            return $this->append($suffix);
282
        }
283
284
        $suffix = preg_quote($suffix);
285
        $regex = '{('.$suffix.')(?:'.$suffix.')++$}D';
286
287
        return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1');
288
    }
289
290
    public function ensureStart(string $prefix): static
291
    {
292
        $prefix = new static($prefix);
293
294
        if (!$this->startsWith($prefix)) {
295
            return $this->prepend($prefix);
0 ignored issues
show
Bug introduced by
$prefix of type iterable is incompatible with the type string expected by parameter $prefix of Symfony\Component\String\AbstractString::prepend(). ( Ignorable by Annotation )

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

295
            return $this->prepend(/** @scrutinizer ignore-type */ $prefix);
Loading history...
296
        }
297
298
        $str = clone $this;
299
        $i = $prefixLen = $prefix->length();
300
301
        while ($this->indexOf($prefix, $i) === $i) {
302
            $str = $str->slice($prefixLen);
303
            $i += $prefixLen;
304
        }
305
306
        return $str;
307
    }
308
309
    /**
310
     * @param string|string[] $string
311
     */
312
    public function equalsTo(string|iterable $string): bool
313
    {
314
        if (\is_string($string)) {
315
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
316
        }
317
318
        foreach ($string as $s) {
319
            if ($this->equalsTo((string) $s)) {
320
                return true;
321
            }
322
        }
323
324
        return false;
325
    }
326
327
    abstract public function folded(): static;
328
329
    public function ignoreCase(): static
330
    {
331
        $str = clone $this;
332
        $str->ignoreCase = true;
333
334
        return $str;
335
    }
336
337
    /**
338
     * @param string|string[] $needle
339
     */
340
    public function indexOf(string|iterable $needle, int $offset = 0): ?int
341
    {
342
        if (\is_string($needle)) {
343
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
344
        }
345
346
        $i = \PHP_INT_MAX;
347
348
        foreach ($needle as $n) {
349
            $j = $this->indexOf((string) $n, $offset);
350
351
            if (null !== $j && $j < $i) {
352
                $i = $j;
353
            }
354
        }
355
356
        return \PHP_INT_MAX === $i ? null : $i;
357
    }
358
359
    /**
360
     * @param string|string[] $needle
361
     */
362
    public function indexOfLast(string|iterable $needle, int $offset = 0): ?int
363
    {
364
        if (\is_string($needle)) {
365
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
366
        }
367
368
        $i = null;
369
370
        foreach ($needle as $n) {
371
            $j = $this->indexOfLast((string) $n, $offset);
372
373
            if (null !== $j && $j >= $i) {
374
                $i = $offset = $j;
375
            }
376
        }
377
378
        return $i;
379
    }
380
381
    public function isEmpty(): bool
382
    {
383
        return '' === $this->string;
384
    }
385
386
    abstract public function join(array $strings, ?string $lastGlue = null): static;
387
388
    public function jsonSerialize(): string
389
    {
390
        return $this->string;
391
    }
392
393
    abstract public function length(): int;
394
395
    abstract public function lower(): static;
396
397
    /**
398
     * Matches the string using a regular expression.
399
     *
400
     * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression.
401
     *
402
     * @return array All matches in a multi-dimensional array ordered according to flags
403
     */
404
    abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array;
405
406
    abstract public function padBoth(int $length, string $padStr = ' '): static;
407
408
    abstract public function padEnd(int $length, string $padStr = ' '): static;
409
410
    abstract public function padStart(int $length, string $padStr = ' '): static;
411
412
    abstract public function prepend(string ...$prefix): static;
413
414
    public function repeat(int $multiplier): static
415
    {
416
        if (0 > $multiplier) {
417
            throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier));
418
        }
419
420
        $str = clone $this;
421
        $str->string = str_repeat($str->string, $multiplier);
422
423
        return $str;
424
    }
425
426
    abstract public function replace(string $from, string $to): static;
427
428
    abstract public function replaceMatches(string $fromRegexp, string|callable $to): static;
429
430
    abstract public function reverse(): static;
431
432
    abstract public function slice(int $start = 0, ?int $length = null): static;
433
434
    abstract public function snake(): static;
435
436
    abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static;
437
438
    /**
439
     * @return static[]
440
     */
441
    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
442
    {
443
        if (null === $flags) {
444
            throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.');
445
        }
446
447
        if ($this->ignoreCase) {
448
            $delimiter .= 'i';
449
        }
450
451
        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));
452
453
        try {
454
            if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) {
455
                throw new RuntimeException('Splitting failed with error: '.preg_last_error_msg());
456
            }
457
        } finally {
458
            restore_error_handler();
459
        }
460
461
        $str = clone $this;
462
463
        if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) {
464
            foreach ($chunks as &$chunk) {
465
                $str->string = $chunk[0];
466
                $chunk[0] = clone $str;
467
            }
468
        } else {
469
            foreach ($chunks as &$chunk) {
470
                $str->string = $chunk;
471
                $chunk = clone $str;
472
            }
473
        }
474
475
        return $chunks;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $chunks returns the type array<mixed,array|string> which is incompatible with the documented return type array<mixed,Symfony\Comp...\String\AbstractString>.
Loading history...
476
    }
477
478
    /**
479
     * @param string|string[] $prefix
480
     */
481
    public function startsWith(string|iterable $prefix): bool
482
    {
483
        if (\is_string($prefix)) {
484
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
485
        }
486
487
        foreach ($prefix as $prefix) {
488
            if ($this->startsWith((string) $prefix)) {
489
                return true;
490
            }
491
        }
492
493
        return false;
494
    }
495
496
    abstract public function title(bool $allWords = false): static;
497
498
    public function toByteString(?string $toEncoding = null): ByteString
499
    {
500
        $b = new ByteString();
501
502
        $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding;
503
504
        if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') {
0 ignored issues
show
Unused Code introduced by
The assignment to $fromEncoding is dead and can be removed.
Loading history...
505
            $b->string = $this->string;
506
507
            return $b;
508
        }
509
510
        try {
511
            $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
512
        } catch (\ValueError $e) {
513
            if (!\function_exists('iconv')) {
514
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
515
            }
516
517
            $b->string = iconv('UTF-8', $toEncoding, $this->string);
518
        }
519
520
        return $b;
521
    }
522
523
    public function toCodePointString(): CodePointString
524
    {
525
        return new CodePointString($this->string);
526
    }
527
528
    public function toString(): string
529
    {
530
        return $this->string;
531
    }
532
533
    public function toUnicodeString(): UnicodeString
534
    {
535
        return new UnicodeString($this->string);
536
    }
537
538
    abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;
539
540
    abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;
541
542
    /**
543
     * @param string|string[] $prefix
544
     */
545
    public function trimPrefix($prefix): static
546
    {
547
        if (\is_array($prefix) || $prefix instanceof \Traversable) { // don't use is_iterable(), it's slow
548
            foreach ($prefix as $s) {
549
                $t = $this->trimPrefix($s);
550
551
                if ($t->string !== $this->string) {
552
                    return $t;
553
                }
554
            }
555
556
            return clone $this;
557
        }
558
559
        $str = clone $this;
560
561
        if ($prefix instanceof self) {
0 ignored issues
show
introduced by
$prefix is never a sub-type of self.
Loading history...
562
            $prefix = $prefix->string;
563
        } else {
564
            $prefix = (string) $prefix;
565
        }
566
567
        if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) {
0 ignored issues
show
Bug introduced by
It seems like $this->ignoreCase can also be of type null; however, parameter $case_insensitive of substr_compare() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

567
        if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), /** @scrutinizer ignore-type */ $this->ignoreCase)) {
Loading history...
568
            $str->string = substr($this->string, \strlen($prefix));
569
        }
570
571
        return $str;
572
    }
573
574
    abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;
575
576
    /**
577
     * @param string|string[] $suffix
578
     */
579
    public function trimSuffix($suffix): static
580
    {
581
        if (\is_array($suffix) || $suffix instanceof \Traversable) { // don't use is_iterable(), it's slow
582
            foreach ($suffix as $s) {
583
                $t = $this->trimSuffix($s);
584
585
                if ($t->string !== $this->string) {
586
                    return $t;
587
                }
588
            }
589
590
            return clone $this;
591
        }
592
593
        $str = clone $this;
594
595
        if ($suffix instanceof self) {
0 ignored issues
show
introduced by
$suffix is never a sub-type of self.
Loading history...
596
            $suffix = $suffix->string;
597
        } else {
598
            $suffix = (string) $suffix;
599
        }
600
601
        if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) {
0 ignored issues
show
Bug introduced by
It seems like $this->ignoreCase can also be of type null; however, parameter $case_insensitive of substr_compare() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

601
        if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, /** @scrutinizer ignore-type */ $this->ignoreCase)) {
Loading history...
602
            $str->string = substr($this->string, 0, -\strlen($suffix));
603
        }
604
605
        return $str;
606
    }
607
608
    public function truncate(int $length, string $ellipsis = '', bool $cut = true): static
609
    {
610
        $stringLength = $this->length();
611
612
        if ($stringLength <= $length) {
613
            return clone $this;
614
        }
615
616
        $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;
617
618
        if ($length < $ellipsisLength) {
619
            $ellipsisLength = 0;
620
        }
621
622
        if (!$cut) {
623
            if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
624
                return clone $this;
625
            }
626
627
            $length += $ellipsisLength;
628
        }
629
630
        $str = $this->slice(0, $length - $ellipsisLength);
631
632
        return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str;
633
    }
634
635
    abstract public function upper(): static;
636
637
    /**
638
     * Returns the printable length on a terminal.
639
     */
640
    abstract public function width(bool $ignoreAnsiDecoration = true): int;
641
642
    public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static
643
    {
644
        $lines = '' !== $break ? $this->split($break) : [clone $this];
645
        $chars = [];
646
        $mask = '';
647
648
        if (1 === \count($lines) && '' === $lines[0]->string) {
649
            return $lines[0];
650
        }
651
652
        foreach ($lines as $i => $line) {
653
            if ($i) {
654
                $chars[] = $break;
655
                $mask .= '#';
656
            }
657
658
            foreach ($line->chunk() as $char) {
659
                $chars[] = $char->string;
660
                $mask .= ' ' === $char->string ? ' ' : '?';
661
            }
662
        }
663
664
        $string = '';
665
        $j = 0;
666
        $b = $i = -1;
667
        $mask = wordwrap($mask, $width, '#', $cut);
668
669
        while (false !== $b = strpos($mask, '#', $b + 1)) {
670
            for (++$i; $i < $b; ++$i) {
671
                $string .= $chars[$j];
672
                unset($chars[$j++]);
673
            }
674
675
            if ($break === $chars[$j] || ' ' === $chars[$j]) {
676
                unset($chars[$j++]);
677
            }
678
679
            $string .= $break;
680
        }
681
682
        $str = clone $this;
683
        $str->string = $string.implode('', $chars);
684
685
        return $str;
686
    }
687
688
    public function __sleep(): array
689
    {
690
        return ['string'];
691
    }
692
693
    public function __clone()
694
    {
695
        $this->ignoreCase = false;
696
    }
697
698
    public function __toString(): string
699
    {
700
        return $this->string;
701
    }
702
}
703