DrupalDebugClassLoader.php

Namespace

Drupal\TestTools\ErrorHandler

File

core/tests/Drupal/TestTools/ErrorHandler/DrupalDebugClassLoader.php

View source
<?php

declare (strict_types=1);
namespace Drupal\TestTools\ErrorHandler;

use Symfony\Component\ErrorHandler\DebugClassLoader;

/**
 * Extends Symfony's DebugClassLoader for Drupal-aware vendor boundaries.
 *
 * Symfony's DebugClassLoader uses the first namespace segment as the vendor
 * boundary for return type deprecation checks. Since all Drupal code shares
 * "Drupal\" as the first segment, cross-extension return type deprecations
 * (e.g., contrib extending core) are never triggered.
 *
 * This subclass treats each Drupal extension (the second namespace segment)
 * as the vendor boundary, enabling return type deprecation notices when an
 * extension overrides a method from a different extension without adding
 * native return types.
 *
 * @internal
 */
class DrupalDebugClassLoader extends DebugClassLoader {
  
  /**
   * Cached ReflectionProperty for DebugClassLoader::$returnTypes.
   */
  private static \ReflectionProperty $returnTypesProperty;
  
  /**
   * Cached ReflectionProperty for DebugClassLoader::$patchTypes.
   */
  private static \ReflectionProperty $patchTypesProperty;
  
  /**
   * {@inheritdoc}
   */
  public function checkAnnotations(\ReflectionClass $refl, string $class) : array {
    $deprecations = parent::checkAnnotations($refl, $class);
    // Only process non-trait Drupal classes.
    if (!str_starts_with($class, 'Drupal\\') || trait_exists($class, FALSE)) {
      return $deprecations;
    }
    self::$returnTypesProperty ??= new \ReflectionProperty(DebugClassLoader::class, 'returnTypes');
    $returnTypes = self::$returnTypesProperty->getValue()[$class] ?? [];
    self::$patchTypesProperty ??= new \ReflectionProperty(DebugClassLoader::class, 'patchTypes');
    if (!$returnTypes || empty(self::$patchTypesProperty->getValue($this)['deprecations'])) {
      return $deprecations;
    }
    $classExtension = self::getExtensionName($class);
    $className = str_contains($class, "@anonymous\x00") ? ((get_parent_class($class) ?: key(class_implements($class))) ?: 'class') . '@anonymous' : $class;
    foreach ($returnTypes as $method => $returnTypeData) {
      [$normalizedType, , $declaringClass] = $returnTypeData;
      // Skip if no cross-extension boundary: empty declaring class (magic
      // methods), same class (own @return), or same/non-Drupal extension.
      $declaringExtension = self::getExtensionName($declaringClass);
      if ($declaringClass === '' || $declaringClass === $class || $declaringExtension === NULL || $classExtension === $declaringExtension) {
        continue;
      }
      // Skip if not actually overridden, or already has a native return type.
      $methodRefl = $refl->getMethod($method);
      if ($methodRefl->class !== $class || $methodRefl->hasReturnType()) {
        continue;
      }
      // Skip if the method's docblock contains @deprecated or @return.
      $docComment = $methodRefl->getDocComment();
      if ($docComment !== FALSE && (str_contains($docComment, '@deprecated') || str_contains($docComment, '@return'))) {
        continue;
      }
      $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, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className);
    }
    return $deprecations;
  }
  
  /**
   * Extracts the Drupal extension name from a fully qualified class name.
   *
   * @param string $class
   *   The fully qualified class name.
   *
   * @return string|null
   *   The extension name, or NULL for non-Drupal classes.
   */
  private static function getExtensionName(string $class) : ?string {
    if (!str_starts_with($class, 'Drupal\\')) {
      return NULL;
    }
    $parts = explode('\\', $class);
    if (isset($parts[2]) && $parts[1] === 'Tests') {
      return $parts[2];
    }
    return $parts[1] ?? NULL;
  }

}

Classes

Title Deprecated Summary
DrupalDebugClassLoader Extends Symfony's DebugClassLoader for Drupal-aware vendor boundaries.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.