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

keradus / PHP-CS-Fixer / 13614616960

01 Mar 2025 10:06PM UTC coverage: 94.929% (-0.03%) from 94.959%
13614616960

push

github

keradus
bumped version

28060 of 29559 relevant lines covered (94.93%)

43.05 hits per line

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

98.96
/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*)
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*
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*\}
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*)
137
                    (?<generic_types>
138
                        (?&types_inner)
139
                        (?:
140
                            \h*,\h*
141
                            (?&types_inner)
142
                        )*+
143
                        (?:\h*,)?
144
                    )
145
                    \h*>
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 string $value;
225

226
    private bool $isCompositeType;
227

228
    /** @var null|'&'|'|' */
229
    private ?string $typesGlue = null;
230

231
    /** @var list<array{start_index: int, expression: self}> */
232
    private array $innerTypeExpressions = [];
233

234
    private ?NamespaceAnalysis $namespace;
235

236
    /** @var list<NamespaceUseAnalysis> */
237
    private array $namespaceUses;
238

239
    /**
240
     * @param list<NamespaceUseAnalysis> $namespaceUses
241
     */
242
    public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
243
    {
244
        $this->value = $value;
351✔
245
        $this->namespace = $namespace;
351✔
246
        $this->namespaceUses = $namespaceUses;
351✔
247

248
        $this->parse();
351✔
249
    }
250

251
    public function toString(): string
252
    {
253
        return $this->value;
222✔
254
    }
255

256
    /**
257
     * @return list<string>
258
     */
259
    public function getTypes(): array
260
    {
261
        if ($this->isCompositeType) {
215✔
262
            return array_map(
168✔
263
                static fn (array $type) => $type['expression']->toString(),
168✔
264
                $this->innerTypeExpressions,
168✔
265
            );
168✔
266
        }
267

268
        return [$this->value];
174✔
269
    }
270

271
    /**
272
     * Determines if type expression is a composite type (union or intersection).
273
     */
274
    public function isCompositeType(): bool
275
    {
276
        return $this->isCompositeType;
149✔
277
    }
278

279
    public function isUnionType(): bool
280
    {
281
        return $this->isCompositeType && '|' === $this->typesGlue;
25✔
282
    }
283

284
    public function isIntersectionType(): bool
285
    {
286
        return $this->isCompositeType && '&' === $this->typesGlue;
5✔
287
    }
288

289
    /**
290
     * @return null|'&'|'|'
291
     */
292
    public function getTypesGlue(): ?string
293
    {
294
        return $this->typesGlue;
54✔
295
    }
296

297
    /**
298
     * @param \Closure(self): self $callback
299
     */
300
    public function mapTypes(\Closure $callback): self
301
    {
302
        $value = $this->value;
54✔
303
        $startIndexOffset = 0;
54✔
304

305
        foreach ($this->innerTypeExpressions as [
54✔
306
            'start_index' => $startIndexOrig,
54✔
307
            'expression' => $inner,
54✔
308
        ]) {
54✔
309
            $innerValueOrig = $inner->value;
53✔
310

311
            $inner = $inner->mapTypes($callback);
53✔
312

313
            if ($inner->value !== $innerValueOrig) {
53✔
314
                $value = substr_replace(
48✔
315
                    $value,
48✔
316
                    $inner->value,
48✔
317
                    $startIndexOrig + $startIndexOffset,
48✔
318
                    \strlen($innerValueOrig)
48✔
319
                );
48✔
320

321
                $startIndexOffset += \strlen($inner->value) - \strlen($innerValueOrig);
48✔
322
            }
323
        }
324

325
        $type = $value === $this->value
54✔
326
            ? $this
54✔
327
            : $this->inner($value);
48✔
328

329
        return $callback($type);
54✔
330
    }
331

332
    /**
333
     * @param \Closure(self): void $callback
334
     */
335
    public function walkTypes(\Closure $callback): void
