Commit 078e6b54 authored by Gábor Hojtsy's avatar Gábor Hojtsy
Browse files

Issue #3129430 by Gábor Hojtsy, eojthebrave, ndeet, fgm, DamienMcKenna: When...

Issue #3129430 by Gábor Hojtsy, eojthebrave, ndeet, fgm, DamienMcKenna: When phpstan is not found, the error is too subtle, does not support custom binary path
parent c1e59f51
......@@ -10,6 +10,7 @@ use Drupal\Core\Extension\Extension;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Template\TwigEnvironment;
use DrupalFinder\DrupalFinder;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
......@@ -50,6 +51,13 @@ final class DeprecationAnalyzer {
*/
protected $vendorPath;
/**
* Path to the binaries.
*
* @var string
*/
protected $binPath;
/**
* Temporary directory to use for running phpstan.
*
......@@ -99,6 +107,20 @@ final class DeprecationAnalyzer {
*/
protected $time;
/**
* Drupal project finder.
*
* @var \DrupalFinder\DrupalFinder
*/
protected $finder;
/**
* Whether the analyzer environment is initialized.
*
* @var bool
*/
protected $environmentInitialized = FALSE;
/**
* Constructs a deprecation analyzer.
*
......@@ -137,8 +159,25 @@ final class DeprecationAnalyzer {
$this->libraryDeprecationAnalyzer = $library_deprecation_analyzer;
$this->themeFunctionDeprecationAnalyzer = $theme_function_deprecation_analyzer;
$this->time = $time;
}
/**
* Initialize the external environment.
*
* @throws \Exception
* In case initialization failed. The analyzer will not work in this case.
*/
public function initEnvironment() {
if (!empty($this->environmentInitialized)) {
// Already successfully initialized, no need to do it again.
return;
}
$this->finder = new DrupalFinder();
$this->finder->locateRoot(DRUPAL_ROOT);
$this->vendorPath = $this->findVendorPath();
$this->vendorPath = $this->finder->getVendorDir();
$this->binPath = $this->findBinPath();
if (function_exists('file_directory_temp')) {
// This is fallback code for 8.7.x and below. It's not called on later
......@@ -157,25 +196,52 @@ final class DeprecationAnalyzer {
$this->phpstanNeonPath = $this->temporaryDirectory . '/deprecation_testing.neon';
$this->createModifiedNeonFile();
$this->environmentInitialized = TRUE;
}
/**
* Finds vendor location.
* Finds bin-dir location.
*
* This can be set in composer.json via `bin-dir` config and may not be inside
* vendor directory.
*
* @return string|null
* Vendor directory path if found, null otherwise.
* @return string
* Bin directory path if found.
*
* @throws \Exception
*/
protected function findVendorPath() {
// The vendor directory may be found inside the webroot (unlikely).
if (file_exists(DRUPAL_ROOT . '/vendor/bin/phpstan')) {
return DRUPAL_ROOT . '/vendor';
protected function findBinPath() {
// The bin directory may be found inside the vendor directory.
if (file_exists($this->vendorPath . '/bin/phpstan')) {
return $this->vendorPath . '/bin';
}
else {
$attempted_paths = [$this->vendorPath . '/bin/phpstan'];
}
// See if we can locate a custom bin directory based on composer.json
// settings.
$composerFileName = trim(getenv('COMPOSER')) ?: 'composer.json';
$rootComposer = $this->finder->getComposerRoot() . '/' . $composerFileName;
$json = json_decode(file_get_contents($rootComposer), TRUE);
if (is_null($json)) {
throw new \Exception('Unable to decode composer information from ' . $rootComposer);
}
// Most likely the vendor directory is found alongside the webroot.
elseif (file_exists(dirname(DRUPAL_ROOT) . '/vendor/bin/phpstan')) {
return dirname(DRUPAL_ROOT) . '/vendor';
if (is_array($json) && isset($json['config']['bin-dir'])) {
$binPath = $this->finder->getComposerRoot() . '/' . $json['config']['bin-dir'];
if (file_exists($binPath . '/phpstan')) {
return $binPath;
}
else {
$attempted_paths[] = $binPath . '/phpstan';
}
}
// One of the above should have worked.
$this->logger->error('PHPStan executable not found.');
// Bail here as continuing makes no sense.
throw new \Exception('Vendor binary path not correct or phpstan is not installed there. Checked: ' . join(',', $attempted_paths));
}
/**
......@@ -188,15 +254,31 @@ final class DeprecationAnalyzer {
* Errors are logged to the logger, data is stored to keyvalue storage.
*/
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->vendorPath . '/bin/phpstan analyse --error-format=json -c ' . $this->phpstanNeonPath . ' ' . $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($result, TRUE)]);
$json = ['files' => [], 'totals' => ['file_errors' => 0]];
}
$result = [
'date' => $this->time->getRequestTime(),
'data' => $json ?? ['files' => [], 'totals' => ['file_errors' => 0]],
'data' => $json,
];
$twig_deprecations = $this->analyzeTwigTemplates($extension->getPath());
......@@ -360,31 +442,27 @@ final class DeprecationAnalyzer {
* dynamically set a temporary directory for PHPStan's cache in the neon file
* provided by Upgrade Status.
*
* @return bool
* True if the temporary directory is created, false if not.
* @throws \Exception
* If creating the temporary directory failed.
*/
protected function prepareTempDirectory() {
$success = $this->fileSystem->prepareDirectory($this->temporaryDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
if (!$success) {
$this->logger->error('Unable to create temporary directory for Upgrade Status: %directory.', ['%directory' => $this->temporaryDirectory]);
return $success;
throw new \Exception('Unable to create temporary directory for Upgrade Status at ' . $this->temporaryDirectory);
}
$phpstan_cache_directory = $this->temporaryDirectory . '/phpstan';
$success = $this->fileSystem->prepareDirectory($phpstan_cache_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
if (!$success) {
$this->logger->error('Unable to create temporary directory for PHPStan: %directory.', ['%directory' => $phpstan_cache_directory]);
throw new \Exception('Unable to create temporary directory for PHPStan at ' . $phpstan_cache_directory);
}
return $success;
}
/**
* Creates the final config file in the temporary directory.
*
* @return bool
* @throws \Exception
* If the PHPStan configuration file cannot be written.
*/
protected function createModifiedNeonFile() {
$module_path = DRUPAL_ROOT . '/' . drupal_get_path('module', 'upgrade_status');
......@@ -400,9 +478,8 @@ final class DeprecationAnalyzer {
$this->vendorPath . "/phpstan/phpstan-deprecation-rules/rules.neon'\n";
$success = file_put_contents($this->phpstanNeonPath, $config);
if (!$success) {
$this->logger->error('Unable to write configuration for PHPStan: %file.', ['%file' => $this->phpstanNeonPath]);
throw new \Exception('Unable to write configuration for PHPStan to ' . $this->phpstanNeonPath);
}
return $success ? TRUE : FALSE;
}
/**
......
......@@ -11,6 +11,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactory;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\upgrade_status\DeprecationAnalyzer;
use Drupal\upgrade_status\ProjectCollector;
use Drupal\upgrade_status\ScanResultFormatter;
use GuzzleHttp\Cookie\CookieJar;
......@@ -63,6 +64,13 @@ class UpgradeStatusForm extends FormBase {
*/
protected $moduleHandler;
/**
* The deprecation analyzer.
*
* @var \Drupal\upgrade_status\DeprecationAnalyzer
*/
protected $deprecationAnalyzer;
/**
* {@inheritdoc}
*/
......@@ -73,7 +81,8 @@ class UpgradeStatusForm extends FormBase {
$container->get('upgrade_status.result_formatter'),
$container->get('renderer'),
$container->get('logger.channel.upgrade_status'),
$container->get('module_handler')
$container->get('module_handler'),
$container->get('upgrade_status.deprecation_analyzer')
);
}
......@@ -92,6 +101,8 @@ class UpgradeStatusForm extends FormBase {
* The logger.
* @param \Drupal\Core\Extension\ModuleHandler $module_handler
* The module handler.
* @param \Drupal\upgrade_status\DeprecationAnalyzer $deprecation_analyzer
* The deprecation analyzer.
*/
public function __construct(
ProjectCollector $project_collector,
......@@ -99,7 +110,8 @@ class UpgradeStatusForm extends FormBase {
ScanResultFormatter $result_formatter,
RendererInterface $renderer,
LoggerInterface $logger,
ModuleHandler $module_handler
ModuleHandler $module_handler,
DeprecationAnalyzer $deprecation_analyzer
) {
$this->projectCollector = $project_collector;
$this->releaseStore = $key_value_expirable->get('update_available_releases');
......@@ -107,6 +119,7 @@ class UpgradeStatusForm extends FormBase {
$this->renderer = $renderer;
$this->logger = $logger;
$this->moduleHandler = $module_handler;
$this->deprecationAnalyzer = $deprecation_analyzer;
}
/**
......@@ -130,6 +143,15 @@ class UpgradeStatusForm extends FormBase {
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'upgrade_status/upgrade_status.admin';
$analyzerReady = TRUE;
try {
$this->deprecationAnalyzer->initEnvironment();
}
catch (\Exception $e) {
$analyzerReady = FALSE;
$this->messenger()->addError($e->getMessage());
}
$form['environment'] = [
'#type' => 'details',
'#title' => $this->t('Drupal core and hosting environment'),
......@@ -178,18 +200,21 @@ class UpgradeStatusForm extends FormBase {
'#value' => $this->t('Scan selected'),
'#weight' => 2,
'#button_type' => 'primary',
'#disabled' => !$analyzerReady,
];
$form['drupal_upgrade_status_form']['action']['export'] = [
'#type' => 'submit',
'#value' => $this->t('Export as HTML'),
'#weight' => 5,
'#submit' => [[$this, 'exportReportHTML']],
'#disabled' => !$analyzerReady,
];
$form['drupal_upgrade_status_form']['action']['export_ascii'] = [
'#type' => 'submit',
'#value' => $this->t('Export as ASCII'),
'#weight' => 6,
'#submit' => [[$this, 'exportReportASCII']],
'#disabled' => !$analyzerReady,
];
return $form;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment