• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keradus / PHP-CS-Fixer / 22042339290

15 Feb 2026 08:14PM UTC coverage: 92.957% (-0.2%) from 93.171%
22042339290

push

github

keradus
test: check PHP env in CI jobs

29302 of 31522 relevant lines covered (92.96%)

44.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.15
/src/DocBlock/TypeExpression.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\DocBlock;
16

17
use PhpCsFixer\Preg;
18
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
19
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
20
use PhpCsFixer\Utils;
21

22
/**
23
 * @author Michael Vorisek <https://github.com/mvorisek>
24
 *
25
 * @internal
26
 *
27
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
28
 */
29
final class TypeExpression
30
{
31
    /**
32
     * Regex to match any PHP identifier.
33
     *
34
     * @internal
35
     */
36
    public const REGEX_IDENTIFIER = '(?:(?!(?<!\*)\d)[^\x00-\x2f\x3a-\x40\x5b-\x5e\x60\x7b-\x7f]++)';
37

38
    /**
39
     * Regex to match any PHPDoc type.
40
     *
41
     * @internal
42
     */
43
    public const REGEX_TYPES = '(?<types>(?x) # one or several types separated by `|` or `&`
44
'.self::REGEX_TYPE.'
45
        (?:
46
            \h*(?<glue>[|&])\h*
47
            (?&type)
48
        )*+
49
    )';
50

51
    /**
52
     * Based on:
53
     * - https://github.com/phpstan/phpdoc-parser/blob/1.26.0/doc/grammars/type.abnf fuzzing grammar
54
     * - and https://github.com/phpstan/phpdoc-parser/blob/1.26.0/src/Parser/PhpDocParser.php parser impl.
55
     */
56
    private const REGEX_TYPE = '(?<type>(?x) # single type
57
            (?:co(ntra)?variant\h+)?
58
            (?<nullable>\??\h*)