336
    {
337
        $this->mapTypes(static function (self $type) use ($callback) {
1✔
338
            $valueOrig = $type->value;
1✔
339
            $callback($type);
1✔
340
            \assert($type->value === $valueOrig);
1✔
341

342
            return $type;
1✔
343
        });
1✔
344
    }
345

346
    /**
347
     * @param \Closure(self, self): (-1|0|1) $compareCallback
348
     */
349
    public function sortTypes(\Closure $compareCallback): self
350
    {
351
        return $this->mapTypes(function (self $type) use ($compareCallback): self {
52✔
352
            if ($type->isCompositeType) {
52✔
353
                $innerTypeExpressions = Utils::stableSort(
51✔
354
                    $type->innerTypeExpressions,
51✔
355
                    static fn (array $v): self => $v['expression'],
51✔
356
                    $compareCallback,
51✔
357
                );
51✔
358

359
                if ($innerTypeExpressions !== $type->innerTypeExpressions) {
51✔
360
                    $value = implode(
51✔
361
                        $type->getTypesGlue(),
51✔
362
                        array_map(static fn (array $v): string => $v['expression']->toString(), $innerTypeExpressions)
51✔
363
                    );
51✔
364

365
                    return $this->inner($value);
51✔
366
                }
367
            }
368

369
            return $type;
52✔
370
        });
52✔
371
    }
372

373
    public function getCommonType(): ?string
374
    {
375
        $aliases = $this->getAliases();
60✔
376

377
        $mainType = null;
60✔
378

379
        foreach ($this->getTypes() as $type) {
60✔
380
            if ('null' === $type) {
60✔
381
                continue;
2✔
382
            }
383

384
            if (str_starts_with($type, '?')) {
60✔
385
                $type = substr($type, 1);
3✔
386
            }
387

388
            if (Preg::match('/\[\h*\]$/', $type)) {
60✔
389
                $type = 'array';
13✔
390
            } elseif (Preg::match('/^(.+?)\h*[<{(]/', $type, $matches)) {
47✔
391
                $type = $matches[1];
17✔
392
            }
393

394
            if (isset($aliases[$type])) {
60✔
395
                $type = $aliases[$type];
4✔
396
            }
397

398
            if (null === $mainType || $type === $mainType) {
60✔
399
                $mainType = $type;
60✔
400

401
                continue;
60✔
402
            }
403

404
            $mainType = $this->getParentType($type, $mainType);
17✔
405

406
            if (null === $mainType) {
17✔
407
                return null;
6✔
408
            }
409
        }
410

411
        return $mainType;
54✔
412
    }
413

414
    public function allowsNull(): bool
415
    {
416
        foreach ($this->getTypes() as $type) {
10✔
417
            if (\in_array($type, ['null', 'mixed'], true) || str_starts_with($type, '?')) {
10✔
418
                return true;
7✔
419
            }
420
        }
421

422
        return false;
3✔
423
    }
424

425
    private function parse(): void
426
    {
427
        $seenGlues = null;
351✔
428
        $innerValues = [];
351✔
429

430
        $index = 0;
351✔
431
        while (true) {
351✔
432
            Preg::match(
351✔
433
                '{\G'.self::REGEX_TYPE.'(?<glue_raw>\h*(?<glue>[|&])\h*(?!$)|$)}',
351✔
434
                $this->value,
351✔
435
                $matches,
351✔
436
                PREG_OFFSET_CAPTURE,
351✔
437
                $index
351✔
438
            );
351✔
439

440
            if ([] === $matches) {
351✔
441
                throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
61✔
442
            }
443

444
            if (null === $seenGlues) {
292✔
445
                if (($matches['glue'][0] ?? '') === '') {
292✔
446
                    break;
290✔
447
                }
448

449
                $seenGlues = ['|' => false, '&' => false];
238✔
450
            }
451

452
            if (($matches['glue'][0] ?? '') !== '') {
238✔
453
                \assert(isset($seenGlues[$matches['glue'][0]]));
238✔
454
                $seenGlues[$matches['glue'][0]] = true;
238✔
455
            }
456

457
            $innerValues[] = [
238✔
458
                'start_index' => $index,
238✔
459
                'value' => $matches['type'][0],
238✔
460
                'next_glue' => $matches['glue'][0] ?? null,
238✔
461
                'next_glue_raw' => $matches['glue_raw'][0] ?? null,
238✔
462
            ];
238✔
463

464
            $consumedValueLength = \strlen($matches[0][0]);
238✔
465
            $index += $consumedValueLength;
238✔
466

467
            if (\strlen($this->value) <= $index) {
238✔
468
                \assert(\strlen($this->value) === $index);
236✔
469

470
                $seenGlues = array_filter($seenGlues);
236✔
471
                \assert([] !== $seenGlues);
236✔
472

473
                $this->isCompositeType = true;
236✔
474
                $this->typesGlue = array_key_first($seenGlues);
236✔
475

476
                if (1 === \count($seenGlues)) {
236✔
477
                    foreach ($innerValues as $innerValue) {
236✔
478
                        $this->innerTypeExpressions[] = [
236✔
479
                            'start_index' => $innerValue['start_index'],
236✔
480
                            'expression' => $this->inner($innerValue['value']),
236✔
481
                        ];
236✔
482
                    }
483
                } else {
484
                    for ($i = 0; $i < \count($innerValues); ++$i) {
4✔
485
                        $innerStartIndex = $innerValues[$i]['start_index'];
4✔
486
                        $innerValue = '';
4✔
487
                        while (true) {
4✔
488
                            $innerValue .= $innerValues[$i]['value'];
4✔
489

490
                            if (($innerValues[$i]['next_glue'] ?? $this->typesGlue) === $this->typesGlue) {
4✔
491
                                break;
4✔
492
                            }
493

494
                            $innerValue .= $innerValues[$i]['next_glue_raw'];
4✔
495

496
                            ++$i;
4✔
497
                        }
498

499
                        $this->innerTypeExpressions[] = [
4✔
500
                            'start_index' => $innerStartIndex,
4✔
501
                            'expression' => $this->inner($innerValue),
4✔
502
                        ];
4✔
503
                    }
504
                }
505

506
                return;
236✔
507
            }
508
        }
509

510
        $this->isCompositeType = false;
290✔
511

512
        if ('' !== $matches['nullable'][0]) {
290✔
513
            $this->innerTypeExpressions[] = [
17✔
514
                'start_index' => \strlen($matches['nullable'][0]),
17✔
515
                'expression' => $this->inner(substr($matches['type'][0], \strlen($matches['nullable'][0]))),
17✔
516
            ];
17✔
517
        } elseif ('' !== $matches['array'][0]) {
290✔
518
            $this->innerTypeExpressions[] = [
27✔
519
                'start_index' => 0,
27✔
520
                'expression' => $this->inner(substr($matches['type'][0], 0, -\strlen($matches['array'][0]))),
27✔
521
            ];
27✔
522
        } elseif ('' !== ($matches['generic'][0] ?? '') && 0 === $matches['generic'][1]) {
290✔
523
            $this->innerTypeExpressions[] = [
51✔
524
                'start_index' => 0,
51✔
525
                'expression' => $this->inner($matches['generic_name'][0]),
51✔
526
            ];
51✔
527

528
            $this->parseCommaSeparatedInnerTypes(
51✔
529
                \strlen($matches['generic_name'][0]) + \strlen($matches['generic_start'][0]),
51✔
530
                $matches['generic_types'][0]
51✔
531
            );
51✔
532
        } elseif ('' !== ($matches['callable'][0] ?? '') && 0 === $matches['callable'][1]) {
290✔
533
            $this->innerTypeExpressions[] = [
63✔
534
                'start_index' => 0,
63✔
535
                'expression' => $this->inner($matches['callable_name'][0]),
63✔
536
            ];
63✔
537

538
            $this->parseCallableTemplateInnerTypes(
63✔
539
                \strlen($matches['callable_name'][0])
63✔
540
                    + \strlen($matches['callable_template_start'][0]),
63✔
541
                $matches['callable_template_inners'][0]
63✔
542
            );
63✔
543

544
            $this->parseCallableArgumentTypes(
63✔
545
                \strlen($matches['callable_name'][0])
63✔
546
                    + \strlen($matches['callable_template'][0])
63✔
547
                    + \strlen($matches['callable_start'][0]),
63✔
548
                $matches['callable_arguments'][0]
63✔
549
            );
63✔
550

551
            if ('' !== ($matches['callable_return'][0] ?? '')) {
63✔
552
                $this->innerTypeExpressions[] = [
47✔
553
                    'start_index' => \strlen($this->value) - \strlen($matches['callable_return'][0]),
47✔
554
                    'expression' => $this->inner($matches['callable_return'][0]),
47✔
555
                ];
47✔
556
            }
557
        } elseif ('' !== ($matches['array_shape'][0] ?? '') && 0 === $matches['array_shape'][1]) {
290✔
558
            $this->innerTypeExpressions[] = [
45✔
559
                'start_index' => 0,
45✔
560
                'expression' => $this->inner($matches['array_shape_name'][0]),
45✔
561
            ];
45✔
562

563
            $nextIndex = \strlen($matches['array_shape_name'][0]) + \strlen($matches['array_shape_start'][0]);
45✔
564

565
            $this->parseArrayShapeInnerTypes(
45✔
566
                $nextIndex,
45✔
567
                $matches['array_shape_inners'][0]
45✔
568
            );
45✔
569

570
            if ('' !== ($matches['array_shape_unsealed_type'][0] ?? '')) {
45✔
571
                $nextIndex += \strlen($matches['array_shape_inners'][0])
8✔
572
                    + \strlen($matches['array_shape_unsealed_variadic'][0])
8✔
573
                    + \strlen($matches['array_shape_unsealed_type_start'][0]);
8✔
574

575
                $this->innerTypeExpressions[] = [
8✔
576
                    'start_index' => $nextIndex,
8✔
577
                    'expression' => $this->inner($matches['array_shape_unsealed_type_a'][0]),
8✔
578
                ];
8✔
579

580
                if ('' !== ($matches['array_shape_unsealed_type_b'][0] ?? '')) {
8✔
581
                    $nextIndex += \strlen($matches['array_shape_unsealed_type_a'][0])
2✔
582
                        + \strlen($matches['array_shape_unsealed_type_comma'][0]);
2✔
583

584
                    $this->innerTypeExpressions[] = [
2✔
585
                        'start_index' => $nextIndex,
2✔
586
                        'expression' => $this->inner($matches['array_shape_unsealed_type_b'][0]),
2✔
587
                    ];
2✔
588
                }
589
            }
590
        } elseif ('' !== ($matches['parenthesized'][0] ?? '') && 0 === $matches['parenthesized'][1]) {
290✔
591
            $index = \strlen($matches['parenthesized_start'][0]);
26✔
592

593
            if ('' !== ($matches['conditional'][0] ?? '')) {
26✔
594
                if ('' !== ($matches['conditional_cond_left_types'][0] ?? '')) {
7✔
595
                    $this->innerTypeExpressions[] = [
2✔
596
                        'start_index' => $index,
2✔
597
                        'expression' => $this->inner($matches['conditional_cond_left_types'][0]),
2✔
598
                    ];
2✔
599
                }
600

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

603
                $this->innerTypeExpressions[] = [
7✔
604
                    'start_index' => $index,
7✔
605
                    'expression' => $this->inner($matches['conditional_cond_right_types'][0]),
7✔
606
                ];
7✔
607

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

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

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

617
                $this->innerTypeExpressions[] = [
7✔
618
                    'start_index' => $index,
7✔
619
                    'expression' => $this->inner($matches['conditional_false_types'][0]),
7✔
620
                ];
7✔
621
            } else {
622
                $this->innerTypeExpressions[] = [
24✔
623
                    'start_index' => $index,
24✔
624
                    'expression' => $this->inner($matches['parenthesized_types'][0]),
24✔
625
                ];
24✔
626
            }
627
        } elseif ('' !== $matches['class_constant'][0]) {
290✔
628
            $this->innerTypeExpressions[] = [
8✔
629
                'start_index' => 0,
8✔
630
                'expression' => $this->inner($matches['class_constant_name'][0]),
8✔
631
            ];
8✔
632
        }
633
    }
634

635
    private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
636
    {
637
        $index = 0;
51✔
638
        while (\strlen($value) !== $index) {
51✔
639
            Preg::match(
51✔
640
                '{\G'.self::REGEX_TYPES.'(?:\h*,\h*|$)}',
51✔
641
                $value,
51✔
642
                $matches,
51✔
643
                0,
51✔
644
                $index
51✔
645
            );
51✔
646

647
            $this->innerTypeExpressions[] = [
51✔
648
                'start_index' => $startIndex + $index,
51✔
649
                'expression' => $this->inner($matches['types']),
51✔
650
            ];
51✔
651

652
            $index += \strlen($matches[0]);
51✔
653
        }
654
    }
655

656
    private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
657
    {
658
        $index = 0;
63✔
659
        while (\strlen($value) !== $index) {
63✔
660
            Preg::match(
11✔
661
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_template_inner>(?&callable_template_inner))(?:\h*,\h*|$))}',
11✔
662
                $value,
11✔
663
                $prematches,
11✔
664
                0,
11✔
665
                $index
11✔
666
            );
11✔
667
            $consumedValue = $prematches['_callable_template_inner'];
11✔
668
            $consumedValueLength = \strlen($consumedValue);
11✔
669
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
11✔
670

671
            $addedPrefix = 'Closure<';
11✔
672
            Preg::match(
11✔
673
                '{^'.self::REGEX_TYPES.'$}',
11✔
674
                $addedPrefix.$consumedValue.'>(): void',
11✔
675
                $matches,
11✔
676
                PREG_OFFSET_CAPTURE
11✔
677
            );
11✔
678

679
            if ('' !== $matches['callable_template_inner_b'][0]) {
11✔
680
                $this->innerTypeExpressions[] = [
5✔
681
                    'start_index' => $startIndex + $index + $matches['callable_template_inner_b_types'][1]
5✔
682
                        - \strlen($addedPrefix),
5✔
683
                    'expression' => $this->inner($matches['callable_template_inner_b_types'][0]),
5✔
684
                ];
5✔
685
            }
686

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

695
            $index += $consumedValueLength + $consumedCommaLength;
11✔
696
        }
697
    }
698

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

714
            $addedPrefix = 'Closure(';
48✔
715
            Preg::match(
48✔
716
                '{^'.self::REGEX_TYPES.'$}',
48✔
717
                $addedPrefix.$consumedValue.'): void',
48✔
718
                $matches,
48✔
719
                PREG_OFFSET_CAPTURE
48✔
720
            );
48✔
721

722
            $this->innerTypeExpressions[] = [
48✔
723
                'start_index' => $startIndex + $index,
48✔
724
                'expression' => $this->inner($matches['callable_argument_type'][0]),
48✔
725
            ];
48✔
726

727
            $index += $consumedValueLength + $consumedCommaLength;
48✔
728
        }
729
    }
730

731
    private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
