Passed
Push — develop ( 30cf64...589229 )
by Guillaume
06:18 queued 04:10
created

DebugClassLoader::patchMethod()   F

Complexity

Conditions 27
Paths 3049

Size

Total Lines 95
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 61
nc 3049
nop 4
dl 0
loc 95
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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 Doctrine\Common\Persistence\Proxy as LegacyProxy;
15
use Doctrine\Persistence\Proxy;
16
use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation;
17
use PHPUnit\Framework\MockObject\MockObject;
18
use Prophecy\Prophecy\ProphecySubjectInterface;
19
use ProxyManager\Proxy\ProxyInterface;
20
21
/**
22
 * Autoloader checking if the class is really defined in the file found.
23
 *
24
 * The ClassLoader will wrap all registered autoloaders
25
 * and will throw an exception if a file is found but does
26
 * not declare the class.
27
 *
28
 * It can also patch classes to turn docblocks into actual return types.
29
 * This behavior is controlled by the SYMFONY_PATCH_TYPE_DECLARATIONS env var,
30
 * which is a url-encoded array with the follow parameters:
31
 *  - "force": any value enables deprecation notices - can be any of:
32
 *      - "docblock" to patch only docblock annotations
33
 *      - "object" to turn union types to the "object" type when possible (not recommended)
34
 *      - "1" to add all possible return types including magic methods
35
 *      - "0" to add possible return types excluding magic methods
36
 *  - "php": the target version of PHP - e.g. "7.1" doesn't generate "object" types
37
 *  - "deprecations": "1" to trigger a deprecation notice when a child class misses a
38
 *                    return type while the parent declares an "@return" annotation
39
 *
40
 * Note that patching doesn't care about any coding style so you'd better to run
41
 * php-cs-fixer after, with rules "phpdoc_trim_consecutive_blank_line_separation"
42
 * and "no_superfluous_phpdoc_tags" enabled typically.
43
 *
44
 * @author Fabien Potencier <[email protected]>
45
 * @author Christophe Coevoet <[email protected]>
46
 * @author Nicolas Grekas <[email protected]>
47
 * @author Guilhem Niot <[email protected]>
48
 */
49
class DebugClassLoader
50
{
51
    private const SPECIAL_RETURN_TYPES = [
52
        'mixed' => 'mixed',
53
        'void' => 'void',
54
        'null' => 'null',
55
        'resource' => 'resource',
56
        'static' => 'object',
57
        '$this' => 'object',
58
        'boolean' => 'bool',
59
        'true' => 'bool',
60
        'false' => 'bool',
61
        'integer' => 'int',
62
        'array' => 'array',
63
        'bool' => 'bool',
64
        'callable' => 'callable',
65
        'float' => 'float',
66
        'int' => 'int',
67
        'iterable' => 'iterable',
68
        'object' => 'object',
69
        'string' => 'string',
70
        'self' => 'self',
71
        'parent' => 'parent',
72
    ];
73
74
    private const BUILTIN_RETURN_TYPES = [
75
        'void' => true,
76
        'array' => true,
77
        'bool' => true,
78
        'callable' => true,
79
        'float' => true,
80
        'int' => true,
81
        'iterable' => true,
82
        'object' => true,
83
        'string' => true,
84
        'self' => true,
85
        'parent' => true,
86
    ];
87
88
    private const MAGIC_METHODS = [
89
        '__set' => 'void',
90
        '__isset' => 'bool',
91
        '__unset' => 'void',
92
        '__sleep' => 'array',
93
        '__wakeup' => 'void',
94
        '__toString' => 'string',
95
        '__clone' => 'void',
96
        '__debugInfo' => 'array',
97
        '__serialize' => 'array',
98
        '__unserialize' => 'void',
99
    ];
100
101
    private const INTERNAL_TYPES = [
102
        'ArrayAccess' => [
103
            'offsetExists' => 'bool',
104
            'offsetSet' => 'void',
105
            'offsetUnset' => 'void',
106
        ],
107
        'Countable' => [
108
            'count' => 'int',
109
        ],
110
        'Iterator' => [
111
            'next' => 'void',
112
            'valid' => 'bool',
113
            'rewind' => 'void',
114
        ],
115
        'IteratorAggregate' => [
116
            'getIterator' => '\Traversable',
117
        ],
118
        'OuterIterator' => [
119
            'getInnerIterator' => '\Iterator',
120
        ],
121
        'RecursiveIterator' => [
122
            'hasChildren' => 'bool',
123
        ],
124
        'SeekableIterator' => [
125
            'seek' => 'void',
126
        ],
127
        'Serializable' => [
128
            'serialize' => 'string',
129
            'unserialize' => 'void',
130
        ],
131
        'SessionHandlerInterface' => [
132
            'open' => 'bool',
133
            'close' => 'bool',
134
            'read' => 'string',
135
            'write' => 'bool',
136
            'destroy' => 'bool',
137
            'gc' => 'bool',
138
        ],
139
        'SessionIdInterface' => [
140
            'create_sid' => 'string',
141
        ],
142
        'SessionUpdateTimestampHandlerInterface' => [
143
            'validateId' => 'bool',
144
            'updateTimestamp' => 'bool',
145
        ],
146
        'Throwable' => [
147
            'getMessage' => 'string',
148
            'getCode' => 'int',
149
            'getFile' => 'string',
150
            'getLine' => 'int',
151
            'getTrace' => 'array',
152
            'getPrevious' => '?\Throwable',
153
            'getTraceAsString' => 'string',
154
        ],
155
    ];
156
157
    private $classLoader;
158
    private $isFinder;
159
    private $loaded = [];
160
    private $patchTypes;
161
162
    private static $caseCheck;
163
    private static $checkedClasses = [];
164
    private static $final = [];
165
    private static $finalMethods = [];
166
    private static $deprecated = [];
167
    private static $internal = [];
168
    private static $internalMethods = [];
169
    private static $annotatedParameters = [];
170
    private static $darwinCache = ['/' => ['/', []]];
171
    private static $method = [];
172
    private static $returnTypes = [];
173
    private static $methodTraits = [];
174
    private static $fileOffsets = [];
175
176
    public function __construct(callable $classLoader)
177
    {
178
        $this->classLoader = $classLoader;
179
        $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
180
        parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);
181
        $this->patchTypes += [
182
            'force' => null,
183
            'php' => null,
184
            'deprecations' => false,
185
        ];
186
187
        if (!isset(self::$caseCheck)) {
188
            $file = is_file(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
189
            $i = strrpos($file, \DIRECTORY_SEPARATOR);
190
            $dir = substr($file, 0, 1 + $i);
191
            $file = substr($file, 1 + $i);
192
            $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file);
193
            $test = realpath($dir.$test);
194
195
            if (false === $test || false === $i) {
196
                // filesystem is case sensitive
197
                self::$caseCheck = 0;
198
            } elseif (substr($test, -\strlen($file)) === $file) {
199
                // filesystem is case insensitive and realpath() normalizes the case of characters
200
                self::$caseCheck = 1;
201
            } elseif (false !== stripos(PHP_OS, 'darwin')) {
202
                // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters
203
                self::$caseCheck = 2;
204
            } else {
205
                // filesystem case checks failed, fallback to disabling them
206
                self::$caseCheck = 0;
207
            }
208
        }
209
    }
210
211
    /**
212
     * Gets the wrapped class loader.
213
     *
214
     * @return callable The wrapped class loader
215
     */
216
    public function getClassLoader(): callable
217
    {
218
        return $this->classLoader;
219
    }
220
221
    /**
222
     * Wraps all autoloaders.
223
     */
224
    public static function enable(): void
225
    {
226
        // Ensures we don't hit https://bugs.php.net/42098
227
        class_exists('Symfony\Component\ErrorHandler\ErrorHandler');
228
        class_exists('Psr\Log\LogLevel');
229
230
        if (!\is_array($functions = spl_autoload_functions())) {
231
            return;
232
        }
233
234
        foreach ($functions as $function) {
235
            spl_autoload_unregister($function);
236
        }
237
238
        foreach ($functions as $function) {
239
            if (!\is_array($function) || !$function[0] instanceof self) {
240
                $function = [new static($function), 'loadClass'];
241
            }
242
243
            spl_autoload_register($function);
244
        }
245
    }
246
247
    /**
248
     * Disables the wrapping.
249
     */
250
    public static function disable(): void
251
    {
252
        if (!\is_array($functions = spl_autoload_functions())) {
253
            return;
254
        }
255
256
        foreach ($functions as $function) {
257
            spl_autoload_unregister($function);
258
        }
259
260
        foreach ($functions as $function) {
261
            if (\is_array($function) && $function[0] instanceof self) {
262
                $function = $function[0]->getClassLoader();
263
            }
264
265
            spl_autoload_register($function);
266
        }
267
    }
268
269
    public static function checkClasses(): bool
270
    {
271
        if (!\is_array($functions = spl_autoload_functions())) {
272
            return false;
273
        }
274
275
        $loader = null;
276
277
        foreach ($functions as $function) {
278
            if (\is_array($function) && $function[0] instanceof self) {
279
                $loader = $function[0];
280
                break;
281
            }
282
        }
283
284
        if (null === $loader) {
285
            return false;
286
        }
287
288
        static $offsets = [
289
            'get_declared_interfaces' => 0,
290
            'get_declared_traits' => 0,
291
            'get_declared_classes' => 0,
292
        ];
293
294
        foreach ($offsets as $getSymbols => $i) {
295
            $symbols = $getSymbols();
296
297
            for (; $i < \count($symbols); ++$i) {
298
                if (!is_subclass_of($symbols[$i], MockObject::class)
299
                    && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class)
300
                    && !is_subclass_of($symbols[$i], Proxy::class)
301
                    && !is_subclass_of($symbols[$i], ProxyInterface::class)
302
                    && !is_subclass_of($symbols[$i], LegacyProxy::class)
303
                ) {
304
                    $loader->checkClass($symbols[$i]);
305
                }
306
            }
307
308
            $offsets[$getSymbols] = $i;
309
        }
310
311
        return true;
312
    }
313
314
    public function findFile(string $class): ?string
315
    {
316
        return $this->isFinder ? ($this->classLoader[0]->findFile($class) ?: null) : null;
317
    }
318
319
    /**
320
     * Loads the given class or interface.
321
     *
322
     * @throws \RuntimeException
323
     */
324
    public function loadClass(string $class): void
325
    {
326
        $e = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
327
328
        try {
329
            if ($this->isFinder && !isset($this->loaded[$class])) {
330
                $this->loaded[$class] = true;
331
                if (!$file = $this->classLoader[0]->findFile($class) ?: '') {
332
                    // no-op
333
                } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) {
334
                    include $file;
335
336
                    return;
337
                } elseif (false === include $file) {
338
                    return;
339
                }
340
            } else {
341
                ($this->classLoader)($class);
342
                $file = '';
343
            }
344
        } finally {
345
            error_reporting($e);
346
        }
347
348
        $this->checkClass($class, $file);
349
    }
350
351
    private function checkClass(string $class, string $file = null): void
352
    {
353
        $exists = null === $file || class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false);
354
355
        if (null !== $file && $class && '\\' === $class[0]) {
356
            $class = substr($class, 1);
357
        }
358
359
        if ($exists) {
360
            if (isset(self::$checkedClasses[$class])) {
361
                return;
362
            }
363
            self::$checkedClasses[$class] = true;
364
365
            $refl = new \ReflectionClass($class);
366
            if (null === $file && $refl->isInternal()) {
367
                return;
368
            }
369
            $name = $refl->getName();
370
371
            if ($name !== $class && 0 === strcasecmp($name, $class)) {
372
                throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
373
            }
374
375
            $deprecations = $this->checkAnnotations($refl, $name);
376
377
            foreach ($deprecations as $message) {
378
                @trigger_error($message, E_USER_DEPRECATED);
379
            }
380
        }
381
382
        if (!$file) {
383
            return;
384
        }
385
386
        if (!$exists) {
387
            if (false !== strpos($class, '/')) {
388
                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));
389
            }
390
391
            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));
392
        }
393
394
        if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) {
395
            throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2]));
396
        }
397
    }
398
399
    public function checkAnnotations(\ReflectionClass $refl, string $class): array
400
    {
401
        if (
402
            'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7' === $class
403
            || 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6' === $class
404
        ) {
405
            return [];
406
        }
407
        $deprecations = [];
408
409
        $className = false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
410
411
        // Don't trigger deprecations for classes in the same vendor
412
        if ($class !== $className) {
413
            $vendor = preg_match('/^namespace ([^;\\\\\s]++)[;\\\\]/m', @file_get_contents($refl->getFileName()), $vendor) ? $vendor[1].'\\' : '';
414
            $vendorLen = \strlen($vendor);
415
        } elseif (2 > $vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) {
416
            $vendorLen = 0;
417
            $vendor = '';
418
        } else {
419
            $vendor = str_replace('_', '\\', substr($class, 0, $vendorLen));
420
        }
421
422
        // Detect annotations on the class
423
        if (false !== $doc = $refl->getDocComment()) {
424
            foreach (['final', 'deprecated', 'internal'] as $annotation) {
425
                if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) {
426
                    self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
427
                }
428
            }
429
430
            if ($refl->isInterface() && false !== strpos($doc, 'method') && preg_match_all('#\n \* @method\s+(static\s+)?+([\w\|&\[\]\\\]+\s+)?(\w+(?:\s*\([^\)]*\))?)+(.+?([[:punct:]]\s*)?)?(?=\r?\n \*(?: @|/$|\r?\n))#', $doc, $notice, PREG_SET_ORDER)) {
431
                foreach ($notice as $method) {
432
                    $static = '' !== $method[1] && !empty($method[2]);
433
                    $name = $method[3];
434
                    $description = $method[4] ?? null;
435
                    if (false === strpos($name, '(')) {
436
                        $name .= '()';
437
                    }
438
                    if (null !== $description) {
439
                        $description = trim($description);
440
                        if (!isset($method[5])) {
441
                            $description .= '.';
442
                        }
443
                    }
444
                    self::$method[$class][] = [$class, $name, $static, $description];
445
                }
446
            }
447
        }
448
449
        $parent = get_parent_class($class) ?: null;
450
        $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
451
        if ($parent) {
452
            $parentAndOwnInterfaces[$parent] = $parent;
453
454
            if (!isset(self::$checkedClasses[$parent])) {
455
                $this->checkClass($parent);
456
            }
457
458
            if (isset(self::$final[$parent])) {
459
                $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);
460
            }
461
        }
462
463
        // Detect if the parent is annotated
464
        foreach ($parentAndOwnInterfaces + class_uses($class, false) as $use) {
465
            if (!isset(self::$checkedClasses[$use])) {
466
                $this->checkClass($use);
467
            }
468
            if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class])) {
469
                $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
470
                $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
471
472
                $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $className, $type, $verb, $use, self::$deprecated[$use]);
473
            }
474
            if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) {
475
                $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);
476
            }
477
            if (isset(self::$method[$use])) {
478
                if ($refl->isAbstract()) {
479
                    if (isset(self::$method[$class])) {
480
                        self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]);
481
                    } else {
482
                        self::$method[$class] = self::$method[$use];
483
                    }
484
                } elseif (!$refl->isInterface()) {
485
                    $hasCall = $refl->hasMethod('__call');
486
                    $hasStaticCall = $refl->hasMethod('__callStatic');
487
                    foreach (self::$method[$use] as $method) {
488
                        list($interface, $name, $static, $description) = $method;
489
                        if ($static ? $hasStaticCall : $hasCall) {
490
                            continue;
491
                        }
492
                        $realName = substr($name, 0, strpos($name, '('));
493
                        if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) {
494
                            $deprecations[] = sprintf('Class "%s" should implement method "%s::%s"%s', $className, ($static ? 'static ' : '').$interface, $name, null == $description ? '.' : ': '.$description);
495
                        }
496
                    }
497
                }
498
            }
499
        }
500
501
        if (trait_exists($class)) {
502
            $file = $refl->getFileName();
503
504
            foreach ($refl->getMethods() as $method) {
505
                if ($method->getFileName() === $file) {
506
                    self::$methodTraits[$file][$method->getStartLine()] = $class;
507
                }
508
            }
509
510
            return $deprecations;
511
        }
512
513
        // Inherit @final, @internal, @param and @return annotations for methods
514
        self::$finalMethods[$class] = [];
515
        self::$internalMethods[$class] = [];
516
        self::$annotatedParameters[$class] = [];
517
        self::$returnTypes[$class] = [];
518
        foreach ($parentAndOwnInterfaces as $use) {
519
            foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes'] as $property) {
520
                if (isset(self::${$property}[$use])) {
521
                    self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
522
                }
523
            }
524
525
            if (null !== (self::INTERNAL_TYPES[$use] ?? null)) {
526
                foreach (self::INTERNAL_TYPES[$use] as $method => $returnType) {
527
                    if ('void' !== $returnType) {
528
                        self::$returnTypes[$class] += [$method => [$returnType, $returnType, $class, '']];
529
                    }
530
                }
531
            }
532
        }
533
534
        foreach ($refl->getMethods() as $method) {
535
            if ($method->class !== $class) {
536
                continue;
537
            }
538
539
            if (null === $ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null) {
540
                $ns = $vendor;
541
                $len = $vendorLen;
542
            } elseif (2 > $len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_'))) {
543
                $len = 0;
544
                $ns = '';
545
            } else {
546
                $ns = str_replace('_', '\\', substr($ns, 0, $len));
547
            }
548
549
            if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
550
                list($declaringClass, $message) = self::$finalMethods[$parent][$method->name];
551
                $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);
552
            }
553
554
            if (isset(self::$internalMethods[$class][$method->name])) {
555
                list($declaringClass, $message) = self::$internalMethods[$class][$method->name];
556
                if (strncmp($ns, $declaringClass, $len)) {
557
                    $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);
558
                }
559
            }
560
561
            // To read method annotations
562
            $doc = $method->getDocComment();
563
564
            if (isset(self::$annotatedParameters[$class][$method->name])) {
565
                $definedParameters = [];
566
                foreach ($method->getParameters() as $parameter) {
567
                    $definedParameters[$parameter->name] = true;
568
                }
569
570
                foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) {
571
                    if (!isset($definedParameters[$parameterName]) && !($doc && preg_match("/\\n\\s+\\* @param +((?(?!callable *\().*?|callable *\(.*\).*?))(?<= )\\\${$parameterName}\\b/", $doc))) {
572
                        $deprecations[] = sprintf($deprecation, $className);
573
                    }
574
                }
575
            }
576
577
            $forcePatchTypes = $this->patchTypes['force'];
578
579
            if ($canAddReturnType = null !== $forcePatchTypes && false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) {
580
                if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
581
                    $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock';
582
                }
583
584
                $canAddReturnType = false !== strpos($refl->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR)
585
                    || $refl->isFinal()
586
                    || $method->isFinal()
587
                    || $method->isPrivate()
588
                    || ('' === (self::$internal[$class] ?? null) && !$refl->isAbstract())
589
                    || '' === (self::$final[$class] ?? null)
590
                    || preg_match('/@(final|internal)$/m', $doc)
591
                ;
592
            }
593
594
            if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? self::MAGIC_METHODS[$method->name] ?? null) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
595
                list($normalizedType, $returnType, $declaringClass, $declaringFile) = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType;
596
597
                if ('void' === $normalizedType) {
598
                    $canAddReturnType = false;
599
                }
600
601
                if ($canAddReturnType && 'docblock' !== $this->patchTypes['force']) {
602
                    $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
603
                }
604
605
                if (strncmp($ns, $declaringClass, $len)) {
606
                    if ($canAddReturnType && 'docblock' === $this->patchTypes['force'] && false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) {
607
                        $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
608
                    } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) {
609
                        $deprecations[] = sprintf('Method "%s::%s()" will return "%s" as of its next major version. Doing the same in %s "%s" will be required when upgrading.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className);
610
                    }
611
                }
612
            }
613
614
            if (!$doc) {
615
                $this->patchTypes['force'] = $forcePatchTypes;
616
617
                continue;
618
            }
619
620
            $matches = [];
621
622
            if (!$method->hasReturnType() && ((false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void'))) {
623
                $matches = $matches ?: [1 => self::MAGIC_METHODS[$method->name]];
624
                $this->setReturnType($matches[1], $method, $parent);
625
626
                if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
627
                    $this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
628
                }
629
630
                if ($method->isPrivate()) {
631
                    unset(self::$returnTypes[$class][$method->name]);
632
                }
633
            }
634
635
            $this->patchTypes['force'] = $forcePatchTypes;
636
637
            if ($method->isPrivate()) {
638
                continue;
639
            }
640
641
            $finalOrInternal = false;
642
643
            foreach (['final', 'internal'] as $annotation) {
644
                if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) {
645
                    $message = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
646
                    self::${$annotation.'Methods'}[$class][$method->name] = [$class, $message];
647
                    $finalOrInternal = true;
648
                }
649
            }
650
651
            if ($finalOrInternal || $method->isConstructor() || false === strpos($doc, '@param') || StatelessInvocation::class === $class) {
652
                continue;
653
            }
654
            if (!preg_match_all('#\n\s+\* @param +((?(?!callable *\().*?|callable *\(.*\).*?))(?<= )\$([a-zA-Z0-9_\x7f-\xff]++)#', $doc, $matches, PREG_SET_ORDER)) {
655
                continue;
656
            }
657
            if (!isset(self::$annotatedParameters[$class][$method->name])) {
658
                $definedParameters = [];
659
                foreach ($method->getParameters() as $parameter) {
660
                    $definedParameters[$parameter->name] = true;
661
                }
662
            }
663
            foreach ($matches as list(, $parameterType, $parameterName)) {
664
                if (!isset($definedParameters[$parameterName])) {
665
                    $parameterType = trim($parameterType);
666
                    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);
667
                }
668
            }
669
        }
670
671
        return $deprecations;
672
    }
673
674
    public function checkCase(\ReflectionClass $refl, string $file, string $class): ?array
675
    {
676
        $real = explode('\\', $class.strrchr($file, '.'));
677
        $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file));
678
679
        $i = \count($tail) - 1;
680
        $j = \count($real) - 1;
681
682
        while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) {
683
            --$i;
684
            --$j;
685
        }
686
687
        array_splice($tail, 0, $i + 1);
688
689
        if (!$tail) {
690
            return null;
691
        }
692
693
        $tail = \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $tail);
694
        $tailLen = \strlen($tail);
695
        $real = $refl->getFileName();
696
697
        if (2 === self::$caseCheck) {
698
            $real = $this->darwinRealpath($real);
699
        }
700
701
        if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true)
702
            && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false)
703
        ) {
704
            return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)];
705
        }
706
707
        return null;
708
    }
709
710
    /**
711
     * `realpath` on MacOSX doesn't normalize the case of characters.
712
     */
713
    private function darwinRealpath(string $real): string
714
    {
715
        $i = 1 + strrpos($real, '/');
716
        $file = substr($real, $i);
717
        $real = substr($real, 0, $i);
718
719
        if (isset(self::$darwinCache[$real])) {
720
            $kDir = $real;
721
        } else {
722
            $kDir = strtolower($real);
723
724
            if (isset(self::$darwinCache[$kDir])) {
725
                $real = self::$darwinCache[$kDir][0];
726
            } else {
727
                $dir = getcwd();
728
729
                if (!@chdir($real)) {
730
                    return $real.$file;
731
                }
732
733
                $real = getcwd().'/';
734
                chdir($dir);
735
736
                $dir = $real;
737
                $k = $kDir;
738
                $i = \strlen($dir) - 1;
739
                while (!isset(self::$darwinCache[$k])) {
740
                    self::$darwinCache[$k] = [$dir, []];
741
                    self::$darwinCache[$dir] = &self::$darwinCache[$k];
742
743
                    while ('/' !== $dir[--$i]) {
744
                    }
745
                    $k = substr($k, 0, ++$i);
746
                    $dir = substr($dir, 0, $i--);
747
                }
748
            }
749
        }
750
751
        $dirFiles = self::$darwinCache[$kDir][1];
752
753
        if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) {
754
            // Get the file name from "file_name.php(123) : eval()'d code"
755
            $file = substr($file, 0, strrpos($file, '(', -17));
756
        }
757
758
        if (isset($dirFiles[$file])) {
759
            return $real.$dirFiles[$file];
760
        }
761
762
        $kFile = strtolower($file);
763
764
        if (!isset($dirFiles[$kFile])) {
765
            foreach (scandir($real, 2) as $f) {
766
                if ('.' !== $f[0]) {
767
                    $dirFiles[$f] = $f;
768
                    if ($f === $file) {
769
                        $kFile = $k = $file;
770
                    } elseif ($f !== $k = strtolower($f)) {
771
                        $dirFiles[$k] = $f;
772
                    }
773
                }
774
            }
775
            self::$darwinCache[$kDir][1] = $dirFiles;
776
        }
777
778
        return $real.$dirFiles[$kFile];
779
    }
780
781
    /**
782
     * `class_implements` includes interfaces from the parents so we have to manually exclude them.
783
     *
784
     * @return string[]
785
     */
786
    private function getOwnInterfaces(string $class, ?string $parent): array
787
    {
788
        $ownInterfaces = class_implements($class, false);
789
790
        if ($parent) {
791
            foreach (class_implements($parent, false) as $interface) {
792
                unset($ownInterfaces[$interface]);
793
            }
794
        }
795
796
        foreach ($ownInterfaces as $interface) {
797
            foreach (class_implements($interface) as $interface) {
798
                unset($ownInterfaces[$interface]);
799
            }
800
        }
801
802
        return $ownInterfaces;
803
    }
804
805
    private function setReturnType(string $types, \ReflectionMethod $method, ?string $parent): void