59
            (?:
60
                (?<array_shape>
61
                    (?<array_shape_name>(?i)(?:array|list|object)(?-i))
62
                    (?<array_shape_start>\h*\{[\h\v*]*)
63
                    (?<array_shape_inners>
64
                        (?<array_shape_inner>
65
                            (?<array_shape_inner_key>(?:(?&constant)|(?&identifier)|(?&name))\h*\??\h*:\h*|)
66
                            (?<array_shape_inner_value>(?&types_inner))
67
                        )
68
                        (?:
69
                            \h*,[\h\v*]*
70
                            (?&array_shape_inner)
71
                        )*+
72
                        (?:\h*,|(?!(?&array_shape_unsealed_variadic)))
73
                    |)
74
                    (?<array_shape_unsealed> # unsealed array shape, e.g. `...`. `...<string>`
75
                        (?<array_shape_unsealed_variadic>\h*\.\.\.)
76
                        (?<array_shape_unsealed_type>
77
                            (?<array_shape_unsealed_type_start>\h*<\h*)
78
                            (?<array_shape_unsealed_type_a>(?&types_inner))
79
                            (?:
80
                                (?<array_shape_unsealed_type_comma>\h*,\h*)
81
                                (?<array_shape_unsealed_type_b>(?&array_shape_unsealed_type_a))
82
                            |)
83
                            \h*>
84
                        |)
85
                    |)
86
                    [\h\v*]*\}
87
                )
88
                |
89
                (?<callable> # callable syntax, e.g. `callable(string, int...): bool`, `\Closure<T>(T, int): T`
90
                    (?<callable_name>(?&name))
91
                    (?<callable_template>
92
                        (?<callable_template_start>\h*<\h*)
93
                        (?<callable_template_inners>
94
                            (?<callable_template_inner>
95
                                (?<callable_template_inner_name>
96
                                    (?&identifier)
97
                                )
98
                                (?<callable_template_inner_b> # template bound
99
                                    \h+(?i)(?<callable_template_inner_b_kw>of|as)(?-i)\h+
100
                                    (?<callable_template_inner_b_types>(?&types_inner))
101
                                |)
102
                                (?<callable_template_inner_d> # template default
103
                                    \h*=\h*
104
                                    (?<callable_template_inner_d_types>(?&types_inner))
105
                                |)
106
                            )
107
                            (?:
108
                                \h*,\h*
109
                                (?&callable_template_inner)
110
                            )*+
111
                        )
112
                        \h*>
113
                        (?=\h*\()
114
                    |)
115
                    (?<callable_start>\h*\(\h*)
116
                    (?<callable_arguments>
117
                        (?<callable_argument>
118
                            (?<callable_argument_type>(?&types_inner))
119
                            (?<callable_argument_is_reference>\h*&|)
120
                            (?<callable_argument_is_variadic>\h*\.\.\.|)
121
                            (?<callable_argument_name>\h*\$(?&identifier)|)
122
                            (?<callable_argument_is_optional>\h*=|)
123
                        )
124
                        (?:
125
                            \h*,\h*
126
                            (?&callable_argument)
127
                        )*+
128
                        (?:\h*,)?
129
                    |)
130
                    \h*\)
131
                    (?:
132
                        \h*\:\h*
133
                        (?<callable_return>(?&type))
134
                    )?
135
                )
136
                |
137
                (?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
138
                    (?<generic_name>(?&name))
139
                    (?<generic_start>\h*<[\h\v*]*)
140
                    (?<generic_types>
141
                        (?&types_inner)
142
                        (?:
143
                            \h*,[\h\v*]*
144
                            (?&types_inner)
145
                        )*+
146
                        (?:\h*,)?
147
                    )
148
                    [\h\v*]*>
149
                )
150
                |
151
                (?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
152
                    (?<class_constant_name>(?&name))
153
                    ::\*?(?:(?&identifier)\*?)*
154
                )
155
                |
156
                (?<constant> # single constant value (case insensitive), e.g.: 1, -1.8E+6, `\'a\'`
157
                    (?i)
158
                    # all sorts of numbers: with or without sign, supports literal separator and several numeric systems,
159
                    # e.g.: 1, +1.1, 1., .1, -1, 123E+8, 123_456_789, 0x7Fb4, 0b0110, 0o777
160
                    [+-]?(?:
161
                        (?:0b[01]++(?:_[01]++)*+)
162
                        | (?:0o[0-7]++(?:_[0-7]++)*+)
163
                        | (?:0x[\da-f]++(?:_[\da-f]++)*+)
164
                        | (?:(?<constant_digits>\d++(?:_\d++)*+)|(?=\.\d))
165
                          (?:\.(?&constant_digits)|(?<=\d)\.)?+
166
                          (?:e[+-]?(?&constant_digits))?+
167
                    )
168
                    | \'(?:[^\'\\\]|\\\.)*+\'
169
                    | "(?:[^"\\\]|\\\.)*+"
170
                    (?-i)
171
                )
172
                |
173
                (?<this> # self reference, e.g.: $this, $self, @static
174
                    (?i)
175
                    [@$](?:this | self | static)
176
                    (?-i)
177
                )
178
                |
179
                (?<name> # full name, e.g.: `int`, `\DateTime`, `\Foo\Bar`, `positive-int`
180
                    \\\?+
181
                    (?<identifier>'.self::REGEX_IDENTIFIER.')
182
                    (?:[\\\\\-](?&identifier))*+
183
                )
184
                |
185
                (?<parenthesized> # parenthesized type, e.g.: `(int)`, `(int|\stdClass)`
186
                    (?<parenthesized_start>
187
                        \(\h*
188
                    )
189
                    (?:
190
                        (?<parenthesized_types>
191
                            (?&types_inner)
192
                        )
193
                        |
194
                        (?<conditional> # conditional type, e.g.: `$foo is \Throwable ? false : $foo`
195
                            (?<conditional_cond_left>
196
                                (?:\$(?&identifier))
197
                                |
198
                                (?<conditional_cond_left_types>(?&types_inner))
199
                            )
200
                            (?<conditional_cond_middle>
201
                                \h+(?i)is(?:\h+not)?(?-i)\h+
202
                            )
203
                            (?<conditional_cond_right_types>(?&types_inner))
204
                            (?<conditional_true_start>\h*\?\h*)
205
                            (?<conditional_true_types>(?&types_inner))
206
                            (?<conditional_false_start>\h*:\h*)
207
                            (?<conditional_false_types>(?&types_inner))
208
                        )
209
                    )
210
                    \h*\)
211
                )
212
            )
213
            (?<array> # array, e.g.: `string[]`, `array<int, string>[][]`
214
                (\h*\[\h*\])*
215
            )
216
            (?:(?=1)0
217
                (?<types_inner>(?>
218
                    (?&type)
219
                    (?:
220
                        \h*[|&]\h*
221
                        (?&type)
222
                    )*+
223
                ))
224
            |)
225
        )';
226

227
    private const ALIASES = [
228
        'boolean' => 'bool',
229
        'callback' => 'callable',
230
        'double' => 'float',
231
        'false' => 'bool',
232
        'integer' => 'int',
233
        'list' => 'array',
234
        'real' => 'float',
235
        'true' => 'bool',
236
    ];
237

238
    private string $value;
239

240
    private bool $isCompositeType;
241

242
    /** @var null|'&'|'|' */
243
    private ?string $typesGlue = null;
244

245
    /** @var list<array{start_index: int, expression: self}> */
246
    private array $innerTypeExpressions = [];
247

248
    private ?NamespaceAnalysis $namespace;
249

250
    /** @var list<NamespaceUseAnalysis> */
251
    private array $namespaceUses;
252

253
    /**
254
     * @param list<NamespaceUseAnalysis> $namespaceUses
255
     */
256
    public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
257
    {
258
        $this->value = $value;
357✔
259
        $this->namespace = $namespace;
357✔
260
        $this->namespaceUses = $namespaceUses;
357✔
261

262
        $this->parse();
357✔
263
    }
264

265
    public function toString(): string
266
    {
267
        return $this->value;
228✔
268
    }
269

270
    /**
271
     * @return list<string>
272
     */
273
    public function getTypes(): array
274
    {
275
        if ($this->isCompositeType) {
221✔
276
            return array_map(
174✔
277
                static fn (array $type) => $type['expression']->toString(),
174✔
278
                $this->innerTypeExpressions,
174✔
279
            );
174✔
280
        }
281

282
        return [$this->value];
179✔
283
    }
284

285
    /**
286
     * Determines if type expression is a composite type (union or intersection).
287
     */
288
    public function isCompositeType(): bool
289
    {
290
        return $this->isCompositeType;
155✔
291
    }
292

293
    public function isUnionType(): bool
294
    {
295
        return $this->isCompositeType && '|' === $this->typesGlue;
26✔
296
    }
297

298
    public function isIntersectionType(): bool
299
    {
300
        return $this->isCompositeType && '&' === $this->typesGlue;
5✔
301
    }
302

303
    /**
304
     * @return null|'&'|'|'
305
     */
306
    public function getTypesGlue(): ?string
307
    {
308
        return $this->typesGlue;
54✔
309
    }
310

311
    /**
312
     * @param \Closure(self): self $callback
313
     */
314
    public function mapTypes(\Closure $callback): self
315
    {
316
        $value = $this->value;
54✔
317
        $startIndexOffset = 0;
54✔
318

319
        foreach ($this->innerTypeExpressions as [
54✔
320
            'start_index' => $startIndexOrig,
54✔
321
            'expression' => $inner,
54✔
322
        ]) {
54✔
323
            $innerValueOrig = $inner->value;
53✔
324

325
            $inner = $inner->mapTypes($callback);
53✔
326

327
            if ($inner->value !== $innerValueOrig) {
53✔
328
                $value = substr_replace(
48✔
329
                    $value,
48✔
330
                    $inner->value,
48✔
331
                    $startIndexOrig + $startIndexOffset,
48✔
332
                    \strlen($innerValueOrig),
48✔
333
                );
48✔
334

335
                $startIndexOffset += \strlen($inner->value) - \strlen($innerValueOrig);
48✔
336
            }
337
        }
338

339
        $type = $value === $this->value
54✔
340
            ? $this
54✔
341
            : $this->inner($value);
48✔
342

343
        return $callback($type);
54✔
344
    }
345

346
    /**
347
     * @param \Closure(self): void $callback
348
     */
349
    public function walkTypes(\Closure $callback): void
350
    {
351
        $this->mapTypes(static function (self $type) use ($callback) {
1✔
352
            $valueOrig = $type->value;
1✔
353
            $callback($type);
1✔
354
            \assert($type->value === $valueOrig);
1✔
355

356
            return $type;
1✔
357
        });
1✔
358
    }
359

360
    /**
361
     * @param \Closure(self, self): (-1|0|1) $compareCallback
362
     */
363
    public function sortTypes(\Closure $compareCallback): self
364
    {
365
        return $this->mapTypes(function (self $type) use ($compareCallback): self {
52✔
366
            if (!$type->isCompositeType) {
52✔
367
                return $type;
52✔
368
            }
369

370
            $innerTypeExpressions = Utils::stableSort(
51✔
371
                $type->innerTypeExpressions,
51✔
372
                static fn (array $v): self => $v['expression'],
51✔
373
                $compareCallback,
51✔
374
            );
51✔
375

376
            if ($innerTypeExpressions !== $type->innerTypeExpressions) {
51✔
377
                $value = implode(
51✔
378
                    $type->getTypesGlue(),
51✔
379
                    array_map(static fn (array $v): string => $v['expression']->toString(), $innerTypeExpressions),
51✔
380
                );
51✔
381

382
                return $this->inner($value);
51✔
383
            }
384

385
            return $type;
25✔
386
        });
52✔
387
    }
388

389
    public function removeDuplicateTypes(): self
390
    {
391
        return $this->mapTypes(function (self $type): self {
×
392
            if (!$type->isCompositeType) {
×
393
                return $type;
×
394
            }
395

396
            $seenNormalized = [];
×
397
            $uniqueTypeExpressions = [];
×
398

399
            foreach ($type->innerTypeExpressions as $innerType) {
×
400
                $normalized = $innerType['expression']
×
401
                    ->sortTypes(static fn (self $a, self $b): int => $a->toString() <=> $b->toString())
×
402
                    ->toString()
×
403
                ;
×
404

405
                if (!\in_array($normalized, $seenNormalized, true)) {
×
406
                    $seenNormalized[] = $normalized;
×
407
                    $uniqueTypeExpressions[] = $innerType['expression'];
×
408
                }
409
            }
410

411
            $value = implode(
×
412
                $type->getTypesGlue(),
×
413
                array_map(static fn (self $expr): string => $expr->toString(), $uniqueTypeExpressions),
×
414
            );
×
415

416
            return $this->inner($value);
×
417
        });
×
418
    }
419

420
    public function getCommonType(): ?string
421
    {
422
        $mainType = null;
60✔
423

424
        foreach ($this->getTypes() as $type) {
60✔
425
            if ('null' === $type) {
60✔
426
                continue;
2✔
427
            }
428

429
            if (str_starts_with($type, '?')) {
60✔
430
                $type = substr($type, 1);
3✔
431
            }
432

433
            if (Preg::match('/\[\h*\]$/', $type)) {
60✔
434
                $type = 'array';
13✔
435
            } elseif (Preg::match('/^(.+?)\h*[<{(]/', $type, $matches)) {
47✔
436
                $type = $matches[1];
17✔
437
            }
438

439
            if (isset(self::ALIASES[$type])) {
60✔
440
                $type = self::ALIASES[$type];
4✔
441
            }
442

443
            if (null === $mainType || $type === $mainType) {
60✔
444
                $mainType = $type;
60✔
445

446
                continue;
60✔
447
            }
448

449
            $mainType = $this->getParentType($type, $mainType);
17✔
450

451
            if (null === $mainType) {
17✔
452
                return null;
6✔
453
            }
454
        }
455

456
        return $mainType;
54✔
457
    }
458

459
    public function allowsNull(): bool
460
    {
461
        foreach ($this->getTypes() as $type) {
10✔
462
            if (\in_array($type, ['null', 'mixed'], true) || str_starts_with($type, '?')) {
10✔
463
                return true;
7✔
464
            }
465
        }
466

467
        return false;
3✔
468
    }
469

470
    private function parse(): void
471
    {
472
        $seenGlues = null;
357✔
473
        $innerValues = [];
357✔
474

475
        $index = 0;
357✔
476
        while (true) {
357✔
477
            Preg::match(
357✔
478
                '{\G'.self::REGEX_TYPE.'(?<glue_raw>\h*(?<glue>[|&])\h*(?!$)|$)}',
357✔
479
                $this->value,
357✔
480
                $matches,
357✔
481
                \PREG_OFFSET_CAPTURE,
357✔
482
                $index,
357✔
483
            );
357✔
484

485
            if ([] === $matches) {
357✔
486
                throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
61✔
487
            }
488

489
            if (null === $seenGlues) {
298✔
490
                if (($matches['glue'][0] ?? '') === '') {
298✔
491
                    break;
296✔
492
                }
493

494
                $seenGlues = ['|' => false, '&' => false];
244✔
495
            }
496

497
            if (($matches['glue'][0] ?? '') !== '') {
244✔
498
                \assert(isset($seenGlues[$matches['glue'][0]]));
244✔
499
                $seenGlues[$matches['glue'][0]] = true;
244✔
500
            }
501

502
            $innerValues[] = [
244✔
503
                'start_index' => $index,
244✔
504
                'value' => $matches['type'][0],
244✔
505
                'next_glue' => $matches['glue'][0] ?? null,
244✔
506
                'next_glue_raw' => $matches['glue_raw'][0] ?? null,
244✔
507
            ];
244✔
508

509
            $consumedValueLength = \strlen($matches[0][0]);
244✔
510
            $index += $consumedValueLength;
244✔
511

512
            if (\strlen($this->value) <= $index) {
244✔
513
                \assert(\strlen($this->value) === $index);
242✔
514

515
                $seenGlues = array_filter($seenGlues);
242✔
516
                \assert([] !== $seenGlues);
242✔
517

518
                $this->isCompositeType = true;
242✔
519
                $this->typesGlue = array_key_first($seenGlues);
242✔
520

521
                if (1 === \count($seenGlues)) {
242✔
522
                    foreach ($innerValues as $innerValue) {
242✔
523
                        $this->innerTypeExpressions[] = [
242✔
524
                            'start_index' => $innerValue['start_index'],
242✔
525
                            'expression' => $this->inner($innerValue['value']),
242✔
526
                        ];
242✔
527
                    }
528
                } else {
529
                    for ($i = 0; $i < \count($innerValues); ++$i) {
4✔
530
                        $innerStartIndex = $innerValues[$i]['start_index'];
4✔
531
                        $innerValue = '';
4✔
532
                        while (true) {
4✔
533
                            $innerValue .= $innerValues[$i]['value'];
4✔
534

535
                            if (($innerValues[$i]['next_glue'] ?? $this->typesGlue) === $this->typesGlue) {
4✔
536
                                break;
4✔
537
                            }
538

539
                            $innerValue .= $innerValues[$i]['next_glue_raw'];
4✔
540

541
                            ++$i;
4✔
542
                            \assert(isset($innerValues[$i])); // for PHPStan
4✔
543
                        }
544

545
                        $this->innerTypeExpressions[] = [
4✔
546
                            'start_index' => $innerStartIndex,
4✔
547
                            'expression' => $this->inner($innerValue),
4✔
548
                        ];
4✔
549
                    }
550
                }
551

552
                return;
242✔
553
            }
554
        }
555

556
        $this->isCompositeType = false;
296✔
557

558
        if ('' !== $matches['nullable'][0]) {
296✔
559
            $this->innerTypeExpressions[] = [
17✔
560
                'start_index' => \strlen($matches['nullable'][0]),
17✔
561
                'expression' => $this->inner(substr($matches['type'][0], \strlen($matches['nullable'][0]))),
17✔
562
            ];
17✔
563
        } elseif ('' !== $matches['array'][0]) {
296✔
564
            $this->innerTypeExpressions[] = [
27✔
565
                'start_index' => 0,
27✔
566
                'expression' => $this->inner(substr($matches['type'][0], 0, -\strlen($matches['array'][0]))),
27✔
567
            ];
27✔
568
        } elseif ('' !== ($matches['generic'][0] ?? '') && 0 === $matches['generic'][1]) {
296✔
569
            $this->innerTypeExpressions[] = [
55✔
570
                'start_index' => 0,
55✔
571
                'expression' => $this->inner($matches['generic_name'][0]),
55✔
572
            ];
55✔
573

574
            $this->parseCommaSeparatedInnerTypes(
55✔
575
                \strlen($matches['generic_name'][0]) + \strlen($matches['generic_start'][0]),
55✔
576
                $matches['generic_types'][0],
55✔
577
            );
55✔
578
        } elseif ('' !== ($matches['callable'][0] ?? '') && 0 === $matches['callable'][1]) {
296✔
579
            $this->innerTypeExpressions[] = [
63✔
580
                'start_index' => 0,
63✔
581
                'expression' => $this->inner($matches['callable_name'][0]),
63✔
582
            ];
63✔
583

584
            $this->parseCallableTemplateInnerTypes(
63✔
585
                \strlen($matches['callable_name'][0])
63✔
586
                    + \strlen($matches['callable_template_start'][0]),
63✔
587
                $matches['callable_template_inners'][0],
63✔
588
            );
63✔
589

590
            $this->parseCallableArgumentTypes(
63✔
591
                \strlen($matches['callable_name'][0])
63✔
592
                    + \strlen($matches['callable_template'][0])
63✔
593
                    + \strlen($matches['callable_start'][0]),
63✔
594
                $matches['callable_arguments'][0],
63✔
595
            );
63✔
596

597
            if ('' !== ($matches['callable_return'][0] ?? '')) {
63✔
598
                $this->innerTypeExpressions[] = [
47✔
599
                    'start_index' => \strlen($this->value) - \strlen($matches['callable_return'][0]),
47✔
600
                    'expression' => $this->inner($matches['callable_return'][0]),
47✔
601
                ];
47✔
602
            }
603
        } elseif ('' !== ($matches['array_shape'][0] ?? '') && 0 === $matches['array_shape'][1]) {
296✔
604
            $this->innerTypeExpressions[] = [
47✔
605
                'start_index' => 0,
47✔
606
                'expression' => $this->inner($matches['array_shape_name'][0]),
47✔
607
            ];
47✔
608

609
            $nextIndex = \strlen($matches['array_shape_name'][0]) + \strlen($matches['array_shape_start'][0]);
47✔
610

611
            $this->parseArrayShapeInnerTypes(
47✔
612
                $nextIndex,
47✔
613
                $matches['array_shape_inners'][0],
47✔
614
            );
47✔
615

616
            if ('' !== ($matches['array_shape_unsealed_type'][0] ?? '')) {
47✔
617
                $nextIndex += \strlen($matches['array_shape_inners'][0])
8✔
618
                    + \strlen($matches['array_shape_unsealed_variadic'][0])
8✔
619
                    + \strlen($matches['array_shape_unsealed_type_start'][0]);
8✔
620

621
                $this->innerTypeExpressions[] = [
8✔
622
                    'start_index' => $nextIndex,
8✔
623
                    'expression' => $this->inner($matches['array_shape_unsealed_type_a'][0]),
8✔
624
                ];
8✔
625

626
                if ('' !== ($matches['array_shape_unsealed_type_b'][0] ?? '')) {
8✔
627
                    $nextIndex += \strlen($matches['array_shape_unsealed_type_a'][0])
2✔
628
                        + \strlen($matches['array_shape_unsealed_type_comma'][0]);
2✔
629

630
                    $this->innerTypeExpressions[] = [
2✔
631
                        'start_index' => $nextIndex,
2✔
632
                        'expression' => $this->inner($matches['array_shape_unsealed_type_b'][0]),
2✔
633
                    ];
2✔
634
                }
635
            }
636
        } elseif ('' !== ($matches['parenthesized'][0] ?? '') && 0 === $matches['parenthesized'][1]) {
296✔
637
            $index = \strlen($matches['parenthesized_start'][0]);
26✔
638

639
            if ('' !== ($matches['conditional'][0] ?? '')) {
26✔
640
                if ('' !== ($matches['conditional_cond_left_types'][0] ?? '')) {
7✔
641
                    $this->innerTypeExpressions[] = [
2✔
642
                        'start_index' => $index,
2✔
643
                        'expression' => $this->inner($matches['conditional_cond_left_types'][0]),
2✔
644
                    ];
2✔
645
                }
646

647
                $index += \strlen($matches['conditional_cond_left'][0]) + \strlen($matches['conditional_cond_middle'][0]);
7✔
648

649
                $this->innerTypeExpressions[] = [
7✔
650
                    'start_index' => $index,
7✔
651
                    'expression' => $this->inner($matches['conditional_cond_right_types'][0]),
7✔
652
                ];
7✔
653

654
                $index += \strlen($matches['conditional_cond_right_types'][0]) + \strlen($matches['conditional_true_start'][0]);
7✔
655

656
                $this->innerTypeExpressions[] = [
7✔
657
                    'start_index' => $index,
7✔
658
                    'expression' => $this->inner($matches['conditional_true_types'][0]),
7✔
659
                ];
7✔
660

661
                $index += \strlen($matches['conditional_true_types'][0]) + \strlen($matches['conditional_false_start'][0]);
7✔
662

663
                $this->innerTypeExpressions[] = [
7✔
664
                    'start_index' => $index,
7✔
665
                    'expression' => $this->inner($matches['conditional_false_types'][0]),
7✔
666
                ];
7✔
667
            } else {
668
                $this->innerTypeExpressions[] = [
24✔
669
                    'start_index' => $index,
24✔
670
                    'expression' => $this->inner($matches['parenthesized_types'][0]),
24✔
671
                ];
24✔
672
            }
673
        } elseif ('' !== $matches['class_constant'][0]) {
296✔
674
            $this->innerTypeExpressions[] = [
8✔
675
                'start_index' => 0,
8✔
676
                'expression' => $this->inner($matches['class_constant_name'][0]),
8✔
677
            ];
8✔
678
        }
679
    }
680

681
    private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
682
    {
683
        $index = 0;
55✔
684
        while (\strlen($value) !== $index) {
55✔
685
            Preg::match(
55✔
686
                '{\G'.self::REGEX_TYPES.'(?:\h*,[\h\v*]*|$)}',
55✔
687
                $value,
55✔
688
                $matches,
55✔
689
                0,
55✔
690
                $index,
55✔
691
            );
55✔
692

693
            $this->innerTypeExpressions[] = [
55✔
694
                'start_index' => $startIndex + $index,
55✔
695
                'expression' => $this->inner($matches['types']),
55✔
696
            ];
55✔
697

698
            $index += \strlen($matches[0]);
55✔
699
        }
700
    }
701

702
    private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
703
    {
704
        $index = 0;
63✔
705
        while (\strlen($value) !== $index) {
63✔
706
            Preg::match(
11✔
707
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_template_inner>(?&callable_template_inner))(?:\h*,\h*|$))}',
11✔
708
                $value,
11✔
709
                $prematches,
11✔
710
                0,
11✔
711
                $index,
11✔
712
            );
11✔
713
            $consumedValue = $prematches['_callable_template_inner'];
11✔
714
            $consumedValueLength = \strlen($consumedValue);
11✔
715
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
11✔
716

717
            $addedPrefix = 'Closure<';