732
    {
733
        $index = 0;
45✔
734
        while (\strlen($value) !== $index) {
45✔
735
            Preg::match(
40✔
736
                '{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_array_shape_inner>(?&array_shape_inner))(?:\h*,\h*|$))}',
40✔
737
                $value,
40✔
738
                $prematches,
40✔
739
                0,
40✔
740
                $index
40✔
741
            );
40✔
742
            $consumedValue = $prematches['_array_shape_inner'];
40✔
743
            $consumedValueLength = \strlen($consumedValue);
40✔
744
            $consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;
40✔
745

746
            $addedPrefix = 'array{';
40✔
747
            Preg::match(
40✔
748
                '{^'.self::REGEX_TYPES.'$}',
40✔
749
                $addedPrefix.$consumedValue.'}',
40✔
750
                $matches,
40✔
751
                PREG_OFFSET_CAPTURE
40✔
752
            );
40✔
753

754
            $this->innerTypeExpressions[] = [
40✔
755
                'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1]
40✔
756
                    - \strlen($addedPrefix),
40✔
757
                'expression' => $this->inner($matches['array_shape_inner_value'][0]),
40✔
758
            ];
40✔
759

760
            $index += $consumedValueLength + $consumedCommaLength;
40✔
761
        }
762
    }
763

764
    private function inner(string $value): self
765
    {
766
        return new self($value, $this->namespace, $this->namespaceUses);
268✔
767
    }
768

769
    private function getParentType(string $type1, string $type2): ?string
770
    {
771
        $types = [
17✔
772
            $this->normalize($type1),
17✔
773
            $this->normalize($type2),
17✔
774
        ];
17✔
775
        natcasesort($types);
17✔
776
        $types = implode('|', $types);
17✔
777

778
        $parents = [
17✔
779
            'array|Traversable' => 'iterable',
17✔
780
            'array|iterable' => 'iterable',
17✔
781
            'iterable|Traversable' => 'iterable',
17✔
782
            'self|static' => 'self',
17✔
783
        ];
17✔
784

785
        return $parents[$types] ?? null;
17✔
786
    }
787

788
    private function normalize(string $type): string
789
    {
790
        $aliases = $this->getAliases();
17✔
791

792
        if (isset($aliases[$type])) {
17✔
793
            return $aliases[$type];
×
794
        }
795

796
        if (\in_array($type, [
17✔
797
            'array',
17✔
798
            'bool',
17✔
799
            'callable',
17✔
800
            'false',
17✔
801
            'float',
17✔
802
            'int',
17✔
803
            'iterable',
17✔
804
            'mixed',
17✔
805
            'never',
17✔
806
            'null',
17✔
807
            'object',
17✔
808
            'resource',
17✔
809
            'string',
17✔
810
            'true',
17✔
811
            'void',
17✔
812
        ], true)) {
17✔
813
            return $type;
16✔
814
        }
815

816
        if (Preg::match('/\[\]$/', $type)) {
12✔
817
            return 'array';
×
818
        }
819

820
        if (Preg::match('/^(.+?)</', $type, $matches)) {
12✔
821
            return $matches[1];
×
822
        }
823

824
        if (str_starts_with($type, '\\')) {
12✔
825
            return substr($type, 1);
1✔
826
        }
827

828
        foreach ($this->namespaceUses as $namespaceUse) {
11✔
829
            if ($namespaceUse->getShortName() === $type) {
6✔
830
                return $namespaceUse->getFullName();
6✔
831
            }
832
        }
833

834
        if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
5✔
835
            return $type;
5✔
836
        }
837

838
        return "{$this->namespace->getFullName()}\\{$type}";
×
839
    }
840

841
    /**
842
     * @return array<string, string>
843
     */
844
    private function getAliases(): array
845
    {
846
        return [
60✔
847
            'boolean' => 'bool',
60✔
848
            'callback' => 'callable',
60✔
849
            'double' => 'float',
60✔
850
            'false' => 'bool',
60✔
851
            'integer' => 'int',
60✔
852
            'list' => 'array',
60✔
853
            'real' => 'float',
60✔
854
            'true' => 'bool',
60✔
855
        ];
60✔
856
    }
857
}
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