You are here

LibraryDeprecationAnalyzer.php in Upgrade Status 8.3

Same filename and directory in other branches
  1. 8.2 src/LibraryDeprecationAnalyzer.php

File

src/LibraryDeprecationAnalyzer.php
View source
<?php

declare (strict_types=1);
namespace Drupal\upgrade_status;

use Drupal\Core\Asset\LibraryDiscoveryParser;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionList;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ProfileExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Template\TwigEnvironment;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Node;
use Twig\Source;
use Twig\Util\TemplateDirIterator;

/**
 * A library deprecation analyzer.
 */
final class LibraryDeprecationAnalyzer {

  /**
   * The library discovery parser.
   *
   * @var \Drupal\Core\Asset\LibraryDiscoveryParser
   */
  protected $libraryDiscoveryParser;

  /**
   * The Twig environment.
   *
   * @var \Drupal\Core\Template\TwigEnvironment
   */
  protected $twigEnvironment;

  /**
   * The list of available modules.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleExtensionList;

  /**
   * The list of available themes.
   *
   * @var \Drupal\Core\Extension\ThemeExtensionList
   */
  protected $themeExtensionList;

  /**
   * The list of available profiles.
   *
   * @var \Drupal\Core\Extension\ProfileExtensionList
   */
  protected $profileExtensionList;

  /**
   * Constructs a new library deprecation analyzer
   *
   * @param \Drupal\Core\Asset\LibraryDiscoveryParser $library_discovery_parser
   *   The library discovery parser.
   * @param \Drupal\Core\Template\TwigEnvironment $twig_environment
   *   The Twig environment.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
   *   The module extension list service.
   * @param \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list
   *   The theme extension handler service.
   * @param \Drupal\Core\Extension\ProfileExtensionList $profile_extension_list
   *   The profile extension handler service.
   */
  public function __construct(LibraryDiscoveryParser $library_discovery_parser, TwigEnvironment $twig_environment, ModuleExtensionList $module_extension_list, ThemeExtensionList $theme_extension_list, ProfileExtensionList $profile_extension_list) {
    $this->libraryDiscoveryParser = $library_discovery_parser;
    $this->twigEnvironment = $twig_environment;
    $this->moduleExtensionList = $module_extension_list;
    $this->themeExtensionList = $theme_extension_list;
    $this->profileExtensionList = $profile_extension_list;
  }

  /**
   * Analyzes usages of deprecated libraries in an extension.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *  The extension to be analyzed.
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   *   A list of deprecation messages.
   *
   * @throws \Exception
   */
  public function analyze(Extension $extension) : array {
    $deprecations = [];
    $deprecations = array_merge($deprecations, $this
      ->analyzeLibraryDependencies($extension));
    if ($extension
      ->getType() === 'theme') {
      $deprecations = array_merge($deprecations, $this
        ->analyzeThemeLibraryOverrides($extension));
      $deprecations = array_merge($deprecations, $this
        ->analyzeThemeLibraryExtends($extension));
    }
    $deprecations = array_merge($deprecations, $this
      ->analyzeTwigLibraryDependencies($extension));
    $deprecations = array_merge($deprecations, $this
      ->analyzePhpLibraryReferences($extension));
    return $deprecations;
  }

  /**
   * Analyzes libraries for dependencies on deprecated libraries.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *   The extension to be analyzed.
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   */
  private function analyzeLibraryDependencies(Extension $extension) : array {

    // Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension() assumes a
    // disabled module is a theme and fails not finding it then, so check if
    // the extension identified he is an installed module or theme. The library
    // info will not be available otherwise.
    $installed_modules = array_keys($this->moduleExtensionList
      ->getAllInstalledInfo());
    $installed_themes = array_keys($this->themeExtensionList
      ->getAllInstalledInfo());
    if (!in_array($extension
      ->getName(), $installed_modules) && !in_array($extension
      ->getName(), $installed_themes)) {
      $message = sprintf("The '%s' extension is not installed. Cannot check deprecated library use.", $extension
        ->getName());
      return [
        new DeprecationMessage($message, $extension
          ->getPath(), 0),
      ];
    }
    try {
      $libraries = $this->libraryDiscoveryParser
        ->buildByExtension($extension
        ->getName());
    } catch (\Exception $e) {
      return [
        new DeprecationMessage($e
          ->getMessage(), $extension
          ->getPath(), 0),
      ];
    }
    $libraries_with_dependencies = array_filter($libraries, function ($library) {
      return isset($library['dependencies']);
    });
    $deprecations = [];
    $file = sprintf('%s/%s.libraries.yml', $extension
      ->getPath(), $extension
      ->getName());
    foreach ($libraries_with_dependencies as $key => $library_with_dependency) {
      foreach ($library_with_dependency['dependencies'] as $dependency) {
        $is_deprecated = $this
          ->isLibraryDeprecated($dependency);
        if (is_null($is_deprecated)) {
          $message = sprintf("The '%s' library (a dependency of '%s') is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $dependency, $key);
          $deprecations[] = new DeprecationMessage($message, $file, 0);
        }
        elseif (!empty($is_deprecated)) {
          if ($is_deprecated instanceof DeprecationMessage) {
            $is_deprecated
              ->setFile($file);
            $deprecations[] = $is_deprecated;
          }
          else {
            $message = sprintf("The '%s' library is depending on a deprecated library. %s", $key, $is_deprecated);
            $deprecations[] = new DeprecationMessage($message, $file, 0);
          }
        }
      }
    }
    return $deprecations;
  }

  /**
   * Analyze library overrides in a theme.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   * @throws \Exception
   */
  private function analyzeThemeLibraryOverrides(Extension $extension) : array {
    if ($extension
      ->getType() !== 'theme') {
      throw new \Exception('Library overrides are only available in themes.');
    }
    if (!isset($extension->info['libraries-override'])) {
      return [];
    }
    return array_reduce(array_keys($extension->info['libraries-override']), function ($deprecated_libraries, $library) use ($extension) {
      $is_deprecated = $this
        ->isLibraryDeprecated($library);
      if (is_null($is_deprecated)) {
        $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library);
        $deprecated_libraries[] = new DeprecationMessage($message, $extension
          ->getFilename(), 0);
      }
      elseif (!empty($is_deprecated)) {
        if ($is_deprecated instanceof DeprecationMessage) {
          $is_deprecated
            ->setFile($extension
            ->getFilename());
          $deprecated_libraries[] = $is_deprecated;
        }
        else {
          $message = "Theme is overriding a deprecated library. {$is_deprecated}";
          $deprecated_libraries[] = new DeprecationMessage($message, $extension
            ->getFilename(), 0);
        }
      }
      return $deprecated_libraries;
    }, []);
  }

