Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 688 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\ErrorHandler;
13
 
14
use Composer\InstalledVersions;
15
use Doctrine\Common\Persistence\Proxy as LegacyProxy;
16
use Doctrine\Persistence\Proxy;
17
use Mockery\MockInterface;
18
use Phake\IMock;
19
use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation;
20
use PHPUnit\Framework\MockObject\MockObject;
21
use Prophecy\Prophecy\ProphecySubjectInterface;
22
use ProxyManager\Proxy\ProxyInterface;
23
use Symfony\Component\ErrorHandler\Internal\TentativeTypes;
1663 lars 24
use Symfony\Component\HttpClient\HttplugClient;
148 lars 25
use Symfony\Component\VarExporter\LazyObjectInterface;
26
 
27
/**
28
 * Autoloader checking if the class is really defined in the file found.
29
 *
30
 * The ClassLoader will wrap all registered autoloaders
31
 * and will throw an exception if a file is found but does
32
 * not declare the class.
33
 *
34
 * It can also patch classes to turn docblocks into actual return types.
35
 * This behavior is controlled by the SYMFONY_PATCH_TYPE_DECLARATIONS env var,
36
 * which is a url-encoded array with the follow parameters:
37
 *  - "force": any value enables deprecation notices - can be any of:
38
 *      - "phpdoc" to patch only docblock annotations
39
 *      - "2" to add all possible return types
40
 *      - "1" to add return types but only to tests/final/internal/private methods
41
 *  - "php": the target version of PHP - e.g. "7.1" doesn't generate "object" types
42
 *  - "deprecations": "1" to trigger a deprecation notice when a child class misses a
43
 *                    return type while the parent declares an "@return" annotation
44
 *
45
 * Note that patching doesn't care about any coding style so you'd better to run
46
 * php-cs-fixer after, with rules "phpdoc_trim_consecutive_blank_line_separation"
47
 * and "no_superfluous_phpdoc_tags" enabled typically.
48
 *
49
 * @author Fabien Potencier <fabien@symfony.com>
50
 * @author Christophe Coevoet <stof@notk.org>
51
 * @author Nicolas Grekas <p@tchwork.com>
52
 * @author Guilhem Niot <guilhem.niot@gmail.com>
53
 */
54
class DebugClassLoader
55
{
56
    private const SPECIAL_RETURN_TYPES = [
57
        'void' => 'void',
58
        'null' => 'null',
59
        'resource' => 'resource',
60
        'boolean' => 'bool',
61
        'true' => 'true',
62
        'false' => 'false',
63
        'integer' => 'int',
64
        'array' => 'array',
65
        'bool' => 'bool',
66
        'callable' => 'callable',
67
        'float' => 'float',
68
        'int' => 'int',
69
        'iterable' => 'iterable',
70
        'object' => 'object',
71
        'string' => 'string',
72
        'self' => 'self',
73
        'parent' => 'parent',
74
        'mixed' => 'mixed',
75
        'static' => 'static',
76
        '$this' => 'static',
77
        'list' => 'array',
78
        'class-string' => 'string',
79
        'never' => 'never',
80
    ];
81
 
82
    private const BUILTIN_RETURN_TYPES = [
83
        'void' => true,
84
        'array' => true,
85
        'false' => true,
86
        'bool' => true,
87
        'callable' => true,
88
        'float' => true,
89
        'int' => true,
90
        'iterable' => true,
91
        'object' => true,
92
        'string' => true,
93
        'self' => true,
94
        'parent' => true,
95
        'mixed' => true,
96
        'static' => true,
97
        'null' => true,
98
        'true' => true,
99
        'never' => true,
100
    ];
101
 
102
    private const MAGIC_METHODS = [
103
        '__isset' => 'bool',
104
        '__sleep' => 'array',
105
        '__toString' => 'string',
106
        '__debugInfo' => 'array',
107
        '__serialize' => 'array',
108
    ];
109
 
110
    /**
111
     * @var callable
112
     */
113
    private $classLoader;
114
    private bool $isFinder;
115
    private array $loaded = [];
116
    private array $patchTypes = [];
117
 
118
    private static int $caseCheck;
119
    private static array $checkedClasses = [];
120
    private static array $final = [];
121
    private static array $finalMethods = [];
122
    private static array $finalProperties = [];
123
    private static array $finalConstants = [];
124
    private static array $deprecated = [];
125
    private static array $internal = [];
126
    private static array $internalMethods = [];
127
    private static array $annotatedParameters = [];
128
    private static array $darwinCache = ['/' => ['/', []]];
129
    private static array $method = [];
130
    private static array $returnTypes = [];
131
    private static array $methodTraits = [];
132
    private static array $fileOffsets = [];
133
 
134
    public function __construct(callable $classLoader)
135
    {
136
        $this->classLoader = $classLoader;
137
        $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
138
        parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);
139
        $this->patchTypes += [
140
            'force' => null,
141
            'php' => \PHP_MAJOR_VERSION.'.'.\PHP_MINOR_VERSION,
142
            'deprecations' => true,
143
        ];
144
 
145
        if ('phpdoc' === $this->patchTypes['force']) {
146
            $this->patchTypes['force'] = 'docblock';
147
        }
148
 
149
        if (!isset(self::$caseCheck)) {
150
            $file = is_file(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
151
            $i = strrpos($file, \DIRECTORY_SEPARATOR);
152
            $dir = substr($file, 0, 1 + $i);
153
            $file = substr($file, 1 + $i);
154
            $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file);
155
            $test = realpath($dir.$test);
156
 
157
            if (false === $test || false === $i) {
158
                // filesystem is case sensitive
159
                self::$caseCheck = 0;
160
            } elseif (str_ends_with($test, $file)) {
161
                // filesystem is case insensitive and realpath() normalizes the case of characters
162
                self::$caseCheck = 1;
163
            } elseif ('Darwin' === \PHP_OS_FAMILY) {
164
                // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters
165
                self::$caseCheck = 2;
166
            } else {
167
                // filesystem case checks failed, fallback to disabling them
168
                self::$caseCheck = 0;
169
            }
170
        }
171
    }
172
 
173
    public function getClassLoader(): callable
174
    {
175
        return $this->classLoader;
176
    }
177
 
178
    /**
179
     * Wraps all autoloaders.
180
     */
181
    public static function enable(): void
182
    {
183
        // Ensures we don't hit https://bugs.php.net/42098
184
        class_exists(\Symfony\Component\ErrorHandler\ErrorHandler::class);
185
        class_exists(\Psr\Log\LogLevel::class);
186
 
187
        if (!\is_array($functions = spl_autoload_functions())) {
188
            return;
189
        }
190
 
191
        foreach ($functions as $function) {
192
            spl_autoload_unregister($function);
193
        }
194
 
195
        foreach ($functions as $function) {
196
            if (!\is_array($function) || !$function[0] instanceof self) {
197
                $function = [new static($function), 'loadClass'];
198
            }
199
 
200
            spl_autoload_register($function);
201
        }
202
    }
203
 
204
    /**
205
     * Disables the wrapping.
206
     */
207
    public static function disable(): void
208
    {
209
        if (!\is_array($functions = spl_autoload_functions())) {
210
            return;
211
        }
212
 
213
        foreach ($functions as $function) {
214
            spl_autoload_unregister($function);
215
        }
216
 
217
        foreach ($functions as $function) {
218
            if (\is_array($function) && $function[0] instanceof self) {
219
                $function = $function[0]->getClassLoader();
220
            }
221
 
222
            spl_autoload_register($function);
223
        }
224
    }
225
 
226
    public static function checkClasses(): bool
227
    {
228
        if (!\is_array($functions = spl_autoload_functions())) {
229
            return false;
230
        }
231
 
232
        $loader = null;
233
 
234
        foreach ($functions as $function) {
235
            if (\is_array($function) && $function[0] instanceof self) {
236
                $loader = $function[0];
237
                break;
238
            }
239
        }
240
 
241
        if (null === $loader) {
242
            return false;
243
        }
244
 
245
        static $offsets = [
246
            'get_declared_interfaces' => 0,
247
            'get_declared_traits' => 0,
248
            'get_declared_classes' => 0,
249
        ];
250
 
251
        foreach ($offsets as $getSymbols => $i) {
252
            $symbols = $getSymbols();
253
 
254
            for (; $i < \count($symbols); ++$i) {
255
                if (!is_subclass_of($symbols[$i], MockObject::class)
256
                    && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class)
257
                    && !is_subclass_of($symbols[$i], Proxy::class)
258
                    && !is_subclass_of($symbols[$i], ProxyInterface::class)
259
                    && !is_subclass_of($symbols[$i], LazyObjectInterface::class)
260
                    && !is_subclass_of($symbols[$i], LegacyProxy::class)
261
                    && !is_subclass_of($symbols[$i], MockInterface::class)
262
                    && !is_subclass_of($symbols[$i], IMock::class)
263
                ) {
264
                    $loader->checkClass($symbols[$i]);
265
                }
266
            }
267
 
268
            $offsets[$getSymbols] = $i;
269
        }
270
 
271
        return true;
272
    }
273
 
274
    public function findFile(string $class): ?string
275
    {
276
        return $this->isFinder ? ($this->classLoader[0]->findFile($class) ?: null) : null;
277
    }
278
 
279
    /**
280
     * Loads the given class or interface.
281
     *
282
     * @throws \RuntimeException
283
     */
284
    public function loadClass(string $class): void
285
    {
286
        $e = error_reporting(error_reporting() | \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR);
287
 
288
        try {
289
            if ($this->isFinder && !isset($this->loaded[$class])) {
290
                $this->loaded[$class] = true;
291
                if (!$file = $this->classLoader[0]->findFile($class) ?: '') {
292
                    // no-op
293
                } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) {
294
                    include $file;
295
 
296
                    return;
297
                } elseif (false === include $file) {
298
                    return;
299
                }
300
            } else {
301
                ($this->classLoader)($class);
302
                $file = '';
303
            }
304
        } finally {
305
            error_reporting($e);
306
        }
307
 
308
        $this->checkClass($class, $file);
309
    }
310
 
311
    private function checkClass(string $class, string $file = null): void
312
    {
313
        $exists = null === $file || class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false);
314
 
315
        if (null !== $file && $class && '\\' === $class[0]) {
316
            $class = substr($class, 1);
317
        }
318
 
319
        if ($exists) {
320
            if (isset(self::$checkedClasses[$class])) {
321
                return;
322
            }
323
            self::$checkedClasses[$class] = true;
324
 
325
            $refl = new \ReflectionClass($class);
326
            if (null === $file && $refl->isInternal()) {
327
                return;
328
            }
329
            $name = $refl->getName();
330
 
331
            if ($name !== $class && 0 === strcasecmp($name, $class)) {
332
                throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
333
            }
334
 
335
            $deprecations = $this->checkAnnotations($refl, $name);
336
 
337
            foreach ($deprecations as $message) {
338
                @trigger_error($message, \E_USER_DEPRECATED);
339
            }
340
        }
341
 
342
        if (!$file) {
343
            return;
344
        }
345
 
346
        if (!$exists) {
347
            if (str_contains($class, '/')) {
348
                throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
349
            }
350
 
351
            throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file));
352
        }
353
 
354
        if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) {
355
            throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2]));
356
        }
357
    }
358
 
359
    public function checkAnnotations(\ReflectionClass $refl, string $class): array
360
    {
361
        if (
362
            'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7' === $class
363
            || 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6' === $class
364
        ) {
365
            return [];
366
        }
367
        $deprecations = [];
368
 
369
        $className = str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
370
 
371
        // Don't trigger deprecations for classes in the same vendor
372
        if ($class !== $className) {
373
            $vendor = preg_match('/^namespace ([^;\\\\\s]++)[;\\\\]/m', @file_get_contents($refl->getFileName()), $vendor) ? $vendor[1].'\\' : '';
374
            $vendorLen = \strlen($vendor);
375
        } elseif (2 > $vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) {
376
            $vendorLen = 0;
377
            $vendor = '';
378
        } else {
379
            $vendor = str_replace('_', '\\', substr($class, 0, $vendorLen));
380
        }
381
 
382
        $parent = get_parent_class($class) ?: null;
383
        self::$returnTypes[$class] = [];
384
        $classIsTemplate = false;
385
 
386
        // Detect annotations on the class
387
        if ($doc = $this->parsePhpDoc($refl)) {
388
            $classIsTemplate = isset($doc['template']);
389
 
390
            foreach (['final', 'deprecated', 'internal'] as $annotation) {
391
                if (null !== $description = $doc[$annotation][0] ?? null) {
392
                    self::${$annotation}[$class] = '' !== $description ? ' '.$description.(preg_match('/[.!]$/', $description) ? '' : '.') : '.';
393
                }
394
            }
395
 
396
            if ($refl->isInterface() && isset($doc['method'])) {
397
                foreach ($doc['method'] as $name => [$static, $returnType, $signature, $description]) {
398
                    self::$method[$class][] = [$class, $static, $returnType, $name.$signature, $description];
399
 
400
                    if ('' !== $returnType) {
401
                        $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent);
402
                    }
403
                }
404
            }
405
        }
406
 
407
        $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
408
        if ($parent) {
409
            $parentAndOwnInterfaces[$parent] = $parent;
410
 
411
            if (!isset(self::$checkedClasses[$parent])) {
412
                $this->checkClass($parent);
413
            }
414
 
415
            if (isset(self::$final[$parent])) {
416
                $deprecations[] = sprintf('The "%s" class is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $className);
417
            }
418
        }
419
 
420
        // Detect if the parent is annotated
421
        foreach ($parentAndOwnInterfaces + class_uses($class, false) as $use) {
422
            if (!isset(self::$checkedClasses[$use])) {
423
                $this->checkClass($use);
424
            }
1663 lars 425
            if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class])
426
                && !(HttplugClient::class === $class && \in_array($use, [\Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true))
427
            ) {
148 lars 428
                $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
429
                $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
430
 
431
                $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]);
432
            }
433
            if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) {
434
                $deprecations[] = sprintf('The "%s" %s is considered internal%s It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $className);
435
            }
436
            if (isset(self::$method[$use])) {
437
                if ($refl->isAbstract()) {
438
                    if (isset(self::$method[$class])) {
439
                        self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]);
440
                    } else {
441
                        self::$method[$class] = self::$method[$use];
442
                    }
443
                } elseif (!$refl->isInterface()) {
444
                    if (!strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)
445
                        && str_starts_with($className, 'Symfony\\')
446
                        && (!class_exists(InstalledVersions::class)
447
                            || 'symfony/symfony' !== InstalledVersions::getRootPackage()['name'])
448
                    ) {
449
                        // skip "same vendor" @method deprecations for Symfony\* classes unless symfony/symfony is being tested
450
                        continue;
451
                    }
452
                    $hasCall = $refl->hasMethod('__call');
453
                    $hasStaticCall = $refl->hasMethod('__callStatic');
454
                    foreach (self::$method[$use] as [$interface, $static, $returnType, $name, $description]) {
455
                        if ($static ? $hasStaticCall : $hasCall) {
456
                            continue;
457
                        }
458
                        $realName = substr($name, 0, strpos($name, '('));
459
                        if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) {
460
                            $deprecations[] = sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '').$interface, $name, $returnType ? ': '.$returnType : '', null === $description ? '.' : ': '.$description);
461
                        }
462
                    }
463
                }
464
            }
465
        }
466
 
467
        if (trait_exists($class)) {
468
            $file = $refl->getFileName();
469
 
470
            foreach ($refl->getMethods() as $method) {
471
                if ($method->getFileName() === $file) {
472
                    self::$methodTraits[$file][$method->getStartLine()] = $class;
473
                }
474
            }
475
 
476
            return $deprecations;
477
        }
478
 
479
        // Inherit @final, @internal, @param and @return annotations for methods
480
        self::$finalMethods[$class] = [];
481
        self::$internalMethods[$class] = [];
482
        self::$annotatedParameters[$class] = [];
483
        self::$finalProperties[$class] = [];
484
        self::$finalConstants[$class] = [];
485
        foreach ($parentAndOwnInterfaces as $use) {
486
            foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes', 'finalProperties', 'finalConstants'] as $property) {
487
                if (isset(self::${$property}[$use])) {
488
                    self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
489
                }
490
            }
491
 
492
            if (null !== (TentativeTypes::RETURN_TYPES[$use] ?? null)) {
493
                foreach (TentativeTypes::RETURN_TYPES[$use] as $method => $returnType) {
494
                    $returnType = explode('|', $returnType);
495
                    foreach ($returnType as $i => $t) {
496
                        if ('?' !== $t && !isset(self::BUILTIN_RETURN_TYPES[$t])) {
497
                            $returnType[$i] = '\\'.$t;
498
                        }
499
                    }
500
                    $returnType = implode('|', $returnType);
501
 
502
                    self::$returnTypes[$class] += [$method => [$returnType, str_starts_with($returnType, '?') ? substr($returnType, 1).'|null' : $returnType, $use, '']];
503
                }
504
            }
505
        }
506
 
507
        foreach ($refl->getMethods() as $method) {
508
            if ($method->class !== $class) {
509
                continue;
510
            }
511
 
512
            if (null === $ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null) {
513
                $ns = $vendor;
514
                $len = $vendorLen;
515
            } elseif (2 > $len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_'))) {
516
                $len = 0;
517
                $ns = '';
518
            } else {
519
                $ns = str_replace('_', '\\', substr($ns, 0, $len));
520
            }
521
 
522
            if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
523
                [$declaringClass, $message] = self::$finalMethods[$parent][$method->name];
524
                $deprecations[] = sprintf('The "%s::%s()" method is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className);
525
            }
526
 
527
            if (isset(self::$internalMethods[$class][$method->name])) {
528
                [$declaringClass, $message] = self::$internalMethods[$class][$method->name];
529
                if (strncmp($ns, $declaringClass, $len)) {
530
                    $deprecations[] = sprintf('The "%s::%s()" method is considered internal%s It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className);
531
                }
532
            }
533
 
534
            // To read method annotations
535
            $doc = $this->parsePhpDoc($method);
536
 
537
            if (($classIsTemplate || isset($doc['template'])) && $method->hasReturnType()) {
538
                unset($doc['return']);
539
            }
540
 
541
            if (isset(self::$annotatedParameters[$class][$method->name])) {
542
                $definedParameters = [];
543
                foreach ($method->getParameters() as $parameter) {
544
                    $definedParameters[$parameter->name] = true;
545
                }
546
 
547
                foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) {
548
                    if (!isset($definedParameters[$parameterName]) && !isset($doc['param'][$parameterName])) {
549
                        $deprecations[] = sprintf($deprecation, $className);
550
                    }
551
                }
552
            }
553
 
554
            $forcePatchTypes = $this->patchTypes['force'];
555
 
556
            if ($canAddReturnType = null !== $forcePatchTypes && !str_contains($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) {
557
                if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
558
                    $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock';
559
                }
560
 
561
                $canAddReturnType = 2 === (int) $forcePatchTypes
562
                    || false !== stripos($method->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR)
563
                    || $refl->isFinal()
564
                    || $method->isFinal()
565
                    || $method->isPrivate()
566
                    || ('.' === (self::$internal[$class] ?? null) && !$refl->isAbstract())
567
                    || '.' === (self::$final[$class] ?? null)
568
                    || '' === ($doc['final'][0] ?? null)
569
                    || '' === ($doc['internal'][0] ?? null)
570
                ;
571
            }
572
 
573
            if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? null) && 'docblock' === $this->patchTypes['force'] && !$method->hasReturnType() && isset(TentativeTypes::RETURN_TYPES[$returnType[2]][$method->name])) {
574
                $this->patchReturnTypeWillChange($method);
575
            }
576
 
577
            if (null !== ($returnType ??= self::MAGIC_METHODS[$method->name] ?? null) && !$method->hasReturnType() && !isset($doc['return'])) {
578
                [$normalizedType, $returnType, $declaringClass, $declaringFile] = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType;
579
 
580
                if ($canAddReturnType && 'docblock' !== $this->patchTypes['force']) {
581
                    $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
582
                }
583
                if (!isset($doc['deprecated']) && strncmp($ns, $declaringClass, $len)) {
584
                    if ('docblock' === $this->patchTypes['force']) {
585
                        $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
586
                    } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) {
587
                        $deprecations[] = sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className);
588
                    }
589
                }
590
            }
591
 
592
            if (!$doc) {
593
                $this->patchTypes['force'] = $forcePatchTypes;
594
 
595
                continue;
596
            }
597
 
598
            if (isset($doc['return']) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
599
                $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType());
600
 
601
                if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
602
                    $this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
603
                }
604
 
605
                if ($method->isPrivate()) {
606
                    unset(self::$returnTypes[$class][$method->name]);
607
                }
608
            }
609
 
610
            $this->patchTypes['force'] = $forcePatchTypes;
611
 
612
            if ($method->isPrivate()) {
613
                continue;
614
            }
615
 
616
            $finalOrInternal = false;
617
 
618
            foreach (['final', 'internal'] as $annotation) {
619
                if (null !== $description = $doc[$annotation][0] ?? null) {
620
                    self::${$annotation.'Methods'}[$class][$method->name] = [$class, '' !== $description ? ' '.$description.(preg_match('/[[:punct:]]$/', $description) ? '' : '.') : '.'];
621
                    $finalOrInternal = true;
622
                }
623
            }
624
 
625
            if ($finalOrInternal || $method->isConstructor() || !isset($doc['param']) || StatelessInvocation::class === $class) {
626
                continue;
627
            }
628
            if (!isset(self::$annotatedParameters[$class][$method->name])) {
629
                $definedParameters = [];
630
                foreach ($method->getParameters() as $parameter) {
631
                    $definedParameters[$parameter->name] = true;
632
                }
633
            }
634
            foreach ($doc['param'] as $parameterName => $parameterType) {
635
                if (!isset($definedParameters[$parameterName])) {
636
                    self::$annotatedParameters[$class][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its %s "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, interface_exists($className) ? 'interface' : 'parent class', $className);
637
                }
638
            }
639
        }
640
 
641
        $finals = isset(self::$final[$class]) || $refl->isFinal() ? [] : [
642
            'finalConstants' => $refl->getReflectionConstants(\ReflectionClassConstant::IS_PUBLIC | \ReflectionClassConstant::IS_PROTECTED),
643
            'finalProperties' => $refl->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED),
644
        ];
645
        foreach ($finals as $type => $reflectors) {
646
            foreach ($reflectors as $r) {
647
                if ($r->class !== $class) {
648
                    continue;
649
                }
650
 
651
                $doc = $this->parsePhpDoc($r);
652
 
653
                foreach ($parentAndOwnInterfaces as $use) {
654
                    if (isset(self::${$type}[$use][$r->name]) && !isset($doc['deprecated']) && ('finalConstants' === $type || substr($use, 0, strrpos($use, '\\')) !== substr($use, 0, strrpos($class, '\\')))) {
655
                        $msg = 'finalConstants' === $type ? '%s" constant' : '$%s" property';
656
                        $deprecations[] = sprintf('The "%s::'.$msg.' is considered final. You should not override it in "%s".', self::${$type}[$use][$r->name], $r->name, $class);
657
                    }
658
                }
659
 
660
                if (isset($doc['final']) || ('finalProperties' === $type && str_starts_with($class, 'Symfony\\') && !$r->hasType())) {
661
                    self::${$type}[$class][$r->name] = $class;
662
                }
663
            }
664
        }
665
 
666
        return $deprecations;
667
    }
668
 
669
    public function checkCase(\ReflectionClass $refl, string $file, string $class): ?array
670
    {
671
        $real = explode('\\', $class.strrchr($file, '.'));
672
        $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file));
673
 
674
        $i = \count($tail) - 1;
675
        $j = \count($real) - 1;
676
 
677
        while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) {
678
            --$i;
679
            --$j;
680
        }
681
 
682
        array_splice($tail, 0, $i + 1);
683
 
684
        if (!$tail) {
685
            return null;
686
        }
687
 
688
        $tail = \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $tail);
689
        $tailLen = \strlen($tail);
690
        $real = $refl->getFileName();
691
 
692
        if (2 === self::$caseCheck) {
693
            $real = $this->darwinRealpath($real);
694
        }
695
 
696
        if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true)
697
            && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false)
698
        ) {
699
            return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)];
700
        }
701
 
702
        return null;
703
    }
704
 
705
    /**
706
     * `realpath` on MacOSX doesn't normalize the case of characters.
707
     */
708
    private function darwinRealpath(string $real): string
709
    {
710
        $i = 1 + strrpos($real, '/');
711
        $file = substr($real, $i);
712
        $real = substr($real, 0, $i);
713
 
714
        if (isset(self::$darwinCache[$real])) {
715
            $kDir = $real;
716
        } else {
717
            $kDir = strtolower($real);
718
 
719
            if (isset(self::$darwinCache[$kDir])) {
720
                $real = self::$darwinCache[$kDir][0];
721
            } else {
722
                $dir = getcwd();
723
 
724
                if (!@chdir($real)) {
725
                    return $real.$file;
726
                }
727
 
728
                $real = getcwd().'/';
729
                chdir($dir);
730
 
731
                $dir = $real;
732
                $k = $kDir;
733
                $i = \strlen($dir) - 1;
734
                while (!isset(self::$darwinCache[$k])) {
735
                    self::$darwinCache[$k] = [$dir, []];
736
                    self::$darwinCache[$dir] = &self::$darwinCache[$k];
737
 
738
                    while ('/' !== $dir[--$i]) {
739
                    }
740
                    $k = substr($k, 0, ++$i);
741
                    $dir = substr($dir, 0, $i--);
742
                }
743
            }
744
        }
745
 
746
        $dirFiles = self::$darwinCache[$kDir][1];
747
 
748
        if (!isset($dirFiles[$file]) && str_ends_with($file, ') : eval()\'d code')) {
749
            // Get the file name from "file_name.php(123) : eval()'d code"
750
            $file = substr($file, 0, strrpos($file, '(', -17));
751
        }
752
 
753
        if (isset($dirFiles[$file])) {
754
            return $real.$dirFiles[$file];
755
        }
756
 
757
        $kFile = strtolower($file);
758
 
759
        if (!isset($dirFiles[$kFile])) {
760
            foreach (scandir($real, 2) as $f) {
761
                if ('.' !== $f[0]) {
762
                    $dirFiles[$f] = $f;
763
                    if ($f === $file) {
764
                        $kFile = $k = $file;
765
                    } elseif ($f !== $k = strtolower($f)) {
766
                        $dirFiles[$k] = $f;
767
                    }
768
                }
769
            }
770
            self::$darwinCache[$kDir][1] = $dirFiles;
771
        }
772
 
773
        return $real.$dirFiles[$kFile];
774
    }
775
 
776
    /**
777
     * `class_implements` includes interfaces from the parents so we have to manually exclude them.
778
     *
779
     * @return string[]
780
     */
781
    private function getOwnInterfaces(string $class, ?string $parent): array
782
    {
783
        $ownInterfaces = class_implements($class, false);
784
 
785
        if ($parent) {
786
            foreach (class_implements($parent, false) as $interface) {
787
                unset($ownInterfaces[$interface]);
788
            }
789
        }
790
 
791
        foreach ($ownInterfaces as $interface) {
792
            foreach (class_implements($interface) as $interface) {
793
                unset($ownInterfaces[$interface]);
794
            }
795
        }
796
 
797
        return $ownInterfaces;
798
    }
799
 
800
    private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, \ReflectionType $returnType = null): void
801
    {
802
        if ('__construct' === $method) {
803
            return;
804
        }
805
 
806
        if ('null' === $types) {
807
            self::$returnTypes[$class][$method] = ['null', 'null', $class, $filename];
808
 
809
            return;
810
        }
811
 
812
        if ($nullable = str_starts_with($types, 'null|')) {
813
            $types = substr($types, 5);
814
        } elseif ($nullable = str_ends_with($types, '|null')) {
815
            $types = substr($types, 0, -5);
816
        }
817
        $arrayType = ['array' => 'array'];
818
        $typesMap = [];
819
        $glue = str_contains($types, '&') ? '&' : '|';
820
        foreach (explode($glue, $types) as $t) {
821
            $t = self::SPECIAL_RETURN_TYPES[strtolower($t)] ?? $t;
822
            $typesMap[$this->normalizeType($t, $class, $parent, $returnType)][$t] = $t;
823
        }
824
 
825
        if (isset($typesMap['array'])) {
826
            if (isset($typesMap['Traversable']) || isset($typesMap['\Traversable'])) {
827
                $typesMap['iterable'] = $arrayType !== $typesMap['array'] ? $typesMap['array'] : ['iterable'];
828
                unset($typesMap['array'], $typesMap['Traversable'], $typesMap['\Traversable']);
829
            } elseif ($arrayType !== $typesMap['array'] && isset(self::$returnTypes[$class][$method]) && !$returnType) {
830
                return;
831
            }
832
        }
833
 
834
        if (isset($typesMap['array']) && isset($typesMap['iterable'])) {
835
            if ($arrayType !== $typesMap['array']) {
836
                $typesMap['iterable'] = $typesMap['array'];
837
            }
838
            unset($typesMap['array']);
839
        }
840
 
841
        $iterable = $object = true;
842
        foreach ($typesMap as $n => $t) {
843
            if ('null' !== $n) {
844
                $iterable = $iterable && (\in_array($n, ['array', 'iterable']) || str_contains($n, 'Iterator'));
845
                $object = $object && (\in_array($n, ['callable', 'object', '$this', 'static']) || !isset(self::SPECIAL_RETURN_TYPES[$n]));
846
            }
847
        }
848
 
849
        $phpTypes = [];
850
        $docTypes = [];
851
 
852
        foreach ($typesMap as $n => $t) {
853
            if ('null' === $n) {
854
                $nullable = true;
855
                continue;
856
            }
857
 
858
            $docTypes[] = $t;
859
 
860
            if ('mixed' === $n || 'void' === $n) {
861
                $nullable = false;
862
                $phpTypes = ['' => $n];
863
                continue;
864
            }
865
 
866
            if ('resource' === $n) {
867
                // there is no native type for "resource"
868
                return;
869
            }
870
 
871
            if (!preg_match('/^(?:\\\\?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)+$/', $n)) {
872
                // exclude any invalid PHP class name (e.g. `Cookie::SAMESITE_*`)
873
                continue;
874
            }
875
 
876
            if (!isset($phpTypes[''])) {
877
                $phpTypes[] = $n;
878
            }
879
        }
880
        $docTypes = array_merge([], ...$docTypes);
881
 
882
        if (!$phpTypes) {
883
            return;
884
        }
885
 
886
        if (1 < \count($phpTypes)) {
887
            if ($iterable && '8.0' > $this->patchTypes['php']) {
888
                $phpTypes = $docTypes = ['iterable'];
889
            } elseif ($object && 'object' === $this->patchTypes['force']) {
890
                $phpTypes = $docTypes = ['object'];
891
            } elseif ('8.0' > $this->patchTypes['php']) {
892
                // ignore multi-types return declarations
893
                return;
894
            }
895
        }
896
 
897
        $phpType = sprintf($nullable ? (1 < \count($phpTypes) ? '%s|null' : '?%s') : '%s', implode($glue, $phpTypes));
898
        $docType = sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes));
899
 
900
        self::$returnTypes[$class][$method] = [$phpType, $docType, $class, $filename];
901
    }
902
 
903
    private function normalizeType(string $type, string $class, ?string $parent, ?\ReflectionType $returnType): string
904
    {
905
        if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) {
906
            if ('parent' === $lcType = self::SPECIAL_RETURN_TYPES[$lcType]) {
907
                $lcType = null !== $parent ? '\\'.$parent : 'parent';
908
            } elseif ('self' === $lcType) {
909
                $lcType = '\\'.$class;
910
            }
911
 
912
            return $lcType;
913
        }
914
 
915
        // We could resolve "use" statements to return the FQDN
916
        // but this would be too expensive for a runtime checker
917
 
918
        if (!str_ends_with($type, '[]')) {
919
            return $type;
920
        }
921
 
922
        if ($returnType instanceof \ReflectionNamedType) {
923
            $type = $returnType->getName();
924
 
925
            if ('mixed' !== $type) {
926
                return isset(self::SPECIAL_RETURN_TYPES[$type]) ? $type : '\\'.$type;
927
            }
928
        }
929
 
930
        return 'array';
931
    }
932
 
933
    /**
934
     * Utility method to add #[ReturnTypeWillChange] where php triggers deprecations.
935
     */
936
    private function patchReturnTypeWillChange(\ReflectionMethod $method)
937
    {
938
        if (\count($method->getAttributes(\ReturnTypeWillChange::class))) {
939
            return;
940
        }
941
 
942
        if (!is_file($file = $method->getFileName())) {
943
            return;
944
        }
945
 
946
        $fileOffset = self::$fileOffsets[$file] ?? 0;
947
 
948
        $code = file($file);
949
 
950
        $startLine = $method->getStartLine() + $fileOffset - 2;
951
 
952
        if (false !== stripos($code[$startLine], 'ReturnTypeWillChange')) {
953
            return;
954
        }
955
 
956
        $code[$startLine] .= "    #[\\ReturnTypeWillChange]\n";
957
        self::$fileOffsets[$file] = 1 + $fileOffset;
958
        file_put_contents($file, $code);
959
    }
960
 
961
    /**
962
     * Utility method to add @return annotations to the Symfony code-base where it triggers self-deprecations.
963
     */
964
    private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
965
    {
966
        static $patchedMethods = [];
967
        static $useStatements = [];
968
 
969
        if (!is_file($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) {
970
            return;
971
        }
972
 
973
        $patchedMethods[$file][$startLine] = true;
974
        $fileOffset = self::$fileOffsets[$file] ?? 0;
975
        $startLine += $fileOffset - 2;
976
        if ($nullable = str_ends_with($returnType, '|null')) {
977
            $returnType = substr($returnType, 0, -5);
978
        }
979
        $glue = str_contains($returnType, '&') ? '&' : '|';
980
        $returnType = explode($glue, $returnType);
981
        $code = file($file);
982
 
983
        foreach ($returnType as $i => $type) {
984
            if (preg_match('/((?:\[\])+)$/', $type, $m)) {
985
                $type = substr($type, 0, -\strlen($m[1]));
986
                $format = '%s'.$m[1];
987
            } else {
988
                $format = null;
989
            }
990
 
991
            if (isset(self::SPECIAL_RETURN_TYPES[$type]) || ('\\' === $type[0] && !$p = strrpos($type, '\\', 1))) {
992
                continue;
993
            }
994
 
995
            [$namespace, $useOffset, $useMap] = $useStatements[$file] ??= self::getUseStatements($file);
996
 
997
            if ('\\' !== $type[0]) {
998
                [$declaringNamespace, , $declaringUseMap] = $useStatements[$declaringFile] ??= self::getUseStatements($declaringFile);
999
 
1000
                $p = strpos($type, '\\', 1);
1001
                $alias = $p ? substr($type, 0, $p) : $type;
1002
 
1003
                if (isset($declaringUseMap[$alias])) {
1004
                    $type = '\\'.$declaringUseMap[$alias].($p ? substr($type, $p) : '');
1005
                } else {
1006
                    $type = '\\'.$declaringNamespace.$type;
1007
                }
1008
 
1009
                $p = strrpos($type, '\\', 1);
1010
            }
1011
 
1012
            $alias = substr($type, 1 + $p);
1013
            $type = substr($type, 1);
1014
 
1015
            if (!isset($useMap[$alias]) && (class_exists($c = $namespace.$alias) || interface_exists($c) || trait_exists($c))) {
1016
                $useMap[$alias] = $c;
1017
            }
1018
 
1019
            if (!isset($useMap[$alias])) {
1020
                $useStatements[$file][2][$alias] = $type;
1021
                $code[$useOffset] = "use $type;\n".$code[$useOffset];
1022
                ++$fileOffset;
1023
            } elseif ($useMap[$alias] !== $type) {
1024
                $alias .= 'FIXME';
1025
                $useStatements[$file][2][$alias] = $type;
1026
                $code[$useOffset] = "use $type as $alias;\n".$code[$useOffset];
1027
                ++$fileOffset;
1028
            }
1029
 
1030
            $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;
1031
        }
1032
 
1033
        if ('docblock' === $this->patchTypes['force'] || ('object' === $normalizedType && '7.1' === $this->patchTypes['php'])) {
1034
            $returnType = implode($glue, $returnType).($nullable ? '|null' : '');
1035
 
1036
            if (str_contains($code[$startLine], '#[')) {
1037
                --$startLine;
1038
            }
1039
 
1040
            if ($method->getDocComment()) {
1041
                $code[$startLine] = "     * @return $returnType\n".$code[$startLine];
1042
            } else {
1043
                $code[$startLine] .= <<<EOTXT
1044
    /**
1045
     * @return $returnType
1046
     */
1047
 
1048
EOTXT;
1049
            }
1050
 
1051
            $fileOffset += substr_count($code[$startLine], "\n") - 1;
1052
        }
1053
 
1054
        self::$fileOffsets[$file] = $fileOffset;
1055
        file_put_contents($file, $code);
1056
 
1057
        $this->fixReturnStatements($method, $normalizedType);
1058
    }
1059
 
1060
    private static function getUseStatements(string $file): array
1061
    {
1062
        $namespace = '';
1063
        $useMap = [];
1064
        $useOffset = 0;
1065
 
1066
        if (!is_file($file)) {
1067
            return [$namespace, $useOffset, $useMap];
1068
        }
1069
 
1070
        $file = file($file);
1071
 
1072
        for ($i = 0; $i < \count($file); ++$i) {
1073
            if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) {
1074
                break;
1075
            }
1076
 
1077
            if (str_starts_with($file[$i], 'namespace ')) {
1078
                $namespace = substr($file[$i], \strlen('namespace '), -2).'\\';
1079
                $useOffset = $i + 2;
1080
            }
1081
 
1082
            if (str_starts_with($file[$i], 'use ')) {
1083
                $useOffset = $i;
1084
 
1085
                for (; str_starts_with($file[$i], 'use '); ++$i) {
1086
                    $u = explode(' as ', substr($file[$i], 4, -2), 2);
1087
 
1088
                    if (1 === \count($u)) {
1089
                        $p = strrpos($u[0], '\\');
1090
                        $useMap[substr($u[0], false !== $p ? 1 + $p : 0)] = $u[0];
1091
                    } else {
1092
                        $useMap[$u[1]] = $u[0];
1093
                    }
1094
                }
1095
 
1096
                break;
1097
            }
1098
        }
1099
 
1100
        return [$namespace, $useOffset, $useMap];
1101
    }
1102
 
1103
    private function fixReturnStatements(\ReflectionMethod $method, string $returnType)
1104
    {
1105
        if ('docblock' !== $this->patchTypes['force']) {
1106
            if ('7.1' === $this->patchTypes['php'] && 'object' === ltrim($returnType, '?')) {
1107
                return;
1108
            }
1109
 
1110
            if ('7.4' > $this->patchTypes['php'] && $method->hasReturnType()) {
1111
                return;
1112
            }
1113
 
1114
            if ('8.0' > $this->patchTypes['php'] && (str_contains($returnType, '|') || \in_array($returnType, ['mixed', 'static'], true))) {
1115
                return;
1116
            }
1117
 
1118
            if ('8.1' > $this->patchTypes['php'] && str_contains($returnType, '&')) {
1119
                return;
1120
            }
1121
        }
1122
 
1123
        if (!is_file($file = $method->getFileName())) {
1124
            return;
1125
        }
1126
 
1127
        $fixedCode = $code = file($file);
1128
        $i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine();
1129
 
1130
        if ('?' !== $returnType && 'docblock' !== $this->patchTypes['force']) {
1131
            $fixedCode[$i - 1] = preg_replace('/\)(?::[^;\n]++)?(;?\n)/', "): $returnType\\1", $code[$i - 1]);
1132
        }
1133
 
1134
        $end = $method->isGenerator() ? $i : $method->getEndLine();
688 lars 1135
        $inClosure = false;
1136
        $braces = 0;
148 lars 1137
        for (; $i < $end; ++$i) {
688 lars 1138
            if (!$inClosure) {
1139
                $inClosure = str_contains($code[$i], 'function (');
1140
            }
1141
 
1142
            if ($inClosure) {
1143
                $braces += substr_count($code[$i], '{') - substr_count($code[$i], '}');
1144
                $inClosure = $braces > 0;
1145
 
1146
                continue;
1147
            }
1148
 
148 lars 1149
            if ('void' === $returnType) {
1150
                $fixedCode[$i] = str_replace('    return null;', '    return;', $code[$i]);
1151
            } elseif ('mixed' === $returnType || '?' === $returnType[0]) {
1152
                $fixedCode[$i] = str_replace('    return;', '    return null;', $code[$i]);
1153
            } else {
1154
                $fixedCode[$i] = str_replace('    return;', "    return $returnType!?;", $code[$i]);
1155
            }
1156
        }
1157
 
1158
        if ($fixedCode !== $code) {
1159
            file_put_contents($file, $fixedCode);
1160
        }
1161
    }
1162
 
1163
    /**
1164
     * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector
1165
     */
1166
    private function parsePhpDoc(\Reflector $reflector): array
1167
    {
1168
        if (!$doc = $reflector->getDocComment()) {
1169
            return [];
1170
        }
1171
 
1172
        $tagName = '';
1173
        $tagContent = '';
1174
 
1175
        $tags = [];
1176
 
1177
        foreach (explode("\n", substr($doc, 3, -2)) as $line) {
1178
            $line = ltrim($line);
1179
            $line = ltrim($line, '*');
1180
 
1181
            if ('' === $line = trim($line)) {
1182
                if ('' !== $tagName) {
1183
                    $tags[$tagName][] = $tagContent;
1184
                }
1185
                $tagName = $tagContent = '';
1186
                continue;
1187
            }
1188
 
1189
            if ('@' === $line[0]) {
1190
                if ('' !== $tagName) {
1191
                    $tags[$tagName][] = $tagContent;
1192
                    $tagContent = '';
1193
                }
1194
 
1195
                if (preg_match('{^@([-a-zA-Z0-9_:]++)(\s|$)}', $line, $m)) {
1196
                    $tagName = $m[1];
1197
                    $tagContent = str_replace("\t", ' ', ltrim(substr($line, 2 + \strlen($tagName))));
1198
                } else {
1199
                    $tagName = '';
1200
                }
1201
            } elseif ('' !== $tagName) {
1202
                $tagContent .= ' '.str_replace("\t", ' ', $line);
1203
            }
1204
        }
1205
 
1206
        if ('' !== $tagName) {
1207
            $tags[$tagName][] = $tagContent;
1208
        }
1209
 
1210
        foreach ($tags['method'] ?? [] as $i => $method) {
1211
            unset($tags['method'][$i]);
1212
 
1213
            $parts = preg_split('{(\s++|\((?:[^()]*+|(?R))*\)(?: *: *[^ ]++)?|<(?:[^<>]*+|(?R))*>|\{(?:[^{}]*+|(?R))*\})}', $method, -1, \PREG_SPLIT_DELIM_CAPTURE);
1214
            $returnType = '';
1215
            $static = 'static' === $parts[0];
1216
 
1217
            for ($i = $static ? 2 : 0; null !== $p = $parts[$i] ?? null; $i += 2) {
1218
                if (\in_array($p, ['', '|', '&', 'callable'], true) || \in_array(substr($returnType, -1), ['|', '&'], true)) {
1219
                    $returnType .= trim($parts[$i - 1] ?? '').$p;
1220
                    continue;
1221
                }
1222
 
1223
                $signature = '(' === ($parts[$i + 1][0] ?? '(') ? $parts[$i + 1] ?? '()' : null;
1224
 
1225
                if (null === $signature && '' === $returnType) {
1226
                    $returnType = $p;
1227
                    continue;
1228
                }
1229
 
1230
                if ($static && 2 === $i) {
1231
                    $static = false;
1232
                    $returnType = 'static';
1233
                }
1234
 
1235
                if (\in_array($description = trim(implode('', \array_slice($parts, 2 + $i))), ['', '.'], true)) {
1236
                    $description = null;
1237
                } elseif (!preg_match('/[.!]$/', $description)) {
1238
                    $description .= '.';
1239
                }
1240
 
1241
                $tags['method'][$p] = [$static, $returnType, $signature ?? '()', $description];
1242
                break;
1243
            }
1244
        }
1245
 
1246
        foreach ($tags['param'] ?? [] as $i => $param) {
1247
            unset($tags['param'][$i]);
1248
 
1249
            if (\strlen($param) !== strcspn($param, '<{(')) {
1250
                $param = preg_replace('{\(([^()]*+|(?R))*\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\{([^{}]*+|(?R))*\}}', '', $param);
1251
            }
1252
 
1253
            if (false === $i = strpos($param, '$')) {
1254
                continue;
1255
            }
1256
 
1257
            $type = 0 === $i ? '' : rtrim(substr($param, 0, $i), ' &');
1258
            $param = substr($param, 1 + $i, (strpos($param, ' ', $i) ?: (1 + $i + \strlen($param))) - $i - 1);
1259
 
1260
            $tags['param'][$param] = $type;
1261
        }
1262
 
1263
        foreach (['var', 'return'] as $k) {
1264
            if (null === $v = $tags[$k][0] ?? null) {
1265
                continue;
1266
            }
1267
            if (\strlen($v) !== strcspn($v, '<{(')) {
1268
                $v = preg_replace('{\(([^()]*+|(?R))*\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\{([^{}]*+|(?R))*\}}', '', $v);
1269
            }
1270
 
1271
            $tags[$k] = substr($v, 0, strpos($v, ' ') ?: \strlen($v)) ?: null;
1272
        }
1273
 
1274
        return $tags;
1275
    }
1276
}