806
    {
807
        $nullable = false;
808
        $typesMap = [];
809
        foreach (explode('|', $types) as $t) {
810
            $typesMap[$this->normalizeType($t, $method->class, $parent)] = $t;
811
        }
812
813
        if (isset($typesMap['array'])) {
814
            if (isset($typesMap['Traversable']) || isset($typesMap['\Traversable'])) {
815
                $typesMap['iterable'] = 'array' !== $typesMap['array'] ? $typesMap['array'] : 'iterable';
816
                unset($typesMap['array'], $typesMap['Traversable'], $typesMap['\Traversable']);
817
            } elseif ('array' !== $typesMap['array'] && isset(self::$returnTypes[$method->class][$method->name])) {
818
                return;
819
            }
820
        }
821
822
        if (isset($typesMap['array']) && isset($typesMap['iterable'])) {
823
            if ('[]' === substr($typesMap['array'], -2)) {
824
                $typesMap['iterable'] = $typesMap['array'];
825
            }
826
            unset($typesMap['array']);
827
        }
828
829
        $iterable = $object = true;
830
        foreach ($typesMap as $n => $t) {
831
            if ('null' !== $n) {
832
                $iterable = $iterable && (\in_array($n, ['array', 'iterable']) || false !== strpos($n, 'Iterator'));
833
                $object = $object && (\in_array($n, ['callable', 'object', '$this', 'static']) || !isset(self::SPECIAL_RETURN_TYPES[$n]));
834
            }
835
        }
836
837
        $normalizedType = key($typesMap);
838
        $returnType = current($typesMap);
839
840
        foreach ($typesMap as $n => $t) {
841
            if ('null' === $n) {
842
                $nullable = true;
843
            } elseif ('null' === $normalizedType) {
844
                $normalizedType = $t;
845
                $returnType = $t;
846
            } elseif ($n !== $normalizedType || !preg_match('/^\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $n)) {
847
                if ($iterable) {
848
                    $normalizedType = $returnType = 'iterable';
849
                } elseif ($object && 'object' === $this->patchTypes['force']) {
850
                    $normalizedType = $returnType = 'object';
851
                } else {
852
                    // ignore multi-types return declarations
853
                    return;
854
                }
855
            }
856
        }
857
858
        if ('void' === $normalizedType) {
859
            $nullable = false;
860
        } elseif (!isset(self::BUILTIN_RETURN_TYPES[$normalizedType]) && isset(self::SPECIAL_RETURN_TYPES[$normalizedType])) {
861
            // ignore other special return types
862
            return;
863
        }
864
865
        if ($nullable) {
866
            $normalizedType = '?'.$normalizedType;
867
            $returnType .= '|null';
868
        }
869
870
        self::$returnTypes[$method->class][$method->name] = [$normalizedType, $returnType, $method->class, $method->getFileName()];
871
    }
872
873
    private function normalizeType(string $type, string $class, ?string $parent): string
874
    {
875
        if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) {
876
            if ('parent' === $lcType = self::SPECIAL_RETURN_TYPES[$lcType]) {
877
                $lcType = null !== $parent ? '\\'.$parent : 'parent';
878
            } elseif ('self' === $lcType) {
879
                $lcType = '\\'.$class;
880
            }
881
882
            return $lcType;
883
        }
884
885
        if ('[]' === substr($type, -2)) {
886
            return 'array';
887
        }
888
889
        if (preg_match('/^(array|iterable|callable) *[<(]/', $lcType, $m)) {
890
            return $m[1];
891
        }
892
893
        // We could resolve "use" statements to return the FQDN
894
        // but this would be too expensive for a runtime checker
895
896
        return $type;
897
    }
898
899
    /**
900
     * Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
901
     */
902
    private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
903
    {
904
        static $patchedMethods = [];
905
        static $useStatements = [];
906
907
        if (!is_file($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) {
908
            return;
909
        }
910
911
        $patchedMethods[$file][$startLine] = true;
912
        $fileOffset = self::$fileOffsets[$file] ?? 0;
913
        $startLine += $fileOffset - 2;
914
        $nullable = '?' === $normalizedType[0] ? '?' : '';
915
        $normalizedType = ltrim($normalizedType, '?');
916
        $returnType = explode('|', $returnType);
917
        $code = file($file);
918
919
        foreach ($returnType as $i => $type) {
920
            if (preg_match('/((?:\[\])+)$/', $type, $m)) {
921
                $type = substr($type, 0, -\strlen($m[1]));
922
                $format = '%s'.$m[1];
923
            } elseif (preg_match('/^(array|iterable)<([^,>]++)>$/', $type, $m)) {
924
                $type = $m[2];
925
                $format = $m[1].'<%s>';
926
            } else {
927
                $format = null;
928
            }
929
930
            if (isset(self::SPECIAL_RETURN_TYPES[$type]) || ('\\' === $type[0] && !$p = strrpos($type, '\\', 1))) {
931
                continue;
932
            }
933
934
            list($namespace, $useOffset, $useMap) = $useStatements[$file] ?? $useStatements[$file] = self::getUseStatements($file);
935
936
            if ('\\' !== $type[0]) {
937
                list($declaringNamespace, , $declaringUseMap) = $useStatements[$declaringFile] ?? $useStatements[$declaringFile] = self::getUseStatements($declaringFile);
938
939
                $p = strpos($type, '\\', 1);
940
                $alias = $p ? substr($type, 0, $p) : $type;
941
942
                if (isset($declaringUseMap[$alias])) {
943
                    $type = '\\'.$declaringUseMap[$alias].($p ? substr($type, $p) : '');
944
                } else {
945
                    $type = '\\'.$declaringNamespace.$type;
946
                }
947
948
                $p = strrpos($type, '\\', 1);
949
            }
950
951
            $alias = substr($type, 1 + $p);
952
            $type = substr($type, 1);
953
954
            if (!isset($useMap[$alias]) && (class_exists($c = $namespace.$alias) || interface_exists($c) || trait_exists($c))) {
955
                $useMap[$alias] = $c;
956
            }
957
958
            if (!isset($useMap[$alias])) {
959
                $useStatements[$file][2][$alias] = $type;
960
                $code[$useOffset] = "use $type;\n".$code[$useOffset];
961
                ++$fileOffset;
962
            } elseif ($useMap[$alias] !== $type) {
963
                $alias .= 'FIXME';
964
                $useStatements[$file][2][$alias] = $type;
965
                $code[$useOffset] = "use $type as $alias;\n".$code[$useOffset];
966
                ++$fileOffset;
967
            }
968
969
            $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;
970
971
            if (!isset(self::SPECIAL_RETURN_TYPES[$normalizedType]) && !isset(self::SPECIAL_RETURN_TYPES[$returnType[$i]])) {
972
                $normalizedType = $returnType[$i];
973
            }
974
        }
975
976
        if ('docblock' === $this->patchTypes['force'] || ('object' === $normalizedType && '7.1' === $this->patchTypes['php'])) {
977
            $returnType = implode('|', $returnType);
978
979
            if ($method->getDocComment()) {
980
                $code[$startLine] = "     * @return $returnType\n".$code[$startLine];
981
            } else {
982
                $code[$startLine] .= <<<EOTXT
983
    /**
984
     * @return $returnType
985
     */
986
987
EOTXT;
988
            }
989
990
            $fileOffset += substr_count($code[$startLine], "\n") - 1;
991
        }
992
993
        self::$fileOffsets[$file] = $fileOffset;
994
        file_put_contents($file, $code);
995
996
        $this->fixReturnStatements($method, $nullable.$normalizedType);
997
    }
998
999
    private static function getUseStatements(string $file): array
1000
    {
1001
        $namespace = '';
1002
        $useMap = [];
1003
        $useOffset = 0;
1004
1005
        if (!is_file($file)) {
1006
            return [$namespace, $useOffset, $useMap];
1007
        }
1008
1009
        $file = file($file);
1010
1011
        for ($i = 0; $i < \count($file); ++$i) {
1012
            if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) {
1013
                break;
1014
            }
1015
1016
            if (0 === strpos($file[$i], 'namespace ')) {
1017
                $namespace = substr($file[$i], \strlen('namespace '), -2).'\\';
1018
                $useOffset = $i + 2;
1019
            }
1020
1021
            if (0 === strpos($file[$i], 'use ')) {
1022
                $useOffset = $i;
1023
1024
                for (; 0 === strpos($file[$i], 'use '); ++$i) {
1025
                    $u = explode(' as ', substr($file[$i], 4, -2), 2);
1026
1027
                    if (1 === \count($u)) {
1028
                        $p = strrpos($u[0], '\\');
1029
                        $useMap[substr($u[0], false !== $p ? 1 + $p : 0)] = $u[0];
1030
                    } else {
1031
                        $useMap[$u[1]] = $u[0];
1032
                    }
1033
                }
1034
1035
                break;
1036
            }
1037
        }
1038
1039
        return [$namespace, $useOffset, $useMap];
1040
    }
1041
1042
    private function fixReturnStatements(\ReflectionMethod $method, string $returnType)
1043
    {
1044
        if ('7.1' === $this->patchTypes['php'] && 'object' === ltrim($returnType, '?') && 'docblock' !== $this->patchTypes['force']) {
1045
            return;
1046
        }
1047
1048
        if (!is_file($file = $method->getFileName())) {
1049
            return;
1050
        }
1051
1052
        $fixedCode = $code = file($file);
1053
        $i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine();
1054
1055
        if ('?' !== $returnType && 'docblock' !== $this->patchTypes['force']) {
1056
            $fixedCode[$i - 1] = preg_replace('/\)(;?\n)/', "): $returnType\\1", $code[$i - 1]);
1057
        }
1058
1059
        $end = $method->isGenerator() ? $i : $method->getEndLine();
1060
        for (; $i < $end; ++$i) {
1061
            if ('void' === $returnType) {
1062
                $fixedCode[$i] = str_replace('    return null;', '    return;', $code[$i]);
1063
            } elseif ('mixed' === $returnType || '?' === $returnType[0]) {
1064
                $fixedCode[$i] = str_replace('    return;', '    return null;', $code[$i]);
1065
            } else {
1066
                $fixedCode[$i] = str_replace('    return;', "    return $returnType!?;", $code[$i]);
1067
            }
1068
        }
1069
1070
        if ($fixedCode !== $code) {
1071
            file_put_contents($file, $fixedCode);
1072
        }
1073
    }
1074
}
1075