  /**
   * Analyze library extends in a theme.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   * @throws \Exception
   */
  private function analyzeThemeLibraryExtends(Extension $extension) : array {
    if ($extension
      ->getType() !== 'theme') {
      throw new \Exception('Library extends are only available in themes.');
    }
    if (!isset($extension->info['libraries-extend'])) {
      return [];
    }
    return array_reduce(array_keys($extension->info['libraries-extend']), function ($deprecated_libraries, $library) use ($extension) {
      $is_deprecated = $this
        ->isLibraryDeprecated($library);
      if (is_null($is_deprecated)) {
        $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library);
        $deprecated_libraries[] = new DeprecationMessage($message, $extension
          ->getFilename(), 0);
      }
      elseif (!empty($is_deprecated)) {
        if ($is_deprecated instanceof DeprecationMessage) {
          $is_deprecated
            ->setFile($extension
            ->getFilename());
          $deprecated_libraries[] = $is_deprecated;
        }
        else {
          $message = "Theme is extending a deprecated library. {$is_deprecated}";
          $deprecated_libraries[] = new DeprecationMessage($message, $extension
            ->getFilename(), 0);
        }
      }
      return $deprecated_libraries;
    }, []);
  }

  /**
   * Analyzes Twig library dependencies.
   *
   * At the moment we analyze only libraries attached using `library_attach()`.
   * However, there could be other ways to attache a library in a template, such
   * as generating and rendering a render array with `#attached`.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   */
  private function analyzeTwigLibraryDependencies(Extension $extension) : array {
    $iterator = new TemplateDirIterator(new TwigRecursiveIterator($extension
      ->getPath()));
    $deprecations = [];
    foreach ($iterator as $name => $contents) {
      try {
        $libraries = $this
          ->findLibrariesAttachedInTemplate($this->twigEnvironment
          ->parse($this->twigEnvironment
          ->tokenize(new Source($contents, $name))));
        foreach ($libraries as $library) {
          $is_deprecated = $this
            ->isLibraryDeprecated($library['library']);
          if (is_null($is_deprecated)) {
            $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library['library']);
            $deprecations[] = new DeprecationMessage($message, $name, $library['line']);
          }
          elseif (!empty($is_deprecated)) {
            if ($is_deprecated instanceof DeprecationMessage) {
              $is_deprecated
                ->setFile($name);
              $deprecations[] = $is_deprecated;
            }
            else {
              $message = 'Template is attaching a deprecated library. ' . $is_deprecated;
              $deprecations[] = new DeprecationMessage($message, $name, $library['line']);
            }
          }
        }
      } catch (SyntaxError $e) {

        // Ignore templates containing syntax errors.
      }
    }
    return $deprecations;
  }

  /**
   * Finds libraries attached using `libraries_attach()` in a Twig template.
   *
   * @param \Twig\Node\Node $node
   *
   * @return string[]
   */
  private function findLibrariesAttachedInTemplate(Node $node) : array {
    if (!is_iterable($node)) {
      return [];
    }
    $libraries = [];
    foreach ($node as $item) {
      if ($item instanceof FunctionExpression) {
        if ($item
          ->getAttribute('name') === 'attach_library') {
          foreach ($item
            ->getNode('arguments') as $argument) {
            if ($argument instanceof ConstantExpression && $argument
              ->hasAttribute('value')) {
              $libraries[] = [
                'library' => $argument
                  ->getAttribute('value'),
                'line' => $item
                  ->getTemplateLine(),
              ];
            }
          }
        }
      }
      else {
        $libraries = array_merge($libraries, $this
          ->findLibrariesAttachedInTemplate($item));
      }
    }
    return $libraries;
  }

  /**
   * Analyzes libraries referenced in PHP.
   *
   * This can only analyze statically attached libraries. We are not checking
   * the context where the library is being referenced, so in some cases this
   * could lead into false negatives. Testing the context would be possible, but
   * could lead into not detecting all references to deprecated libraries.
   *
   * @param \Drupal\Core\Extension\Extension $extension
   *
   * @return \Drupal\upgrade_status\DeprecationMessage[]
   */
  private function analyzePhpLibraryReferences(Extension $extension) : array {
    $iterator = new \RegexIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($extension
      ->getPath()), \RecursiveIteratorIterator::LEAVES_ONLY), '/\\.(php|module|theme|profile|inc)$/');
    $deprecations = [];
    foreach ($iterator as $file) {
      try {
        $tokens = token_get_all(file_get_contents($file
          ->getPathName()));
      } catch (\ParseError $error) {

        // Ignore syntax errors.
        continue;
      }

      // Find nodes that look like attaching a library.
      $potential_libraries = array_values(array_map(function ($token) {
        list($type, $value, $line) = $token;
        return [
          'value' => substr($value, 1, -1),
          'line' => $line,
        ];
      }, array_filter($tokens, function ($token) {
        if (is_array($token)) {
          list($type, $value) = $token;
          return $type === T_CONSTANT_ENCAPSED_STRING && preg_match('/^[\\"\'][a-zA-Z0-9\\.\\-\\_]+\\/[a-zA-Z0-9\\.\\-\\_]+[\\"\']$/', $value);
        }
        return FALSE;
      })));
      foreach ($potential_libraries as $potential_library) {
        list($extension_name) = explode('/', $potential_library['value'], 2);
        $extension_lists = [
          $this->moduleExtensionList,
          $this->themeExtensionList,
          $this->profileExtensionList,
        ];

        // Iterate through all extension lists to see if we have found a valid
        // extension.
        $valid_extension = array_reduce($extension_lists, function ($valid_extension, ExtensionList $extension_list) use ($extension_name) {
          if ($valid_extension || $extension_list
            ->exists($extension_name)) {
            return TRUE;
          }
          return FALSE;
        }, FALSE);
        if ($valid_extension) {
          $is_deprecated = $this
            ->isLibraryDeprecated($potential_library['value']);
          if (is_null($is_deprecated)) {
            $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $potential_library['value']);
            $deprecations[] = new DeprecationMessage($message, $file
              ->getPathName(), $potential_library['line']);
          }
          elseif (!empty($is_deprecated)) {
            if ($is_deprecated instanceof DeprecationMessage) {
              $is_deprecated
                ->setFile($file
                ->getPathName());
              $deprecations[] = $is_deprecated;
            }
            else {
              $message = "The referenced library is deprecated. {$is_deprecated}";
              $deprecations[] = new DeprecationMessage($message, $file
                ->getPathName(), $potential_library['line']);
            }
          }
        }
      }
    }
    return $deprecations;
  }

  /**
   * Tests if library is deprecated.
   *
   * @param string $library
   *   A string representing library. For example, 'node/drupal.node'.
   *
   * @return bool|string|NULL|DeprecationMessage
   *   FALSE if the library is not deprecated. NULL if the library's module
   *   is not enabled. Deprecation message string otherwise.
   */
  private function isLibraryDeprecated($library) {
    if (!strpos($library, '/')) {
      return new DeprecationMessage('The ' . $library . ' library does not have an extension name and is therefore not valid.');
    }
    list($extension_name, $library_name) = explode('/', $library, 2);

    // Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension() assumes a
    // disabled module is a theme and fails not finding it then, so check if
    // the extension identified he is an installed module or theme. The library
    // info will not be available otherwise.
    if ($extension_name != 'core') {
      $installed_modules = array_keys($this->moduleExtensionList
        ->getAllInstalledInfo());
      $installed_themes = array_keys($this->themeExtensionList
        ->getAllInstalledInfo());
      if (!in_array($extension_name, $installed_modules) && !in_array($extension_name, $installed_themes)) {
        return NULL;
      }
    }
    try {
      $dependency_libraries = $this->libraryDiscoveryParser
        ->buildByExtension($extension_name);
    } catch (\Exception $e) {
      return new DeprecationMessage($e
        ->getMessage());
    }
    if (isset($dependency_libraries[$library_name]) && isset($dependency_libraries[$library_name]['deprecated'])) {
      return str_replace('%library_id%', "{$extension_name}/{$library_name}", $dependency_libraries[$library_name]['deprecated']);
    }
    return FALSE;
  }

}

Classes

Namesort descending Description
LibraryDeprecationAnalyzer A library deprecation analyzer.