public function DeprecationAnalyzer::analyze in Upgrade Status 8.3
Same name and namespace in other branches
- 8.2 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 263
Class
Namespace
Drupal\upgrade_statusCode
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 = [];
$error_filename = $this->temporaryDirectory . '/phpstan_error_output';
$command = $this->binPath . '/phpstan analyse --memory-limit=-1 --error-format=json -c ' . $this->phpstanNeonPath . ' ' . $project_dir . ' 2> ' . $error_filename;
exec($command, $output);
$json = json_decode(implode('', $output), TRUE);
if (!isset($json['files']) || !is_array($json['files'])) {
$stdout = trim(implode('', $output)) ?: 'Empty.';
$stderr = trim(file_get_contents($error_filename)) ?: 'Empty.';
$formatted_error = "<h6>PHPStan command failed:</h6> <p>" . $command . "</p> <h6>Command output:</h6> <p>" . $stdout . "</p> <h6>Command error:</h6> <p>" . $stderr . '</p>';
$this->logger
->error('%phpstan_fail', [
'%phpstan_fail' => strip_tags($formatted_error),
]);
$json = [
'files' => [
// Add a failure message with the nonexistent 'PHPStan failed'
// filename, so the error conforms to the expected format.
'PHPStan failed' => [
'messages' => [
[
'message' => $formatted_error,
'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 ready for the next major core version 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. Reading
// .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);
// Check for missing base theme key.
if ($info['type'] === 'theme') {
if (!isset($info['base theme'])) {
$result['data']['files'][$error_path]['messages'][] = [
'message' => "The now required 'base theme' key is missing. See https://www.drupal.org/node/3066038.",
'line' => 0,
];
$result['data']['totals']['errors']++;
$result['data']['totals']['file_errors']++;
}
}
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 (!ProjectCollector::isCompatibleWithNextMajorDrupal($info['core_version_requirement'])) {
$result['data']['files'][$error_path]['messages'][] = [
'message' => "Value of core_version_requirement: {$info['core_version_requirement']} is not compatible with the next major version of Drupal core. 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;
}
// @todo
// Change values to ExtensionLifecycle class constants once at least
// Drupal 9.3 is required.
if (!empty($info['lifecycle'])) {
$link = !empty($info['lifecycle_link']) ? $info['lifecycle_link'] : 'https://www.drupal.org/node/3215042';
if ($info['lifecycle'] == 'deprecated') {
$result['data']['files'][$error_path]['messages'][] = [
'message' => "This extension is deprecated. Don't use it. See {$link}.",
'line' => 0,
];
$result['data']['totals']['errors']++;
$result['data']['totals']['file_errors']++;
$result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
}
elseif ($info['lifecycle'] == 'obsolete') {
$result['data']['files'][$error_path]['messages'][] = [
'message' => "This extension is obsolete. Obsolete extensions are usually uninstalled automatically when not needed anymore. You only need to do something about this if the uninstallation was unsuccesful. See {$link}.",
'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;
}
// No need to check info files for PHP 8 compatibility information because
// they can only define minimal PHP versions not maximum or excluded PHP
// versions.
}
// 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 in general, 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'}) && !projectCollector::isCompatibleWithNextMajorDrupal($composer_json->require->{'drupal/core'})) {
$result['data']['files'][$extension
->getPath() . '/composer.json']['messages'][] = [
'message' => "The drupal/core requirement is not compatible with the next major version of Drupal. Either remove it or update it to be compatible. 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;
}
elseif (projectCollector::getDrupalCoreMajorVersion() > 8 && !empty($composer_json->require->{'php'} && !projectCollector::isCompatibleWithPHP8($composer_json->require->{'php'}))) {
$result['data']['files'][$extension
->getPath() . '/composer.json']['messages'][] = [
'message' => "The PHP requirement is not compatible with PHP 8. Once the codebase is actually compatible, either remove this limitation or update it to be compatible.",
'line' => 0,
];
$result['data']['totals']['errors']++;
$result['data']['totals']['file_errors']++;
$result['data']['totals']['upgrade_status_split']['declared_ready'] = FALSE;
}
}
// Assume next step is to relax (there were no errors found).
$result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_RELAX;
foreach ($result['data']['files'] as $path => &$errors) {
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;
// If the category was 'rector' that means at least one error was
// identified as covered by rector, so next step should be to run
// rector on this project.
if ($category == 'rector') {
$result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_RECTOR;
}
elseif ($result['data']['totals']['upgrade_status_next'] == ProjectCollector::NEXT_RELAX) {
$result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_MANUAL;
}
// 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 compatibility with the next major version. Issues in the
// 'ignore' category are intentionally not counted in either.
@$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 upgrade 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(), $result);
}