You are here

public function DeprecationAnalyzer::analyze in Upgrade Status 8.2

Same name and namespace in other branches
  1. 8.3 src/DeprecationAnalyzer.php \Drupal\upgrade_status\DeprecationAnalyzer::analyze()

Analyze the codebase of an extension including all its sub-components.

Parameters

\Drupal\Core\Extension\Extension $extension: The extension to analyze.

Return value

null Errors are logged to the logger, data is stored to keyvalue storage.

File

src/DeprecationAnalyzer.php, line 256

Class

DeprecationAnalyzer

Namespace

Drupal\upgrade_status

Code

public function analyze(Extension $extension) {
  try {
    $this
      ->initEnvironment();
  } catch (\Exception $e) {

    // Should not get here as integrations are expected to invoke
    // initEnvironment() first by itself to ensure the environment
    // is going to work when needed (and inform users about any
    // issues). That said, if they did not do that and there was
    // no issue with the environment, then they are lucky.
    return;
  }
  $project_dir = DRUPAL_ROOT . '/' . $extension
    ->getPath();
  $this->logger
    ->notice('Processing %path.', [
    '%path' => $project_dir,
  ]);
  $output = [];
  exec($this->binPath . '/phpstan analyse --error-format=json -c ' . $this->phpstanNeonPath . ' ' . $project_dir, $output);
  $json = json_decode(implode('', $output), TRUE);
  if (!isset($json['files']) || !is_array($json['files'])) {
    $this->logger
      ->error('PHPStan failed: %results', [
      '%results' => print_r($output, TRUE),
    ]);
    $json = [
      'files' => [
        'PHPStan failed' => 'PHP API deprecations cannot be checked. Reason: ' . print_r($output, TRUE),
        'line' => 0,
      ],
      'totals' => [
        'errors' => 1,
        'file_errors' => 1,
      ],
    ];
  }
  $result = [
    'date' => $this->time
      ->getRequestTime(),
    'data' => $json,
  ];
  $twig_deprecations = $this
    ->analyzeTwigTemplates($extension
    ->getPath());
  foreach ($twig_deprecations as $twig_deprecation) {
    preg_match('/\\s([a-zA-Z0-9\\_\\-\\/]+.html\\.twig)\\s/', $twig_deprecation, $file_matches);
    preg_match('/\\s(\\d).?$/', $twig_deprecation, $line_matches);
    $twig_deprecation = preg_replace('! in (.+)\\.twig at line \\d+\\.!', '.', $twig_deprecation);
    $twig_deprecation .= ' See https://drupal.org/node/3071078.';
    $result['data']['files'][$file_matches[1]]['messages'] = [
      [
        'message' => $twig_deprecation,
        'line' => $line_matches[1] ?: 0,
      ],
    ];
    $result['data']['totals']['errors']++;
    $result['data']['totals']['file_errors']++;
  }
  $deprecation_messages = $this->libraryDeprecationAnalyzer
    ->analyze($extension);
  foreach ($deprecation_messages as $deprecation_message) {
    $result['data']['files'][$deprecation_message
      ->getFile()]['messages'][] = [
      'message' => $deprecation_message
        ->getMessage(),
      'line' => $deprecation_message
        ->getLine(),
    ];
    $result['data']['totals']['errors']++;
    $result['data']['totals']['file_errors']++;
  }
  $theme_function_deprecations = $this->themeFunctionDeprecationAnalyzer
    ->analyze($extension);
  foreach ($theme_function_deprecations as $deprecation_message) {
    $result['data']['files'][$deprecation_message
      ->getFile()]['messages'][] = [
      'message' => $deprecation_message
        ->getMessage(),
      'line' => $deprecation_message
        ->getLine(),
    ];
    $result['data']['totals']['errors']++;
    $result['data']['totals']['file_errors']++;
  }

  // Assume this project is Drupal 9 ready unless proven otherwise.
  $result['data']['totals']['upgrade_status_split']['declared_ready'] = TRUE;
  $info_files = $this
    ->getSubExtensionInfoFiles($project_dir);
  foreach ($info_files as $info_file) {
    try {

      // Manually add on info file incompatibility to results. Reding
      // .info.yml files directly, not from extension discovery because that
      // is cached.
      $info = Yaml::decode(file_get_contents($info_file)) ?: [];
      if (!empty($info['package']) && $info['package'] == 'Testing' && !strpos($info_file, '/upgrade_status_test')) {

        // If this info file was for a testing project other than our own
        // testing projects, ignore it.
        continue;
      }
      $error_path = str_replace(DRUPAL_ROOT . '/', '', $info_file);
      if (!isset($info['core_version_requirement'])) {
        $result['data']['files'][$error_path]['messages'][] = [
          'message' => "Add core_version_requirement: ^8 || ^9 to designate that the module is compatible with Drupal 9. See https://drupal.org/node/3070687.",
          'line' => 0,
        ];
        $result['data']['totals']['errors']++;
        $result['data']['totals']['file_errors']++;
        $result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
      }
      elseif (!Semver::satisfies('9.0.0', $info['core_version_requirement'])) {
        $result['data']['files'][$error_path]['messages'][] = [
          'message' => "Value of core_version_requirement: {$info['core_version_requirement']} is not compatible with Drupal 9.0.0. See https://drupal.org/node/3070687.",
          'line' => 0,
        ];
        $result['data']['totals']['errors']++;
        $result['data']['totals']['file_errors']++;
        $result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
      }
    } catch (InvalidDataTypeException $e) {
      $result['data']['files'][$error_path]['messages'][] = [
        'message' => 'Parse error. ' . $e
          ->getMessage(),
        'line' => 0,
      ];
      $result['data']['totals']['errors']++;
      $result['data']['totals']['file_errors']++;
      $result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
    }
  }

  // Manually add on composer.json file incompatibility to results.
  if (file_exists($project_dir . '/composer.json')) {
    $composer_json = json_decode(file_get_contents($project_dir . '/composer.json'));
    if (empty($composer_json) || !is_object($composer_json)) {
      $result['data']['files'][$extension
        ->getPath() . '/composer.json']['messages'][] = [
        'message' => "Parse error in composer.json. Having a composer.json is not a requirement for Drupal 9 compatibility but if there is one, it should be valid. See https://drupal.org/node/2514612.",
        'line' => 0,
      ];
      $result['data']['totals']['errors']++;
      $result['data']['totals']['file_errors']++;
      $result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
    }
    elseif (!empty($composer_json->require->{'drupal/core'}) && !Semver::satisfies('9.0.0', $composer_json->require->{'drupal/core'})) {
      $result['data']['files'][$extension
        ->getPath() . '/composer.json']['messages'][] = [
        'message' => "The drupal/core requirement is not Drupal 9 compatible. Either remove it or update it to be compatible with Drupal 9. See https://drupal.org/node/2514612#s-drupal-9-compatibility.",
        'line' => 0,
      ];
      $result['data']['totals']['errors']++;
      $result['data']['totals']['file_errors']++;
      $result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
    }
  }
  foreach ($result['data']['files'] as $path => &$errors) {
    if (!empty($errors['messages'])) {
      foreach ($errors['messages'] as &$error) {

        // Overwrite message with processed text. Save category.
        [
          $message,
          $category,
        ] = $this
          ->categorizeMessage($error['message'], $extension);
        $error['message'] = $message;
        $error['upgrade_status_category'] = $category;

        // Sum up the error based on the category it ended up in. Split the
        // categories into two high level buckets needing attention now or
        // later for Drupal 9 compatibility. Ignore Drupal 10 here.
        @$result['data']['totals']['upgrade_status_category'][$category]++;
        if (in_array($category, [
          'safe',
          'old',
          'rector',
        ])) {
          @$result['data']['totals']['upgrade_status_split']['error']++;
        }
        elseif (in_array($category, [
          'later',
          'uncategorized',
        ])) {
          @$result['data']['totals']['upgrade_status_split']['warning']++;
        }
      }
    }
  }

  // For contributed projects, attempt to grab Drupal 9 plan information.
  if (!empty($extension->info['project'])) {
    try {

      /** @var \Psr\Http\Message\ResponseInterface $response */
      $response = $this->httpClient
        ->request('GET', 'https://www.drupal.org/api-d7/node.json?field_project_machine_name=' . $extension
        ->getName());
      if ($response
        ->getStatusCode()) {
        $data = json_decode($response
          ->getBody(), TRUE);
        if (!empty($data['list'][0]['field_next_major_version_info']['value'])) {
          $result['plans'] = str_replace('href="/', 'href="https://drupal.org/', $data['list'][0]['field_next_major_version_info']['value']);

          // @todo implement "replaced by" collection once drupal.org exposes
          // that in an accessible way
          // @todo once/if drupal.org deprecation testing is in place, grab
          // the status from there so we know if it improves by updating
        }
      }
    } catch (\Exception $e) {
      $this->logger
        ->error($e
        ->getMessage());
    }
  }

  // Store the analysis results in our storage bin.
  $this->scanResultStorage
    ->set($extension
    ->getName(), json_encode($result));
}