11✔
718
            Preg::match(
11✔
719
                '{^'.self::REGEX_TYPES.'$}',
11✔
720
                $addedPrefix.$consumedValue.'>(): void',
11✔
721
                $matches,
11✔
722
                \PREG_OFFSET_CAPTURE,
11✔
723
            );
11✔
724

725
            if ('' !== $matches['callable_template_inner_b'][0]) {
11✔
726
                $this->innerTypeExpressions[] = [
5✔
727
                    'start_index' => $startIndex + $index + $matches['callable_template_inner_b_types'][1]
5✔
728
                        - \strlen($addedPrefix),
5✔
729
                    'expression' => $this->inner($matches['callable_template_inner_b_types'][0]),
5✔
730
                ];
5✔
731
            }
732

733
            if ('' !== $matches['callable_template_inner_d'][0]) {
11✔
734
                $this->innerTypeExpressions[] = [
4✔
735
                    'start_index' => $startIndex + $index + $matches['callable_template_inner_d_types'][1]
4✔
736
                        - \strlen($addedPrefix),
4✔
737
                    'expression' => $this->inner($matches['callable_template_inner_d_types'][0]),
4✔
738
                ];
4✔
739
            }
740

741
            $index += $consumedValueLength + $consumedCommaLength;
11✔
742
        }
743
    }
744

745
    private function parseCallableArgumentTypes(int $startIndex, string $value): void
746
    {
747
        $index = 0;
63✔
748
        while (\strlen($value) !== $index) {
63✔
749
            Preg::match(
48✔
750
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_argument>(?&callable_argument))(?:\h*,\h*|$))}',
48✔
751
                $value,
48✔
752
                $prematches,
48✔
753
                0,
48✔
754
                $index,
48✔
755
            );
48✔
756
            $consumedValue = $prematches['_callable_argument'];
48✔
757
            $consumedValueLength = \strlen($consumedValue);
48✔
758
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
48✔
759

760
            $addedPrefix = 'Closure(';
