Skip to content

Commit

Permalink
[ErrorHandler] improve DebugClassLoader patching logic
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Aug 22, 2019
1 parent a77e326 commit 09f10d3
Showing 1 changed file with 72 additions and 27 deletions.
99 changes: 72 additions & 27 deletions src/Symfony/Component/ErrorHandler/DebugClassLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class DebugClassLoader
private $classLoader;
private $isFinder;
private $loaded = [];
private $compatPatch;
private $patchTypes;
private static $caseCheck;
private static $checkedClasses = [];
private static $final = [];
Expand All @@ -85,7 +85,7 @@ public function __construct(callable $classLoader)
{
$this->classLoader = $classLoader;
$this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
$this->compatPatch = getenv('SYMFONY_PATCH_TYPE_DECLARATIONS_COMPAT') ?: null;
parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);

if (!isset(self::$caseCheck)) {
$file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
Expand Down Expand Up @@ -160,13 +160,22 @@ public static function disable(): void
spl_autoload_unregister($function);
}

$loader = null;

foreach ($functions as $function) {
if (\is_array($function) && $function[0] instanceof self) {
$loader = $function[0];
$function = $function[0]->getClassLoader();
}

spl_autoload_register($function);
}

if (null !== $loader) {
foreach (array_merge(get_declared_interfaces(), get_declared_traits(), get_declared_classes()) as $class) {
$loader->checkClass($class);
}
}
}

public function findFile(string $class): ?string
Expand Down Expand Up @@ -348,7 +357,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
if (trait_exists($class)) {
$file = $refl->getFileName();

foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
foreach ($refl->getMethods() as $method) {
if ($method->getFileName() === $file) {
self::$methodTraits[$file][$method->getStartLine()] = $class;
}
Expand All @@ -370,7 +379,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
}
}

foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
foreach ($refl->getMethods() as $method) {
if ($method->class !== $class) {
continue;
}
Expand Down Expand Up @@ -413,18 +422,24 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
}
}

if ($canAddReturnType = '' !== ($this->patchTypes['class-prefix'] ?? '') && 0 === strpos($class, $this->patchTypes['class-prefix'])) {
$canAddReturnType = false !== strpos($refl->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR)
|| $refl->isFinal()
|| $method->isFinal()
|| $method->isPrivate()
|| ('' === (self::$internal[$class] ?? null) && !$refl->isAbstract())
|| '' === (self::$final[$class] ?? null)
|| preg_match('/@(final|internal)$/m', $doc);
}

if (isset(self::$returnTypes[$class][$method->name]) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
list($normalizedType, $returnType, $declaringClass, $declaringFile) = self::$returnTypes[$class][$method->name];

if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
self::fixReturnStatements($method, $normalizedType);
if ($canAddReturnType) {
$this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
}

if (strncmp($ns, $declaringClass, $len)) {
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
self::patchMethod($method, $returnType, $declaringFile);
}

$deprecations[] = sprintf('Method "%s::%s()" will return "%s" as of its next major version. Doing the same in child class "%s" will be required when upgrading.', $declaringClass, $method->name, $normalizedType, $class);
}
}
Expand All @@ -436,11 +451,19 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
if (!$method->hasReturnType() && false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) {
$this->setReturnType($matches[1], $method, $parent);

if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
self::fixReturnStatements($method, self::$returnTypes[$class][$method->name][0] ?? '?');
if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
$this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
}

if ($method->isPrivate()) {
unset(self::$returnTypes[$class][$method->name]);
}
}

if ($method->isPrivate()) {
continue;
}

$finalOrInternal = false;

foreach (['final', 'internal'] as $annotation) {
Expand Down Expand Up @@ -630,7 +653,7 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
} elseif ('null' === $normalizedType) {
$normalizedType = $t;
$returnType = $t;
} elseif ($n !== $normalizedType) {
} 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)) {
// ignore multi-types return declarations
return;
}
Expand Down Expand Up @@ -680,7 +703,7 @@ private function normalizeType(string $type, string $class, ?string $parent): st
/**
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
*/
private static function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile)
private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
{
static $patchedMethods = [];
static $useStatements = [];
Expand All @@ -690,8 +713,10 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
}

$patchedMethods[$file][$startLine] = true;
$patchedMethods[$file][0] = $patchedMethods[$file][0] ?? 0;
$startLine += $patchedMethods[$file][0] - 2;
$fileOffset = $patchedMethods[$file][0] ?? 0;
$startLine += $fileOffset - 2;
$nullable = '?' === $normalizedType[0] ? '?' : '';
$normalizedType = ltrim($normalizedType, '?');
$returnType = explode('|', $returnType);
$code = file($file);

Expand Down Expand Up @@ -737,32 +762,42 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
if (!isset($useMap[$alias])) {
$useStatements[$file][2][$alias] = $type;
$code[$useOffset] = "use $type;\n".$code[$useOffset];
++$patchedMethods[$file][0];
++$fileOffset;
} elseif ($useMap[$alias] !== $type) {
$alias .= 'FIXME';
$useStatements[$file][2][$alias] = $type;
$code[$useOffset] = "use $type as $alias;\n".$code[$useOffset];
++$patchedMethods[$file][0];
++$fileOffset;
}

$returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;

if (!isset(self::SPECIAL_RETURN_TYPES[$normalizedType]) && !isset(self::SPECIAL_RETURN_TYPES[$returnType[$i]])) {
$normalizedType = $returnType[$i];
}
}

$returnType = implode('|', $returnType);
if ('object' === $normalizedType && ($this->patchTypes['keep-compat-with-php71'] ?? false)) {
$returnType = implode('|', $returnType);

if ($method->getDocComment()) {
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
} else {
$code[$startLine] .= <<<EOTXT
if ($method->getDocComment()) {
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
} else {
$code[$startLine] .= <<<EOTXT
/**
* @return $returnType
*/
EOTXT;
}

$fileOffset += substr_count($code[$startLine], "\n") - 1;
}

$patchedMethods[$file][0] += substr_count($code[$startLine], "\n") - 1;
$patchedMethods[$file][0] = $fileOffset;
file_put_contents($file, $code);

$this->fixReturnStatements($method, $nullable.$normalizedType, $patchedMethods[$file][0]);
}

private static function getUseStatements(string $file): array
Expand Down Expand Up @@ -808,18 +843,28 @@ private static function getUseStatements(string $file): array
return [$namespace, $useOffset, $useMap];
}

private static function fixReturnStatements(\ReflectionMethod $method, string $returnType)
private function fixReturnStatements(\ReflectionMethod $method, ?string $returnType, int $i = 0)
{
if (($this->patchTypes['keep-compat-with-php71'] ?? false) && 'object' === ltrim($returnType, '?')) {
return;
}

if (!file_exists($file = $method->getFileName())) {
return;
}

$fixedCode = $code = file($file);
$i += $method->getStartLine();

if (null !== $returnType) {
$fixedCode[$i - 1] = preg_replace('/\)(;?\n)/', "): $returnType\\1", $code[$i - 1]);
}

$end = $method->getEndLine();
for ($i = $method->getStartLine(); $i < $end; ++$i) {
for (; $i < $end; ++$i) {
if ('void' === $returnType) {
$fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]);
} elseif ('mixed' === $returnType || '?' === $returnType[0]) {
} elseif ('mixed' === $returnType || null === $returnType[0]) {
$fixedCode[$i] = str_replace(' return;', ' return null;', $code[$i]);
} else {
$fixedCode[$i] = str_replace(' return;', " return $returnType!?;", $code[$i]);
Expand Down

0 comments on commit 09f10d3

Please sign in to comment.