PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / symfony / error-handler / DebugClassLoader.php
matomo / app / vendor / prefixed / symfony / error-handler Last commit date
Error 1 year ago ErrorEnhancer 1 year ago ErrorRenderer 1 year ago Exception 1 year ago Internal 2 years ago Resources 1 year ago BufferingLogger.php 1 year ago Debug.php 1 year ago DebugClassLoader.php 1 year ago ErrorHandler.php 1 year ago LICENSE 2 years ago README.md 2 years ago ThrowableUtils.php 2 years ago
DebugClassLoader.php
943 lines
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 namespace Matomo\Dependencies\Symfony\Component\ErrorHandler;
12
13 use Composer\InstalledVersions;
14 use Doctrine\Common\Persistence\Proxy as LegacyProxy;
15 use Doctrine\Persistence\Proxy;
16 use Mockery\MockInterface;
17 use Phake\IMock;
18 use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation;
19 use PHPUnit\Framework\MockObject\MockObject;
20 use Prophecy\Prophecy\ProphecySubjectInterface;
21 use ProxyManager\Proxy\ProxyInterface;
22 use Matomo\Dependencies\Symfony\Component\ErrorHandler\Internal\TentativeTypes;
23 use Symfony\Component\HttpClient\HttplugClient;
24 /**
25 * Autoloader checking if the class is really defined in the file found.
26 *
27 * The ClassLoader will wrap all registered autoloaders
28 * and will throw an exception if a file is found but does
29 * not declare the class.
30 *
31 * It can also patch classes to turn docblocks into actual return types.
32 * This behavior is controlled by the SYMFONY_PATCH_TYPE_DECLARATIONS env var,
33 * which is a url-encoded array with the follow parameters:
34 * - "force": any value enables deprecation notices - can be any of:
35 * - "phpdoc" to patch only docblock annotations
36 * - "2" to add all possible return types
37 * - "1" to add return types but only to tests/final/internal/private methods
38 * - "php": the target version of PHP - e.g. "7.1" doesn't generate "object" types
39 * - "deprecations": "1" to trigger a deprecation notice when a child class misses a
40 * return type while the parent declares an "@return" annotation
41 *
42 * Note that patching doesn't care about any coding style so you'd better to run
43 * php-cs-fixer after, with rules "phpdoc_trim_consecutive_blank_line_separation"
44 * and "no_superfluous_phpdoc_tags" enabled typically.
45 *
46 * @author Fabien Potencier <fabien@symfony.com>
47 * @author Christophe Coevoet <stof@notk.org>
48 * @author Nicolas Grekas <p@tchwork.com>
49 * @author Guilhem Niot <guilhem.niot@gmail.com>
50 */
51 class DebugClassLoader
52 {
53 private const SPECIAL_RETURN_TYPES = ['void' => 'void', 'null' => 'null', 'resource' => 'resource', 'boolean' => 'bool', 'true' => 'true', 'false' => 'false', 'integer' => 'int', 'array' => 'array', 'bool' => 'bool', 'callable' => 'callable', 'float' => 'float', 'int' => 'int', 'iterable' => 'iterable', 'object' => 'object', 'string' => 'string', 'self' => 'self', 'parent' => 'parent', 'mixed' => 'mixed', 'static' => 'static', '$this' => 'static', 'list' => 'array', 'class-string' => 'string', 'never' => 'never'];
54 private const BUILTIN_RETURN_TYPES = ['void' => \true, 'array' => \true, 'false' => \true, 'bool' => \true, 'callable' => \true, 'float' => \true, 'int' => \true, 'iterable' => \true, 'object' => \true, 'string' => \true, 'self' => \true, 'parent' => \true, 'mixed' => \true, 'static' => \true, 'null' => \true, 'true' => \true, 'never' => \true];
55 private const MAGIC_METHODS = ['__isset' => 'bool', '__sleep' => 'array', '__toString' => 'string', '__debugInfo' => 'array', '__serialize' => 'array'];
56 private $classLoader;
57 private $isFinder;
58 private $loaded = [];
59 private $patchTypes;
60 private static $caseCheck;
61 private static $checkedClasses = [];
62 private static $final = [];
63 private static $finalMethods = [];
64 private static $deprecated = [];
65 private static $internal = [];
66 private static $internalMethods = [];
67 private static $annotatedParameters = [];
68 private static $darwinCache = ['/' => ['/', []]];
69 private static $method = [];
70 private static $returnTypes = [];
71 private static $methodTraits = [];
72 private static $fileOffsets = [];
73 public function __construct(callable $classLoader)
74 {
75 $this->classLoader = $classLoader;
76 $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
77 parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);
78 $this->patchTypes += ['force' => null, 'php' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, 'deprecations' => \PHP_VERSION_ID >= 70400];
79 if ('phpdoc' === $this->patchTypes['force']) {
80 $this->patchTypes['force'] = 'docblock';
81 }
82 if (!isset(self::$caseCheck)) {
83 $file = is_file(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
84 $i = strrpos($file, \DIRECTORY_SEPARATOR);
85 $dir = substr($file, 0, 1 + $i);
86 $file = substr($file, 1 + $i);
87 $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file);
88 $test = realpath($dir . $test);
89 if (\false === $test || \false === $i) {
90 // filesystem is case sensitive
91 self::$caseCheck = 0;
92 } elseif (substr($test, -\strlen($file)) === $file) {
93 // filesystem is case insensitive and realpath() normalizes the case of characters
94 self::$caseCheck = 1;
95 } elseif ('Darwin' === \PHP_OS_FAMILY) {
96 // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters
97 self::$caseCheck = 2;
98 } else {
99 // filesystem case checks failed, fallback to disabling them
100 self::$caseCheck = 0;
101 }
102 }
103 }
104 public function getClassLoader() : callable
105 {
106 return $this->classLoader;
107 }
108 /**
109 * Wraps all autoloaders.
110 */
111 public static function enable() : void
112 {
113 // Ensures we don't hit https://bugs.php.net/42098
114 class_exists(\Matomo\Dependencies\Symfony\Component\ErrorHandler\ErrorHandler::class);
115 class_exists(\Matomo\Dependencies\Psr\Log\LogLevel::class);
116 if (!\is_array($functions = spl_autoload_functions())) {
117 return;
118 }
119 foreach ($functions as $function) {
120 spl_autoload_unregister($function);
121 }
122 foreach ($functions as $function) {
123 if (!\is_array($function) || !$function[0] instanceof self) {
124 $function = [new static($function), 'loadClass'];
125 }
126 spl_autoload_register($function);
127 }
128 }
129 /**
130 * Disables the wrapping.
131 */
132 public static function disable() : void
133 {
134 if (!\is_array($functions = spl_autoload_functions())) {
135 return;
136 }
137 foreach ($functions as $function) {
138 spl_autoload_unregister($function);
139 }
140 foreach ($functions as $function) {
141 if (\is_array($function) && $function[0] instanceof self) {
142 $function = $function[0]->getClassLoader();
143 }
144 spl_autoload_register($function);
145 }
146 }
147 public static function checkClasses() : bool
148 {
149 if (!\is_array($functions = spl_autoload_functions())) {
150 return \false;
151 }
152 $loader = null;
153 foreach ($functions as $function) {
154 if (\is_array($function) && $function[0] instanceof self) {
155 $loader = $function[0];
156 break;
157 }
158 }
159 if (null === $loader) {
160 return \false;
161 }
162 static $offsets = ['get_declared_interfaces' => 0, 'get_declared_traits' => 0, 'get_declared_classes' => 0];
163 foreach ($offsets as $getSymbols => $i) {
164 $symbols = $getSymbols();
165 for (; $i < \count($symbols); ++$i) {
166 if (!is_subclass_of($symbols[$i], MockObject::class) && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) && !is_subclass_of($symbols[$i], Proxy::class) && !is_subclass_of($symbols[$i], ProxyInterface::class) && !is_subclass_of($symbols[$i], LegacyProxy::class) && !is_subclass_of($symbols[$i], MockInterface::class) && !is_subclass_of($symbols[$i], IMock::class)) {
167 $loader->checkClass($symbols[$i]);
168 }
169 }
170 $offsets[$getSymbols] = $i;
171 }
172 return \true;
173 }
174 public function findFile(string $class) : ?string
175 {
176 return $this->isFinder ? $this->classLoader[0]->findFile($class) ?: null : null;
177 }
178 /**
179 * Loads the given class or interface.
180 *
181 * @throws \RuntimeException
182 */
183 public function loadClass(string $class) : void
184 {
185 $e = error_reporting(error_reporting() | \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR);
186 try {
187 if ($this->isFinder && !isset($this->loaded[$class])) {
188 $this->loaded[$class] = \true;
189 if (!($file = $this->classLoader[0]->findFile($class) ?: '')) {
190 // no-op
191 } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) {
192 include $file;
193 return;
194 } elseif (\false === (include $file)) {
195 return;
196 }
197 } else {
198 ($this->classLoader)($class);
199 $file = '';
200 }
201 } finally {
202 error_reporting($e);
203 }
204 $this->checkClass($class, $file);
205 }
206 private function checkClass(string $class, ?string $file = null) : void
207 {
208 $exists = null === $file || class_exists($class, \false) || interface_exists($class, \false) || trait_exists($class, \false);
209 if (null !== $file && $class && '\\' === $class[0]) {
210 $class = substr($class, 1);
211 }
212 if ($exists) {
213 if (isset(self::$checkedClasses[$class])) {
214 return;
215 }
216 self::$checkedClasses[$class] = \true;
217 $refl = new \ReflectionClass($class);
218 if (null === $file && $refl->isInternal()) {
219 return;
220 }
221 $name = $refl->getName();
222 if ($name !== $class && 0 === strcasecmp($name, $class)) {
223 throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
224 }
225 $deprecations = $this->checkAnnotations($refl, $name);
226 foreach ($deprecations as $message) {
227 @trigger_error($message, \E_USER_DEPRECATED);
228 }
229 }
230 if (!$file) {
231 return;
232 }
233 if (!$exists) {
234 if (\false !== strpos($class, '/')) {
235 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));
236 }
237 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));
238 }
239 if (self::$caseCheck && ($message = $this->checkCase($refl, $file, $class))) {
240 throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2]));
241 }
242 }
243 public function checkAnnotations(\ReflectionClass $refl, string $class) : array
244 {
245 if ('Symfony\\Bridge\\PhpUnit\\Legacy\\SymfonyTestsListenerForV7' === $class || 'Symfony\\Bridge\\PhpUnit\\Legacy\\SymfonyTestsListenerForV6' === $class) {
246 return [];
247 }
248 $deprecations = [];
249 $className = \false !== strpos($class, "@anonymous\x00") ? ((get_parent_class($class) ?: key(class_implements($class))) ?: 'class') . '@anonymous' : $class;
250 // Don't trigger deprecations for classes in the same vendor
251 if ($class !== $className) {
252 $vendor = preg_match('/^namespace ([^;\\\\\\s]++)[;\\\\]/m', @file_get_contents($refl->getFileName()), $vendor) ? $vendor[1] . '\\' : '';
253 $vendorLen = \strlen($vendor);
254 } elseif (2 > ($vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_')))) {
255 $vendorLen = 0;
256 $vendor = '';
257 } else {
258 $vendor = str_replace('_', '\\', substr($class, 0, $vendorLen));
259 }
260 $parent = get_parent_class($class) ?: null;
261 self::$returnTypes[$class] = [];
262 $classIsTemplate = \false;
263 // Detect annotations on the class
264 if ($doc = $this->parsePhpDoc($refl)) {
265 $classIsTemplate = isset($doc['template']);
266 foreach (['final', 'deprecated', 'internal'] as $annotation) {
267 if (null !== ($description = $doc[$annotation][0] ?? null)) {
268 self::${$annotation}[$class] = '' !== $description ? ' ' . $description . (preg_match('/[.!]$/', $description) ? '' : '.') : '.';
269 }
270 }
271 if ($refl->isInterface() && isset($doc['method'])) {
272 foreach ($doc['method'] as $name => [$static, $returnType, $signature, $description]) {
273 self::$method[$class][] = [$class, $static, $returnType, $name . $signature, $description];
274 if ('' !== $returnType) {
275 $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent);
276 }
277 }
278 }
279 }
280 $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
281 if ($parent) {
282 $parentAndOwnInterfaces[$parent] = $parent;
283 if (!isset(self::$checkedClasses[$parent])) {
284 $this->checkClass($parent);
285 }
286 if (isset(self::$final[$parent])) {
287 $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);
288 }
289 }
290 // Detect if the parent is annotated
291 foreach ($parentAndOwnInterfaces + class_uses($class, \false) as $use) {
292 if (!isset(self::$checkedClasses[$use])) {
293 $this->checkClass($use);
294 }
295 if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class]) && !(HttplugClient::class === $class && \in_array($use, [\Http\Client\HttpClient::class, \Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], \true))) {
296 $type = class_exists($class, \false) ? 'class' : (interface_exists($class, \false) ? 'interface' : 'trait');
297 $verb = class_exists($use, \false) || interface_exists($class, \false) ? 'extends' : (interface_exists($use, \false) ? 'implements' : 'uses');
298 $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]);
299 }
300 if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) {
301 $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);
302 }
303 if (isset(self::$method[$use])) {
304 if ($refl->isAbstract()) {
305 if (isset(self::$method[$class])) {
306 self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]);
307 } else {
308 self::$method[$class] = self::$method[$use];
309 }
310 } elseif (!$refl->isInterface()) {
311 if (!strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && 0 === strpos($className, 'Symfony\\') && (!class_exists(InstalledVersions::class) || 'symfony/symfony' !== InstalledVersions::getRootPackage()['name'])) {
312 // skip "same vendor" @method deprecations for Symfony\* classes unless symfony/symfony is being tested
313 continue;
314 }
315 $hasCall = $refl->hasMethod('__call');
316 $hasStaticCall = $refl->hasMethod('__callStatic');
317 foreach (self::$method[$use] as [$interface, $static, $returnType, $name, $description]) {
318 if ($static ? $hasStaticCall : $hasCall) {
319 continue;
320 }
321 $realName = substr($name, 0, strpos($name, '('));
322 if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || $static && !$methodRefl->isStatic() || !$static && $methodRefl->isStatic()) {
323 $deprecations[] = sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '') . $interface, $name, $returnType ? ': ' . $returnType : '', null === $description ? '.' : ': ' . $description);
324 }
325 }
326 }
327 }
328 }
329 if (trait_exists($class)) {
330 $file = $refl->getFileName();
331 foreach ($refl->getMethods() as $method) {
332 if ($method->getFileName() === $file) {
333 self::$methodTraits[$file][$method->getStartLine()] = $class;
334 }
335 }
336 return $deprecations;
337 }
338 // Inherit @final, @internal, @param and @return annotations for methods
339 self::$finalMethods[$class] = [];
340 self::$internalMethods[$class] = [];
341 self::$annotatedParameters[$class] = [];
342 foreach ($parentAndOwnInterfaces as $use) {
343 foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes'] as $property) {
344 if (isset(self::${$property}[$use])) {
345 self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
346 }
347 }
348 if (null !== (TentativeTypes::RETURN_TYPES[$use] ?? null)) {
349 foreach (TentativeTypes::RETURN_TYPES[$use] as $method => $returnType) {
350 $returnType = explode('|', $returnType);
351 foreach ($returnType as $i => $t) {
352 if ('?' !== $t && !isset(self::BUILTIN_RETURN_TYPES[$t])) {
353 $returnType[$i] = '\\' . $t;
354 }
355 }
356 $returnType = implode('|', $returnType);
357 self::$returnTypes[$class] += [$method => [$returnType, 0 === strpos($returnType, '?') ? substr($returnType, 1) . '|null' : $returnType, $use, '']];
358 }
359 }
360 }
361 foreach ($refl->getMethods() as $method) {
362 if ($method->class !== $class) {
363 continue;
364 }
365 if (null === ($ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null)) {
366 $ns = $vendor;
367 $len = $vendorLen;
368 } elseif (2 > ($len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_')))) {
369 $len = 0;
370 $ns = '';
371 } else {
372 $ns = str_replace('_', '\\', substr($ns, 0, $len));
373 }
374 if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
375 [$declaringClass, $message] = self::$finalMethods[$parent][$method->name];
376 $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);
377 }
378 if (isset(self::$internalMethods[$class][$method->name])) {
379 [$declaringClass, $message] = self::$internalMethods[$class][$method->name];
380 if (strncmp($ns, $declaringClass, $len)) {
381 $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);
382 }
383 }
384 // To read method annotations
385 $doc = $this->parsePhpDoc($method);
386 if (($classIsTemplate || isset($doc['template'])) && $method->hasReturnType()) {
387 unset($doc['return']);
388 }
389 if (isset(self::$annotatedParameters[$class][$method->name])) {
390 $definedParameters = [];
391 foreach ($method->getParameters() as $parameter) {
392 $definedParameters[$parameter->name] = \true;
393 }
394 foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) {
395 if (!isset($definedParameters[$parameterName]) && !isset($doc['param'][$parameterName])) {
396 $deprecations[] = sprintf($deprecation, $className);
397 }
398 }
399 }
400 $forcePatchTypes = $this->patchTypes['force'];
401 if ($canAddReturnType = null !== $forcePatchTypes && \false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR . 'vendor' . \DIRECTORY_SEPARATOR)) {
402 if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
403 $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock';
404 }
405 $canAddReturnType = 2 === (int) $forcePatchTypes || \false !== stripos($method->getFileName(), \DIRECTORY_SEPARATOR . 'Tests' . \DIRECTORY_SEPARATOR) || $refl->isFinal() || $method->isFinal() || $method->isPrivate() || '.' === (self::$internal[$class] ?? null) && !$refl->isAbstract() || '.' === (self::$final[$class] ?? null) || '' === ($doc['final'][0] ?? null) || '' === ($doc['internal'][0] ?? null);
406 }
407 if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? null) && 'docblock' === $this->patchTypes['force'] && !$method->hasReturnType() && isset(TentativeTypes::RETURN_TYPES[$returnType[2]][$method->name])) {
408 $this->patchReturnTypeWillChange($method);
409 }
410 if (null !== ($returnType ?? ($returnType = self::MAGIC_METHODS[$method->name] ?? null)) && !$method->hasReturnType() && !isset($doc['return'])) {
411 [$normalizedType, $returnType, $declaringClass, $declaringFile] = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType;
412 if ($canAddReturnType && 'docblock' !== $this->patchTypes['force']) {
413 $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
414 }
415 if (!isset($doc['deprecated']) && strncmp($ns, $declaringClass, $len)) {
416 if ('docblock' === $this->patchTypes['force']) {
417 $this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
418 } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) {
419 $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);
420 }
421 }
422 }
423 if (!$doc) {
424 $this->patchTypes['force'] = $forcePatchTypes;
425 continue;
426 }
427 if (isset($doc['return']) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
428 $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType());
429 if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
430 $this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
431 }
432 if ($method->isPrivate()) {
433 unset(self::$returnTypes[$class][$method->name]);
434 }
435 }
436 $this->patchTypes['force'] = $forcePatchTypes;
437 if ($method->isPrivate()) {
438 continue;
439 }
440 $finalOrInternal = \false;
441 foreach (['final', 'internal'] as $annotation) {
442 if (null !== ($description = $doc[$annotation][0] ?? null)) {
443 self::${$annotation . 'Methods'}[$class][$method->name] = [$class, '' !== $description ? ' ' . $description . (preg_match('/[[:punct:]]$/', $description) ? '' : '.') : '.'];
444 $finalOrInternal = \true;
445 }
446 }
447 if ($finalOrInternal || $method->isConstructor() || !isset($doc['param']) || StatelessInvocation::class === $class) {
448 continue;
449 }
450 if (!isset(self::$annotatedParameters[$class][$method->name])) {
451 $definedParameters = [];
452 foreach ($method->getParameters() as $parameter) {
453 $definedParameters[$parameter->name] = \true;
454 }
455 }
456 foreach ($doc['param'] as $parameterName => $parameterType) {
457 if (!isset($definedParameters[$parameterName])) {
458 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);
459 }
460 }
461 }
462 return $deprecations;
463 }
464 public function checkCase(\ReflectionClass $refl, string $file, string $class) : ?array
465 {
466 $real = explode('\\', $class . strrchr($file, '.'));
467 $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file));
468 $i = \count($tail) - 1;
469 $j = \count($real) - 1;
470 while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) {
471 --$i;
472 --$j;
473 }
474 array_splice($tail, 0, $i + 1);
475 if (!$tail) {
476 return null;
477 }
478 $tail = \DIRECTORY_SEPARATOR . implode(\DIRECTORY_SEPARATOR, $tail);
479 $tailLen = \strlen($tail);
480 $real = $refl->getFileName();
481 if (2 === self::$caseCheck) {
482 $real = $this->darwinRealpath($real);
483 }
484 if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, \true) && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, \false)) {
485 return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)];
486 }
487 return null;
488 }
489 /**
490 * `realpath` on MacOSX doesn't normalize the case of characters.
491 */
492 private function darwinRealpath(string $real) : string
493 {
494 $i = 1 + strrpos($real, '/');
495 $file = substr($real, $i);
496 $real = substr($real, 0, $i);
497 if (isset(self::$darwinCache[$real])) {
498 $kDir = $real;
499 } else {
500 $kDir = strtolower($real);
501 if (isset(self::$darwinCache[$kDir])) {
502 $real = self::$darwinCache[$kDir][0];
503 } else {
504 $dir = getcwd();
505 if (!@chdir($real)) {
506 return $real . $file;
507 }
508 $real = getcwd() . '/';
509 chdir($dir);
510 $dir = $real;
511 $k = $kDir;
512 $i = \strlen($dir) - 1;
513 while (!isset(self::$darwinCache[$k])) {
514 self::$darwinCache[$k] = [$dir, []];
515 self::$darwinCache[$dir] =& self::$darwinCache[$k];
516 while ('/' !== $dir[--$i]) {
517 }
518 $k = substr($k, 0, ++$i);
519 $dir = substr($dir, 0, $i--);
520 }
521 }
522 }
523 $dirFiles = self::$darwinCache[$kDir][1];
524 if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) {
525 // Get the file name from "file_name.php(123) : eval()'d code"
526 $file = substr($file, 0, strrpos($file, '(', -17));
527 }
528 if (isset($dirFiles[$file])) {
529 return $real . $dirFiles[$file];
530 }
531 $kFile = strtolower($file);
532 if (!isset($dirFiles[$kFile])) {
533 foreach (scandir($real, 2) as $f) {
534 if ('.' !== $f[0]) {
535 $dirFiles[$f] = $f;
536 if ($f === $file) {
537 $kFile = $k = $file;
538 } elseif ($f !== ($k = strtolower($f))) {
539 $dirFiles[$k] = $f;
540 }
541 }
542 }
543 self::$darwinCache[$kDir][1] = $dirFiles;
544 }
545 return $real . $dirFiles[$kFile];
546 }
547 /**
548 * `class_implements` includes interfaces from the parents so we have to manually exclude them.
549 *
550 * @return string[]
551 */
552 private function getOwnInterfaces(string $class, ?string $parent) : array
553 {
554 $ownInterfaces = class_implements($class, \false);
555 if ($parent) {
556 foreach (class_implements($parent, \false) as $interface) {
557 unset($ownInterfaces[$interface]);
558 }
559 }
560 foreach ($ownInterfaces as $interface) {
561 foreach (class_implements($interface) as $interface) {
562 unset($ownInterfaces[$interface]);
563 }
564 }
565 return $ownInterfaces;
566 }
567 private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, ?\ReflectionType $returnType = null) : void
568 {
569 if ('__construct' === $method) {
570 return;
571 }
572 if ('null' === $types) {
573 self::$returnTypes[$class][$method] = ['null', 'null', $class, $filename];
574 return;
575 }
576 if ($nullable = 0 === strpos($types, 'null|')) {
577 $types = substr($types, 5);
578 } elseif ($nullable = '|null' === substr($types, -5)) {
579 $types = substr($types, 0, -5);
580 }
581 $arrayType = ['array' => 'array'];
582 $typesMap = [];
583 $glue = \false !== strpos($types, '&') ? '&' : '|';
584 foreach (explode($glue, $types) as $t) {
585 $t = self::SPECIAL_RETURN_TYPES[strtolower($t)] ?? $t;
586 $typesMap[$this->normalizeType($t, $class, $parent, $returnType)][$t] = $t;
587 }
588 if (isset($typesMap['array'])) {
589 if (isset($typesMap['Traversable']) || isset($typesMap['\\Traversable'])) {
590 $typesMap['iterable'] = $arrayType !== $typesMap['array'] ? $typesMap['array'] : ['iterable'];
591 unset($typesMap['array'], $typesMap['Traversable'], $typesMap['\\Traversable']);
592 } elseif ($arrayType !== $typesMap['array'] && isset(self::$returnTypes[$class][$method]) && !$returnType) {
593 return;
594 }
595 }
596 if (isset($typesMap['array']) && isset($typesMap['iterable'])) {
597 if ($arrayType !== $typesMap['array']) {
598 $typesMap['iterable'] = $typesMap['array'];
599 }
600 unset($typesMap['array']);
601 }
602 $iterable = $object = \true;
603 foreach ($typesMap as $n => $t) {
604 if ('null' !== $n) {
605 $iterable = $iterable && (\in_array($n, ['array', 'iterable']) || \false !== strpos($n, 'Iterator'));
606 $object = $object && (\in_array($n, ['callable', 'object', '$this', 'static']) || !isset(self::SPECIAL_RETURN_TYPES[$n]));
607 }
608 }
609 $phpTypes = [];
610 $docTypes = [];
611 foreach ($typesMap as $n => $t) {
612 if ('null' === $n) {
613 $nullable = \true;
614 continue;
615 }
616 $docTypes[] = $t;
617 if ('mixed' === $n || 'void' === $n) {
618 $nullable = \false;
619 $phpTypes = ['' => $n];
620 continue;
621 }
622 if ('resource' === $n) {
623 // there is no native type for "resource"
624 return;
625 }
626 if (!isset($phpTypes[''])) {
627 $phpTypes[] = $n;
628 }
629 }
630 $docTypes = array_merge([], ...$docTypes);
631 if (!$phpTypes) {
632 return;
633 }
634 if (1 < \count($phpTypes)) {
635 if ($iterable && '8.0' > $this->patchTypes['php']) {
636 $phpTypes = $docTypes = ['iterable'];
637 } elseif ($object && 'object' === $this->patchTypes['force']) {
638 $phpTypes = $docTypes = ['object'];
639 } elseif ('8.0' > $this->patchTypes['php']) {
640 // ignore multi-types return declarations
641 return;
642 }
643 }
644 $phpType = sprintf($nullable ? 1 < \count($phpTypes) ? '%s|null' : '?%s' : '%s', implode($glue, $phpTypes));
645 $docType = sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes));
646 self::$returnTypes[$class][$method] = [$phpType, $docType, $class, $filename];
647 }
648 private function normalizeType(string $type, string $class, ?string $parent, ?\ReflectionType $returnType) : string
649 {
650 if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) {
651 if ('parent' === ($lcType = self::SPECIAL_RETURN_TYPES[$lcType])) {
652 $lcType = null !== $parent ? '\\' . $parent : 'parent';
653 } elseif ('self' === $lcType) {
654 $lcType = '\\' . $class;
655 }
656 return $lcType;
657 }
658 // We could resolve "use" statements to return the FQDN
659 // but this would be too expensive for a runtime checker
660 if ('[]' !== substr($type, -2)) {
661 return $type;
662 }
663 if ($returnType instanceof \ReflectionNamedType) {
664 $type = $returnType->getName();
665 if ('mixed' !== $type) {
666 return isset(self::SPECIAL_RETURN_TYPES[$type]) ? $type : '\\' . $type;
667 }
668 }
669 return 'array';
670 }
671 /**
672 * Utility method to add #[ReturnTypeWillChange] where php triggers deprecations.
673 */
674 private function patchReturnTypeWillChange(\ReflectionMethod $method)
675 {
676 if (\PHP_VERSION_ID >= 80000 && \count($method->getAttributes(\ReturnTypeWillChange::class))) {
677 return;
678 }
679 if (!is_file($file = $method->getFileName())) {
680 return;
681 }
682 $fileOffset = self::$fileOffsets[$file] ?? 0;
683 $code = file($file);
684 $startLine = $method->getStartLine() + $fileOffset - 2;
685 if (\false !== stripos($code[$startLine], 'ReturnTypeWillChange')) {
686 return;
687 }
688 $code[$startLine] .= " #[\\ReturnTypeWillChange]\n";
689 self::$fileOffsets[$file] = 1 + $fileOffset;
690 file_put_contents($file, $code);
691 }
692 /**
693 * Utility method to add @return annotations to the Symfony code-base where it triggers self-deprecations.
694 */
695 private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
696 {
697 static $patchedMethods = [];
698 static $useStatements = [];
699 if (!is_file($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) {
700 return;
701 }
702 $patchedMethods[$file][$startLine] = \true;
703 $fileOffset = self::$fileOffsets[$file] ?? 0;
704 $startLine += $fileOffset - 2;
705 if ($nullable = '|null' === substr($returnType, -5)) {
706 $returnType = substr($returnType, 0, -5);
707 }
708 $glue = \false !== strpos($returnType, '&') ? '&' : '|';
709 $returnType = explode($glue, $returnType);
710 $code = file($file);
711 foreach ($returnType as $i => $type) {
712 if (preg_match('/((?:\\[\\])+)$/', $type, $m)) {
713 $type = substr($type, 0, -\strlen($m[1]));
714 $format = '%s' . $m[1];
715 } else {
716 $format = null;
717 }
718 if (isset(self::SPECIAL_RETURN_TYPES[$type]) || '\\' === $type[0] && !($p = strrpos($type, '\\', 1))) {
719 continue;
720 }
721 [$namespace, $useOffset, $useMap] = $useStatements[$file] ?? ($useStatements[$file] = self::getUseStatements($file));
722 if ('\\' !== $type[0]) {
723 [$declaringNamespace, , $declaringUseMap] = $useStatements[$declaringFile] ?? ($useStatements[$declaringFile] = self::getUseStatements($declaringFile));
724 $p = strpos($type, '\\', 1);
725 $alias = $p ? substr($type, 0, $p) : $type;
726 if (isset($declaringUseMap[$alias])) {
727 $type = '\\' . $declaringUseMap[$alias] . ($p ? substr($type, $p) : '');
728 } else {
729 $type = '\\' . $declaringNamespace . $type;
730 }
731 $p = strrpos($type, '\\', 1);
732 }
733 $alias = substr($type, 1 + $p);
734 $type = substr($type, 1);
735 if (!isset($useMap[$alias]) && (class_exists($c = $namespace . $alias) || interface_exists($c) || trait_exists($c))) {
736 $useMap[$alias] = $c;
737 }
738 if (!isset($useMap[$alias])) {
739 $useStatements[$file][2][$alias] = $type;
740 $code[$useOffset] = "use {$type};\n" . $code[$useOffset];
741 ++$fileOffset;
742 } elseif ($useMap[$alias] !== $type) {
743 $alias .= 'FIXME';
744 $useStatements[$file][2][$alias] = $type;
745 $code[$useOffset] = "use {$type} as {$alias};\n" . $code[$useOffset];
746 ++$fileOffset;
747 }
748 $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;
749 }
750 if ('docblock' === $this->patchTypes['force'] || 'object' === $normalizedType && '7.1' === $this->patchTypes['php']) {
751 $returnType = implode($glue, $returnType) . ($nullable ? '|null' : '');
752 if (\false !== strpos($code[$startLine], '#[')) {
753 --$startLine;
754 }
755 if ($method->getDocComment()) {
756 $code[$startLine] = " * @return {$returnType}\n" . $code[$startLine];
757 } else {
758 $code[$startLine] .= <<<EOTXT
759 /**
760 * @return {$returnType}
761 */
762
763 EOTXT;
764 }
765 $fileOffset += substr_count($code[$startLine], "\n") - 1;
766 }
767 self::$fileOffsets[$file] = $fileOffset;
768 file_put_contents($file, $code);
769 $this->fixReturnStatements($method, $normalizedType);
770 }
771 private static function getUseStatements(string $file) : array
772 {
773 $namespace = '';
774 $useMap = [];
775 $useOffset = 0;
776 if (!is_file($file)) {
777 return [$namespace, $useOffset, $useMap];
778 }
779 $file = file($file);
780 for ($i = 0; $i < \count($file); ++$i) {
781 if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) {
782 break;
783 }
784 if (0 === strpos($file[$i], 'namespace ')) {
785 $namespace = substr($file[$i], \strlen('namespace '), -2) . '\\';
786 $useOffset = $i + 2;
787 }
788 if (0 === strpos($file[$i], 'use ')) {
789 $useOffset = $i;
790 for (; 0 === strpos($file[$i], 'use '); ++$i) {
791 $u = explode(' as ', substr($file[$i], 4, -2), 2);
792 if (1 === \count($u)) {
793 $p = strrpos($u[0], '\\');
794 $useMap[substr($u[0], \false !== $p ? 1 + $p : 0)] = $u[0];
795 } else {
796 $useMap[$u[1]] = $u[0];
797 }
798 }
799 break;
800 }
801 }
802 return [$namespace, $useOffset, $useMap];
803 }
804 private function fixReturnStatements(\ReflectionMethod $method, string $returnType)
805 {
806 if ('docblock' !== $this->patchTypes['force']) {
807 if ('7.1' === $this->patchTypes['php'] && 'object' === ltrim($returnType, '?')) {
808 return;
809 }
810 if ('7.4' > $this->patchTypes['php'] && $method->hasReturnType()) {
811 return;
812 }
813 if ('8.0' > $this->patchTypes['php'] && (\false !== strpos($returnType, '|') || \in_array($returnType, ['mixed', 'static'], \true))) {
814 return;
815 }
816 if ('8.1' > $this->patchTypes['php'] && \false !== strpos($returnType, '&')) {
817 return;
818 }
819 }
820 if (!is_file($file = $method->getFileName())) {
821 return;
822 }
823 $fixedCode = $code = file($file);
824 $i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine();
825 if ('?' !== $returnType && 'docblock' !== $this->patchTypes['force']) {
826 $fixedCode[$i - 1] = preg_replace('/\\)(?::[^;\\n]++)?(;?\\n)/', "): {$returnType}\\1", $code[$i - 1]);
827 }
828 $end = $method->isGenerator() ? $i : $method->getEndLine();
829 $inClosure = \false;
830 $braces = 0;
831 for (; $i < $end; ++$i) {
832 if (!$inClosure) {
833 $inClosure = \false !== strpos($code[$i], 'function (');
834 }
835 if ($inClosure) {
836 $braces += substr_count($code[$i], '{') - substr_count($code[$i], '}');
837 $inClosure = $braces > 0;
838 continue;
839 }
840 if ('void' === $returnType) {
841 $fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]);
842 } elseif ('mixed' === $returnType || '?' === $returnType[0]) {
843 $fixedCode[$i] = str_replace(' return;', ' return null;', $code[$i]);
844 } else {
845 $fixedCode[$i] = str_replace(' return;', " return {$returnType}!?;", $code[$i]);
846 }
847 }
848 if ($fixedCode !== $code) {
849 file_put_contents($file, $fixedCode);
850 }
851 }
852 /**
853 * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector
854 */
855 private function parsePhpDoc(\Reflector $reflector) : array
856 {
857 if (!($doc = $reflector->getDocComment())) {
858 return [];
859 }
860 $tagName = '';
861 $tagContent = '';
862 $tags = [];
863 foreach (explode("\n", substr($doc, 3, -2)) as $line) {
864 $line = ltrim($line);
865 $line = ltrim($line, '*');
866 if ('' === ($line = trim($line))) {
867 if ('' !== $tagName) {
868 $tags[$tagName][] = $tagContent;
869 }
870 $tagName = $tagContent = '';
871 continue;
872 }
873 if ('@' === $line[0]) {
874 if ('' !== $tagName) {
875 $tags[$tagName][] = $tagContent;
876 $tagContent = '';
877 }
878 if (preg_match('{^@([-a-zA-Z0-9_:]++)(\\s|$)}', $line, $m)) {
879 $tagName = $m[1];
880 $tagContent = str_replace("\t", ' ', ltrim(substr($line, 2 + \strlen($tagName))));
881 } else {
882 $tagName = '';
883 }
884 } elseif ('' !== $tagName) {
885 $tagContent .= ' ' . str_replace("\t", ' ', $line);
886 }
887 }
888 if ('' !== $tagName) {
889 $tags[$tagName][] = $tagContent;
890 }
891 foreach ($tags['method'] ?? [] as $i => $method) {
892 unset($tags['method'][$i]);
893 $parts = preg_split('{(\\s++|\\((?:[^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<(?:[^<>]*+|(?R))*>|\\{(?:[^{}]*+|(?R))*\\})}', $method, -1, \PREG_SPLIT_DELIM_CAPTURE);
894 $returnType = '';
895 $static = 'static' === $parts[0];
896 for ($i = $static ? 2 : 0; null !== ($p = $parts[$i] ?? null); $i += 2) {
897 if (\in_array($p, ['', '|', '&', 'callable'], \true) || \in_array(substr($returnType, -1), ['|', '&'], \true)) {
898 $returnType .= trim($parts[$i - 1] ?? '') . $p;
899 continue;
900 }
901 $signature = '(' === ($parts[$i + 1][0] ?? '(') ? $parts[$i + 1] ?? '()' : null;
902 if (null === $signature && '' === $returnType) {
903 $returnType = $p;
904 continue;
905 }
906 if ($static && 2 === $i) {
907 $static = \false;
908 $returnType = 'static';
909 }
910 if (\in_array($description = trim(implode('', \array_slice($parts, 2 + $i))), ['', '.'], \true)) {
911 $description = null;
912 } elseif (!preg_match('/[.!]$/', $description)) {
913 $description .= '.';
914 }
915 $tags['method'][$p] = [$static, $returnType, $signature ?? '()', $description];
916 break;
917 }
918 }
919 foreach ($tags['param'] ?? [] as $i => $param) {
920 unset($tags['param'][$i]);
921 if (\strlen($param) !== strcspn($param, '<{(')) {
922 $param = preg_replace('{\\(([^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\\{([^{}]*+|(?R))*\\}}', '', $param);
923 }
924 if (\false === ($i = strpos($param, '$'))) {
925 continue;
926 }
927 $type = 0 === $i ? '' : rtrim(substr($param, 0, $i), ' &');
928 $param = substr($param, 1 + $i, (strpos($param, ' ', $i) ?: 1 + $i + \strlen($param)) - $i - 1);
929 $tags['param'][$param] = $type;
930 }
931 foreach (['var', 'return'] as $k) {
932 if (null === ($v = $tags[$k][0] ?? null)) {
933 continue;
934 }
935 if (\strlen($v) !== strcspn($v, '<{(')) {
936 $v = preg_replace('{\\(([^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\\{([^{}]*+|(?R))*\\}}', '', $v);
937 }
938 $tags[$k] = substr($v, 0, strpos($v, ' ') ?: \strlen($v)) ?: null;
939 }
940 return $tags;
941 }
942 }
943