48✔
761
            Preg::match(
48✔
762
                '{^'.self::REGEX_TYPES.'$}',
48✔
763
                $addedPrefix.$consumedValue.'): void',
48✔
764
                $matches,
48✔
765
                \PREG_OFFSET_CAPTURE,
48✔
766
            );
48✔
767

768
            $this->innerTypeExpressions[] = [
48✔
769
                'start_index' => $startIndex + $index,
48✔
770
                'expression' => $this->inner($matches['callable_argument_type'][0]),
48✔
771
            ];
48✔
772

773
            $index += $consumedValueLength + $consumedCommaLength;
48✔
774
        }
775
    }
776

777
    private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
778
    {
779
        $index = 0;
47✔
780
        while (\strlen($value) !== $index) {
47✔
781
            Preg::match(
42✔
782
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_array_shape_inner>(?&array_shape_inner))(?:\h*,[\h\v*]*|$))}',
42✔
783
                $value,
42✔
784
                $prematches,
42✔
785
                0,
42✔
786
                $index,
42✔
787
            );
42✔
788
            $consumedValue = $prematches['_array_shape_inner'];
42✔
789
            $consumedValueLength = \strlen($consumedValue);
42✔
790
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
42✔
791

792
            $addedPrefix = 'array{';
42✔
793
            Preg::match(
42✔
794
                '{^'.self::REGEX_TYPES.'$}',
42✔
795
                $addedPrefix.$consumedValue.'}',
42✔
796
                $matches,
42✔
797
                \PREG_OFFSET_CAPTURE,
42✔
798
            );
42✔
799

800
            $this->innerTypeExpressions[] = [
42✔
801
                'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1]
42✔
802
                    - \strlen($addedPrefix),
42✔
803
                'expression' => $this->inner($matches['array_shape_inner_value'][0]),
42✔
804
            ];
42✔
805

806
            $index += $consumedValueLength + $consumedCommaLength;
42✔
807
        }
808
    }
809

810
    private function inner(string $value): self
811
    {
812
        return new self($value, $this->namespace, $this->namespaceUses);
274✔
813
    }
814

815
    private function getParentType(string $type1, string $type2): ?string
816
    {
817
        $types = [
17✔
818
            $this->normalize($type1),
17✔
819
            $this->normalize($type2),
17✔
820
        ];
17✔
821
        natcasesort($types);
17✔
822
        $types = implode('|', $types);
17✔
823

824
        $parents = [
17✔
825
            'array|Traversable' => 'iterable',
17✔
826
            'array|iterable' => 'iterable',
17✔
827
            'iterable|Traversable' => 'iterable',
17✔
828
            'self|static' => 'self',
17✔
829
        ];
17✔
830

831
        return $parents[$types] ?? null;
17✔
832
    }
833

834
    private function normalize(string $type): string
835
    {
836
        if (isset(self::ALIASES[$type])) {
17✔
837
            return self::ALIASES[$type];
×
838
        }
839

840
        if (\in_array($type, [
17✔
841
            'array',
17✔
842
            'bool',
17✔
843
            'callable',
17✔
844
            'false',
17✔
845
            'float',
17✔
846
            'int',
17✔
847
            'iterable',
17✔
848
            'mixed',
17✔
849
            'never',
17✔
850
            'null',
17✔
851
            'object',
17✔
852
            'resource',
17✔
853
            'string',
17✔
854
            'true',
17✔
855
            'void',
17✔
856
        ], true)) {
17✔
857
            return $type;
16✔
858
        }
859

860
        if (Preg::match('/\[\]$/', $type)) {
12✔
861
            return 'array';
×
862
        }
863

864
        if (Preg::match('/^(.+?)</', $type, $matches)) {
12✔
865
            return $matches[1];
×
866
        }
867

868
        if (str_starts_with($type, '\\')) {
12✔
869
            return substr($type, 1);
1✔
870
        }
871

872
        foreach ($this->namespaceUses as $namespaceUse) {
11✔
873
            if ($namespaceUse->getShortName() === $type) {
6✔
874
                return $namespaceUse->getFullName();
6✔
875
            }
876
        }
877

878
        if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
5✔
879
            return $type;
5✔
880
        }
881

882
        return "{$this->namespace->getFullName()}\\{$type}";
×
883
    }
884
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc