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

keradus / PHP-CS-Fixer / 17252691116

26 Aug 2025 11:09PM UTC coverage: 94.743% (-0.01%) from 94.755%
17252691116

push

github

keradus
chore: apply phpdoc_tag_no_named_arguments

28313 of 29884 relevant lines covered (94.74%)

45.64 hits per line

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

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

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

237
    private string $value;
238

239
    private bool $isCompositeType;
240

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

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

247
    private ?NamespaceAnalysis $namespace;
248

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

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

261
        $this->parse();
353✔
262
    }
263

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

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

281
        return [$this->value];
176✔
282
    }
283

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

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

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

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

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

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

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

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

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

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

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

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

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

359
    /**
360
     * @param \Closure(self, self): (-1|0|1) $compareCallback
361
     */
362
    public function sortTypes(\Closure $compareCallback): self
363
    {
364
        return $this->mapTypes(function (self $type) use ($compareCallback): self {
52✔
365
            if ($type->isCompositeType) {
52✔
366
                $innerTypeExpressions = Utils::stableSort(
51✔
367
                    $type->innerTypeExpressions,
51✔
368
                    static fn (array $v): self => $v['expression'],
51✔
369
                    $compareCallback,
51✔
370
                );
51✔
371

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

378
                    return $this->inner($value);
51✔
379
                }
380
            }
381

382
            return $type;
52✔
383
        });
52✔
384
    }
385

386
    public function getCommonType(): ?string
387
    {
388
        $mainType = null;
60✔
389

390
        foreach ($this->getTypes() as $type) {
60✔
391
            if ('null' === $type) {
60✔
392
                continue;
2✔
393
            }
394

395
            if (str_starts_with($type, '?')) {
60✔
396
                $type = substr($type, 1);
3✔
397
            }
398

399
            if (Preg::match('/\[\h*\]$/', $type)) {
60✔
400
                $type = 'array';
13✔
401
            } elseif (Preg::match('/^(.+?)\h*[<{(]/', $type, $matches)) {
47✔
402
                $type = $matches[1];
17✔
403
            }
404

405
            if (isset(self::ALIASES[$type])) {
60✔
406
                $type = self::ALIASES[$type];
4✔
407
            }
408

409
            if (null === $mainType || $type === $mainType) {
60✔
410
                $mainType = $type;
60✔
411

412
                continue;
60✔
413
            }
414

415
            $mainType = $this->getParentType($type, $mainType);
17✔
416

417
            if (null === $mainType) {
17✔
418
                return null;
6✔
419
            }
420
        }
421

422
        return $mainType;
54✔
423
    }
424

425
    public function allowsNull(): bool
426
    {
427
        foreach ($this->getTypes() as $type) {
10✔
428
            if (\in_array($type, ['null', 'mixed'], true) || str_starts_with($type, '?')) {
10✔
429
                return true;
7✔
430
            }
431
        }
432

433
        return false;
3✔
434
    }
435

436
    private function parse(): void
437
    {
438
        $seenGlues = null;
353✔
439
        $innerValues = [];
353✔
440

441
        $index = 0;
353✔
442
        while (true) {
353✔
443
            Preg::match(
353✔
444
                '{\G'.self::REGEX_TYPE.'(?<glue_raw>\h*(?<glue>[|&])\h*(?!$)|$)}',
353✔
445
                $this->value,
353✔
446
                $matches,
353✔
447
                \PREG_OFFSET_CAPTURE,
353✔
448
                $index
353✔
449
            );
353✔
450

451
            if ([] === $matches) {
353✔
452
                throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
61✔
453
            }
454

455
            if (null === $seenGlues) {
294✔
456
                if (($matches['glue'][0] ?? '') === '') {
294✔
457
                    break;
292✔
458
                }
459

460
                $seenGlues = ['|' => false, '&' => false];
240✔
461
            }
462

463
            if (($matches['glue'][0] ?? '') !== '') {
240✔
464
                \assert(isset($seenGlues[$matches['glue'][0]]));
240✔
465
                $seenGlues[$matches['glue'][0]] = true;
240✔
466
            }
467

468
            $innerValues[] = [
240✔
469
                'start_index' => $index,
240✔
470
                'value' => $matches['type'][0],
240✔
471
                'next_glue' => $matches['glue'][0] ?? null,
240✔
472
                'next_glue_raw' => $matches['glue_raw'][0] ?? null,
240✔
473
            ];
240✔
474

475
            $consumedValueLength = \strlen($matches[0][0]);
240✔
476
            $index += $consumedValueLength;
240✔
477

478
            if (\strlen($this->value) <= $index) {
240✔
479
                \assert(\strlen($this->value) === $index);
238✔
480

481
                $seenGlues = array_filter($seenGlues);
238✔
482
                \assert([] !== $seenGlues);
238✔
483

484
                $this->isCompositeType = true;
238✔
485
                $this->typesGlue = array_key_first($seenGlues);
238✔
486

487
                if (1 === \count($seenGlues)) {
238✔
488
                    foreach ($innerValues as $innerValue) {
238✔
489
                        $this->innerTypeExpressions[] = [
238✔
490
                            'start_index' => $innerValue['start_index'],
238✔
491
                            'expression' => $this->inner($innerValue['value']),
238✔
492
                        ];
238✔
493
                    }
494
                } else {
495
                    for ($i = 0; $i < \count($innerValues); ++$i) {
4✔
496
                        $innerStartIndex = $innerValues[$i]['start_index'];
4✔
497
                        $innerValue = '';
4✔
498
                        while (true) {
4✔
499
                            $innerValue .= $innerValues[$i]['value'];
4✔
500

501
                            if (($innerValues[$i]['next_glue'] ?? $this->typesGlue) === $this->typesGlue) {
4✔
502
                                break;
4✔
503
                            }
504

505
                            $innerValue .= $innerValues[$i]['next_glue_raw'];
4✔
506

507
                            ++$i;
4✔
508
                        }
509

510
                        $this->innerTypeExpressions[] = [
4✔
511
                            'start_index' => $innerStartIndex,
4✔
512
                            'expression' => $this->inner($innerValue),
4✔
513
                        ];
4✔
514
                    }
515
                }
516

517
                return;
238✔
518
            }
519
        }
520

521
        $this->isCompositeType = false;
292✔
522

523
        if ('' !== $matches['nullable'][0]) {
292✔
524
            $this->innerTypeExpressions[] = [
17✔
525
                'start_index' => \strlen($matches['nullable'][0]),
17✔
526
                'expression' => $this->inner(substr($matches['type'][0], \strlen($matches['nullable'][0]))),
17✔
527
            ];
17✔
528
        } elseif ('' !== $matches['array'][0]) {
292✔
529
            $this->innerTypeExpressions[] = [
27✔
530
                'start_index' => 0,
27✔
531
                'expression' => $this->inner(substr($matches['type'][0], 0, -\strlen($matches['array'][0]))),
27✔
532
            ];
27✔
533
        } elseif ('' !== ($matches['generic'][0] ?? '') && 0 === $matches['generic'][1]) {
292✔
534
            $this->innerTypeExpressions[] = [
51✔
535
                'start_index' => 0,
51✔
536
                'expression' => $this->inner($matches['generic_name'][0]),
51✔
537
            ];
51✔
538

539
            $this->parseCommaSeparatedInnerTypes(
51✔
540
                \strlen($matches['generic_name'][0]) + \strlen($matches['generic_start'][0]),
51✔
541
                $matches['generic_types'][0]
51✔
542
            );
51✔
543
        } elseif ('' !== ($matches['callable'][0] ?? '') && 0 === $matches['callable'][1]) {
292✔
544
            $this->innerTypeExpressions[] = [
63✔
545
                'start_index' => 0,
63✔
546
                'expression' => $this->inner($matches['callable_name'][0]),
63✔
547
            ];
63✔
548

549
            $this->parseCallableTemplateInnerTypes(
63✔
550
                \strlen($matches['callable_name'][0])
63✔
551
                    + \strlen($matches['callable_template_start'][0]),
63✔
552
                $matches['callable_template_inners'][0]
63✔
553
            );
63✔
554

555
            $this->parseCallableArgumentTypes(
63✔
556
                \strlen($matches['callable_name'][0])
63✔
557
                    + \strlen($matches['callable_template'][0])
63✔
558
                    + \strlen($matches['callable_start'][0]),
63✔
559
                $matches['callable_arguments'][0]
63✔
560
            );
63✔
561

562
            if ('' !== ($matches['callable_return'][0] ?? '')) {
63✔
563
                $this->innerTypeExpressions[] = [
47✔
564
                    'start_index' => \strlen($this->value) - \strlen($matches['callable_return'][0]),
47✔
565
                    'expression' => $this->inner($matches['callable_return'][0]),
47✔
566
                ];
47✔
567
            }
568
        } elseif ('' !== ($matches['array_shape'][0] ?? '') && 0 === $matches['array_shape'][1]) {
292✔
569
            $this->innerTypeExpressions[] = [
47✔
570
                'start_index' => 0,
47✔
571
                'expression' => $this->inner($matches['array_shape_name'][0]),
47✔
572
            ];
47✔
573

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

576
            $this->parseArrayShapeInnerTypes(
47✔
577
                $nextIndex,
47✔
578
                $matches['array_shape_inners'][0]
47✔
579
            );
47✔
580

581
            if ('' !== ($matches['array_shape_unsealed_type'][0] ?? '')) {
47✔
582
                $nextIndex += \strlen($matches['array_shape_inners'][0])
8✔
583
                    + \strlen($matches['array_shape_unsealed_variadic'][0])
8✔
584
                    + \strlen($matches['array_shape_unsealed_type_start'][0]);
8✔
585

586
                $this->innerTypeExpressions[] = [
8✔
587
                    'start_index' => $nextIndex,
8✔
588
                    'expression' => $this->inner($matches['array_shape_unsealed_type_a'][0]),
8✔
589
                ];
8✔
590

591
                if ('' !== ($matches['array_shape_unsealed_type_b'][0] ?? '')) {
8✔
592
                    $nextIndex += \strlen($matches['array_shape_unsealed_type_a'][0])
2✔
593
                        + \strlen($matches['array_shape_unsealed_type_comma'][0]);
2✔
594

595
                    $this->innerTypeExpressions[] = [
2✔
596
                        'start_index' => $nextIndex,
2✔
597
                        'expression' => $this->inner($matches['array_shape_unsealed_type_b'][0]),
2✔
598
                    ];
2✔
599
                }
600
            }
601
        } elseif ('' !== ($matches['parenthesized'][0] ?? '') && 0 === $matches['parenthesized'][1]) {
292✔
602
            $index = \strlen($matches['parenthesized_start'][0]);
26✔
603

604
            if ('' !== ($matches['conditional'][0] ?? '')) {
26✔
605
                if ('' !== ($matches['conditional_cond_left_types'][0] ?? '')) {
7✔
606
                    $this->innerTypeExpressions[] = [
2✔
607
                        'start_index' => $index,
2✔
608
                        'expression' => $this->inner($matches['conditional_cond_left_types'][0]),
2✔
609
                    ];
2✔
610
                }
611

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

614
                $this->innerTypeExpressions[] = [
7✔
615
                    'start_index' => $index,
7✔
616
                    'expression' => $this->inner($matches['conditional_cond_right_types'][0]),
7✔
617
                ];
7✔
618

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

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

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

628
                $this->innerTypeExpressions[] = [
7✔
629
                    'start_index' => $index,
7✔
630
                    'expression' => $this->inner($matches['conditional_false_types'][0]),
7✔
631
                ];
7✔
632
            } else {
633
                $this->innerTypeExpressions[] = [
24✔
634
                    'start_index' => $index,
24✔
635
                    'expression' => $this->inner($matches['parenthesized_types'][0]),
24✔
636
                ];
24✔
637
            }
638
        } elseif ('' !== $matches['class_constant'][0]) {
292✔
639
            $this->innerTypeExpressions[] = [
8✔
640
                'start_index' => 0,
8✔
641
                'expression' => $this->inner($matches['class_constant_name'][0]),
8✔
642
            ];
8✔
643
        }
644
    }
645

646
    private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
647
    {
648
        $index = 0;
51✔
649
        while (\strlen($value) !== $index) {
51✔
650
            Preg::match(
51✔
651
                '{\G'.self::REGEX_TYPES.'(?:\h*,[\h\v*]*|$)}',
51✔
652
                $value,
51✔
653
                $matches,
51✔
654
                0,
51✔
655
                $index
51✔
656
            );
51✔
657

658
            $this->innerTypeExpressions[] = [
51✔
659
                'start_index' => $startIndex + $index,
51✔
660
                'expression' => $this->inner($matches['types']),
51✔
661
            ];
51✔
662

663
            $index += \strlen($matches[0]);
51✔
664
        }
665
    }
666

667
    private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
668
    {
669
        $index = 0;
63✔
670
        while (\strlen($value) !== $index) {
63✔
671
            Preg::match(
11✔
672
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_template_inner>(?&callable_template_inner))(?:\h*,\h*|$))}',
11✔
673
                $value,
11✔
674
                $prematches,
11✔
675
                0,
11✔
676
                $index
11✔
677
            );
11✔
678
            $consumedValue = $prematches['_callable_template_inner'];
11✔
679
            $consumedValueLength = \strlen($consumedValue);
11✔
680
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
11✔
681

682
            $addedPrefix = 'Closure<';
11✔
683
            Preg::match(
11✔
684
                '{^'.self::REGEX_TYPES.'$}',
11✔
685
                $addedPrefix.$consumedValue.'>(): void',
11✔
686
                $matches,
11✔
687
                \PREG_OFFSET_CAPTURE
11✔
688
            );
11✔
689

690
            if ('' !== $matches['callable_template_inner_b'][0]) {
11✔
691
                $this->innerTypeExpressions[] = [
5✔
692
                    'start_index' => $startIndex + $index + $matches['callable_template_inner_b_types'][1]
5✔
693
                        - \strlen($addedPrefix),
5✔
694
                    'expression' => $this->inner($matches['callable_template_inner_b_types'][0]),
5✔
695
                ];
5✔
696
            }
697

698
            if ('' !== $matches['callable_template_inner_d'][0]) {
11✔
699
                $this->innerTypeExpressions[] = [
4✔
700
                    'start_index' => $startIndex + $index + $matches['callable_template_inner_d_types'][1]
4✔
701
                        - \strlen($addedPrefix),
4✔
702
                    'expression' => $this->inner($matches['callable_template_inner_d_types'][0]),
4✔
703
                ];
4✔
704
            }
705

706
            $index += $consumedValueLength + $consumedCommaLength;
11✔
707
        }
708
    }
709

710
    private function parseCallableArgumentTypes(int $startIndex, string $value): void
711
    {
712
        $index = 0;
63✔
713
        while (\strlen($value) !== $index) {
63✔
714
            Preg::match(
48✔
715
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_argument>(?&callable_argument))(?:\h*,\h*|$))}',
48✔
716
                $value,
48✔
717
                $prematches,
48✔
718
                0,
48✔
719
                $index
48✔
720
            );
48✔
721
            $consumedValue = $prematches['_callable_argument'];
48✔
722
            $consumedValueLength = \strlen($consumedValue);
48✔
723
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
48✔
724

725
            $addedPrefix = 'Closure(';
48✔
726
            Preg::match(
48✔
727
                '{^'.self::REGEX_TYPES.'$}',
48✔
728
                $addedPrefix.$consumedValue.'): void',
48✔
729
                $matches,
48✔
730
                \PREG_OFFSET_CAPTURE
48✔
731
            );
48✔
732

733
            $this->innerTypeExpressions[] = [
48✔
734
                'start_index' => $startIndex + $index,
48✔
735
                'expression' => $this->inner($matches['callable_argument_type'][0]),
48✔
736
            ];
48✔
737

738
            $index += $consumedValueLength + $consumedCommaLength;
48✔
739
        }
740
    }
741

742
    private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
743
    {
744
        $index = 0;
47✔
745
        while (\strlen($value) !== $index) {
47✔
746
            Preg::match(
42✔
747
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_array_shape_inner>(?&array_shape_inner))(?:\h*,[\h\v*]*|$))}',
42✔
748
                $value,
42✔
749
                $prematches,
42✔
750
                0,
42✔
751
                $index
42✔
752
            );
42✔
753
            $consumedValue = $prematches['_array_shape_inner'];
42✔
754
            $consumedValueLength = \strlen($consumedValue);
42✔
755
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
42✔
756

757
            $addedPrefix = 'array{';
42✔
758
            Preg::match(
42✔
759
                '{^'.self::REGEX_TYPES.'$}',
42✔
760
                $addedPrefix.$consumedValue.'}',
42✔
761
                $matches,
42✔
762
                \PREG_OFFSET_CAPTURE
42✔
763
            );
42✔
764

765
            $this->innerTypeExpressions[] = [
42✔
766
                'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1]
42✔
767
                    - \strlen($addedPrefix),
42✔
768
                'expression' => $this->inner($matches['array_shape_inner_value'][0]),
42✔
769
            ];
42✔
770

771
            $index += $consumedValueLength + $consumedCommaLength;
42✔
772
        }
773
    }
774

775
    private function inner(string $value): self
776
    {
777
        return new self($value, $this->namespace, $this->namespaceUses);
270✔
778
    }
779

780
    private function getParentType(string $type1, string $type2): ?string
781
    {
782
        $types = [
17✔
783
            $this->normalize($type1),
17✔
784
            $this->normalize($type2),
17✔
785
        ];
17✔
786
        natcasesort($types);
17✔
787
        $types = implode('|', $types);
17✔
788

789
        $parents = [
17✔
790
            'array|Traversable' => 'iterable',
17✔
791
            'array|iterable' => 'iterable',
17✔
792
            'iterable|Traversable' => 'iterable',
17✔
793
            'self|static' => 'self',
17✔
794
        ];
17✔
795

796
        return $parents[$types] ?? null;
17✔
797
    }
798

799
    private function normalize(string $type): string
800
    {
801
        if (isset(self::ALIASES[$type])) {
17✔
802
            return self::ALIASES[$type];
×
803
        }
804

805
        if (\in_array($type, [
17✔
806
            'array',
17✔
807
            'bool',
17✔
808
            'callable',
17✔
809
            'false',
17✔
810
            'float',
17✔
811
            'int',
17✔
812
            'iterable',
17✔
813
            'mixed',
17✔
814
            'never',
17✔
815
            'null',
17✔
816
            'object',
17✔
817
            'resource',
17✔
818
            'string',
17✔
819
            'true',
17✔
820
            'void',
17✔
821
        ], true)) {
17✔
822
            return $type;
16✔
823
        }
824

825
        if (Preg::match('/\[\]$/', $type)) {
12✔
826
            return 'array';
×
827
        }
828

829
        if (Preg::match('/^(.+?)</', $type, $matches)) {
12✔
830
            return $matches[1];
×
831
        }
832

833
        if (str_starts_with($type, '\\')) {
12✔
834
            return substr($type, 1);
1✔
835
        }
836

837
        foreach ($this->namespaceUses as $namespaceUse) {
11✔
838
            if ($namespaceUse->getShortName() === $type) {
6✔
839
                return $namespaceUse->getFullName();
6✔
840
            }
841
        }
842

843
        if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
5✔
844
            return $type;
5✔
845
        }
846

847
        return "{$this->namespace->getFullName()}\\{$type}";
×
848
    }
849
}
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