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

keradus / PHP-CS-Fixer / 17194174105

24 Aug 2025 09:46PM UTC coverage: 94.742% (-0.01%) from 94.752%
17194174105

push

github

keradus
Merge branch '__sets_update' of github.com:keradus/PHP-CS-Fixer into __sets_update

8 of 9 new or added lines in 4 files covered. (88.89%)

7 existing lines in 3 files now uncovered.

28307 of 29878 relevant lines covered (94.74%)

45.65 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
final class TypeExpression
28
{
29
    /**
30
     * Regex to match any PHP identifier.
31
     *
32
     * @internal
33
     */
34
    public const REGEX_IDENTIFIER = '(?:(?!(?<!\*)\d)[^\x00-\x2f\x3a-\x40\x5b-\x5e\x60\x7b-\x7f]++)';
35

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

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

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

235
    private string $value;
236

237
    private bool $isCompositeType;
238

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

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

245
    private ?NamespaceAnalysis $namespace;
246

247
    /** @var list<NamespaceUseAnalysis> */
248
    private array $namespaceUses;
249

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

259
        $this->parse();
353✔
260
    }
261

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

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

279
        return [$this->value];
176✔
280
    }
281

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

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

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

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

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

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

322
            $inner = $inner->mapTypes($callback);
53✔
323

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

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

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

340
        return $callback($type);
54✔
341
    }
342

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

353
            return $type;
1✔
354
        });
1✔
355
    }
356

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

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

376
                    return $this->inner($value);
51✔
377
                }
378
            }
379

380
            return $type;
52✔
381
        });
52✔
382
    }
383

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

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

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

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

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

407
            if (null === $mainType || $type === $mainType) {
60✔
408
                $mainType = $type;
60✔
409

410
                continue;
60✔
411
            }
412

413
            $mainType = $this->getParentType($type, $mainType);
17✔
414

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

420
        return $mainType;
54✔
421
    }
422

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

431
        return false;
3✔
432
    }
433

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

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

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

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

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

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

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

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

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

479
                $seenGlues = array_filter($seenGlues);
238✔
480
                \assert([] !== $seenGlues);
238✔
481

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

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

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

503
                            $innerValue .= $innerValues[$i]['next_glue_raw'];
4✔
504

505
                            ++$i;
4✔
506
                        }
507

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

515
                return;
238✔
516
            }
517
        }
518

519
        $this->isCompositeType = false;
292✔
520

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

704
            $index += $consumedValueLength + $consumedCommaLength;
11✔
705
        }
706
    }
707

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

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

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

736
            $index += $consumedValueLength + $consumedCommaLength;
48✔
737
        }
738
    }
739

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

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

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

769
            $index += $consumedValueLength + $consumedCommaLength;
42✔
770
        }
771
    }
772

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

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

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

794
        return $parents[$types] ?? null;
17✔
795
    }
796

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

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

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

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

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

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

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

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