ai_upgrade_assistant-0.2.0-alpha2/src/Service/ProjectAnalyzer.php
src/Service/ProjectAnalyzer.php
<?php
namespace Drupal\ai_upgrade_assistant\Service;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Service for analyzing Drupal projects for upgrade compatibility.
*/
class ProjectAnalyzer {
use DependencySerializationTrait;
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The logger factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The HuggingFace service.
*
* @var \Drupal\ai_upgrade_assistant\Service\HuggingFaceService
*/
protected $huggingFace;
/**
* The PHP parser service.
*
* @var \Drupal\ai_upgrade_assistant\Service\PhpParserService
*/
protected $phpParser;
/**
* The AI model manager.
*
* @var \Drupal\ai_upgrade_assistant\Service\AiModelManager
*/
protected $aiModelManager;
/**
* The community learning service.
*
* @var \Drupal\ai_upgrade_assistant\Service\CommunityLearningService
*/
protected $communityLearning;
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The analysis tracker service.
*
* @var \Drupal\ai_upgrade_assistant\Service\AnalysisTracker
*/
protected $analysisTracker;
/**
* Constructs a new ProjectAnalyzer object.
*/
public function __construct(
ModuleHandlerInterface $moduleHandler,
ModuleExtensionList $moduleExtensionList,
ConfigFactoryInterface $configFactory,
FileSystemInterface $fileSystem,
LoggerChannelFactoryInterface $loggerFactory,
HuggingFaceService $huggingFace,
PhpParserService $phpParser,
AiModelManager $aiModelManager,
CommunityLearningService $communityLearning,
ClientInterface $httpClient,
AnalysisTracker $analysisTracker
) {
$this->moduleHandler = $moduleHandler;
$this->moduleExtensionList = $moduleExtensionList;
$this->configFactory = $configFactory;
$this->fileSystem = $fileSystem;
$this->loggerFactory = $loggerFactory;
$this->huggingFace = $huggingFace;
$this->phpParser = $phpParser;
$this->aiModelManager = $aiModelManager;
$this->communityLearning = $communityLearning;
$this->httpClient = $httpClient;
$this->analysisTracker = $analysisTracker;
}
/**
* Creates a new instance of the service.
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('extension.list.module'),
$container->get('config.factory'),
$container->get('file_system'),
$container->get('logger.factory'),
$container->get('ai_upgrade_assistant.huggingface'),
$container->get('ai_upgrade_assistant.php_parser'),
$container->get('ai_upgrade_assistant.ai_model_manager'),
$container->get('ai_upgrade_assistant.community_learning'),
$container->get('http_client'),
$container->get('ai_upgrade_assistant.analysis_tracker')
);
}
/**
* Analyzes code for potential upgrade issues.
*
* @param string $module
* The name of the module to analyze.
* @param string $path
* Optional path to analyze. If not provided, will use module path.
*
* @return array
* Analysis results keyed by file path.
*/
public function analyzeCode($module, $path = NULL) {
$results = [];
try {
// Get module info if path not provided
if (!$path) {
$module_data = $this->moduleExtensionList->get($module);
if (!$module_data) {
throw new \Exception('Module not found: ' . $module);
}
$path = $module_data->getPath();
}
// Ensure path exists
if (!file_exists($path)) {
throw new \Exception('Path does not exist: ' . $path);
}
// Find all PHP files
$files = is_dir($path) ? $this->findPhpFiles($path) : [$path];
foreach ($files as $file) {
try {
if (!file_exists($file)) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('File does not exist: @file', ['@file' => $file]);
continue;
}
$code_string = file_get_contents($file);
if ($code_string === FALSE) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('Could not read file: @file', ['@file' => $file]);
continue;
}
$ast = $this->phpParser->parseFile($file);
if (!$ast) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('Could not parse file @file', ['@file' => $file]);
continue;
}
$file_config = $this->buildCodeContext($ast, $file);
$patterns = $this->communityLearning->findSimilarPatterns($file);
if ($patterns) {
$file_config['patterns'] = array_map(function ($pattern) {
return is_object($pattern) && isset($pattern->pattern_data)
? unserialize($pattern->pattern_data)
: [];
}, $patterns);
}
// Try AI analysis first, fall back to basic analysis if not available
try {
$results[$file] = $this->huggingFace->analyzeCode($code_string, $file_config);
}
catch (\Exception $e) {
if (strpos($e->getMessage(), 'subscription') !== FALSE) {
// Perform basic analysis without AI
$results[$file] = $this->performBasicAnalysis($ast, $file);
}
else {
throw $e;
}
}
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error analyzing file @file: @error', [
'@file' => $file,
'@error' => $e->getMessage(),
]);
$results[$file] = [
'status' => 'error',
'message' => $e->getMessage(),
];
}
}
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error analyzing module @module: @error', [
'@module' => $module,
'@error' => $e->getMessage(),
]);
return [
'status' => 'error',
'message' => $e->getMessage(),
];
}
return $results;
}
/**
* Analyzes security issues for a module.
*/
public function analyzeSecurityIssues($module) {
try {
$module_data = $this->moduleExtensionList->get($module);
if (!$module_data) {
throw new \Exception('Module not found: ' . $module);
}
$advisories = $this->fetchSecurityAdvisories($module);
$code_issues = [];
$path = $module_data->getPath();
$files = $this->findPhpFiles($path);
foreach ($files as $file) {
try {
$ast = $this->phpParser->parseFile($file);
if (!$ast) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('Unable to parse file @file', ['@file' => $file]);
continue;
}
$file_issues = $this->analyzeCodeSecurity($ast, $file);
if (!empty($file_issues)) {
$code_issues[$file] = $file_issues;
}
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error analyzing file @file: @error', [
'@file' => $file,
'@error' => $e->getMessage(),
]);
}
}
$risk_level = $this->calculateSecurityRiskLevel($advisories, $code_issues);
return [
'status' => 'analyzed',
'risk_level' => $risk_level,
'advisories' => $advisories,
'code_issues' => $code_issues,
];
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error analyzing security for module @module: @error', [
'@module' => $module,
'@error' => $e->getMessage(),
]);
return [
'status' => 'error',
'message' => $e->getMessage(),
];
}
}
/**
* Analyzes dependencies for a module.
*
* @param string $module
* The name of the module to analyze.
*
* @return array
* An array containing dependency analysis results.
*
* @throws \Exception
* If the module is not found or other errors occur.
*/
public function analyzeDependencies($module) {
try {
$module_data = $this->moduleExtensionList->get($module);
if (!$module_data) {
throw new \Exception('Module not found: ' . $module);
}
$info = $module_data->info;
$results = [
'core' => [],
'contrib' => [],
'custom' => [],
'themes' => [],
'libraries' => [],
'status' => 'success',
];
// Check core version compatibility
if (isset($info['core_version_requirement'])) {
$results['core']['version_requirement'] = $info['core_version_requirement'];
}
elseif (isset($info['core'])) {
$results['core']['version_requirement'] = $info['core'];
}
// Analyze module dependencies
if (isset($info['dependencies'])) {
foreach ($info['dependencies'] as $dependency) {
$dependency_parts = explode(':', $dependency);
$dependency_name = end($dependency_parts);
$dependency_type = count($dependency_parts) > 1 ? $dependency_parts[0] : 'module';
try {
$dep_info = $this->moduleExtensionList->get($dependency_name);
$is_custom = strpos($dep_info->getPath(), 'modules/custom') !== FALSE;
$dep_data = [
'name' => $dependency_name,
'type' => $dependency_type,
'version' => isset($dep_info->info['version']) ? $dep_info->info['version'] : NULL,
'status' => $this->moduleHandler->moduleExists($dependency_name) ? 'enabled' : 'disabled',
'path' => $dep_info->getPath(),
];
if ($is_custom) {
$results['custom'][$dependency_name] = $dep_data;
}
else {
$results['contrib'][$dependency_name] = $dep_data;
}
}
catch (\Exception $e) {
$results['missing'][$dependency_name] = [
'name' => $dependency_name,
'type' => $dependency_type,
'error' => $e->getMessage(),
];
}
}
}
// Analyze theme dependencies
if (isset($info['themes'])) {
foreach ($info['themes'] as $theme) {
$results['themes'][$theme] = [
'name' => $theme,
'status' => \Drupal::service('theme_handler')->themeExists($theme) ? 'installed' : 'missing',
];
}
}
// Analyze library dependencies
if (isset($info['libraries'])) {
foreach ($info['libraries'] as $library) {
$library_exists = \Drupal::service('library.discovery')->getLibraryByName($module, $library);
$results['libraries'][$library] = [
'name' => $library,
'status' => $library_exists ? 'found' : 'missing',
];
}
}
return $results;
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error analyzing dependencies for module @module: @error', [
'@module' => $module,
'@error' => $e->getMessage(),
]);
return [
'status' => 'error',
'message' => $e->getMessage(),
];
}
}
/**
* Gets detailed module information.
*/
public function getModuleInfo($module_name) {
try {
// Get module data from extension list
$module = $this->moduleExtensionList->get($module_name);
if (!$module) {
throw new \Exception("Module $module_name not found");
}
$info = $module->info;
// Get project info from update status or drupal.org
$project_info = $this->getProjectInfo($module_name);
// Determine if this is a custom module
$path = $module->getPath();
$is_custom = strpos($path, 'modules/custom') !== FALSE;
// Get installed status
$is_installed = $this->moduleHandler->moduleExists($module_name);
// Build module info array
$module_info = [
'name' => $info['name'] ?? $module_name,
'machine_name' => $module_name,
'type' => $is_custom ? 'custom' : 'contrib',
'status' => $is_installed ? 'enabled' : 'disabled',
'version' => $project_info['existing_version'] ?? $info['version'] ?? null,
'description' => $info['description'] ?? '',
'project_url' => $is_custom ? null : 'https://www.drupal.org/project/' . $module_name,
'recommended_version' => $project_info['recommended'] ?? null,
'composer_name' => $project_info['composer_name'] ?? ($is_custom ? null : 'drupal/' . $module_name),
'path' => $path,
'core_compatibility' => $info['core_version_requirement'] ?? $info['core'] ?? null,
'php_version' => $info['php'] ?? null,
'dependencies' => $info['dependencies'] ?? [],
'package' => $info['package'] ?? null,
];
// Add helpful message if version unknown
if (!$module_info['version']) {
if ($is_custom) {
$module_info['version'] = 'custom module';
} else {
$module_info['version'] = 'unknown (enable Update module for version info)';
}
}
return $module_info;
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error getting module info for @module: @error', [
'@module' => $module_name,
'@error' => $e->getMessage(),
]);
// Return basic info even on error
return [
'name' => $module_name,
'machine_name' => $module_name,
'version' => 'unknown',
'error' => $e->getMessage(),
];
}
}
/**
* Gets project update information.
*/
protected function getProjectInfo($module_name) {
try {
// First try update module if available
if ($this->moduleHandler->moduleExists('update')) {
$update_manager = \Drupal::service('update.manager');
$available = update_get_available(TRUE);
$project_data = update_calculate_project_data($available);
if (isset($project_data[$module_name])) {
return $project_data[$module_name];
}
}
// Fallback to direct drupal.org API lookup
$url = "https://updates.drupal.org/release-history/$module_name/current";
try {
$response = $this->httpClient->get($url);
$data = new \SimpleXMLElement($response->getBody());
if ($data->project) {
$recommended = $data->xpath('//release[not(version_extra)]/version')[0] ?? null;
return [
'existing_version' => (string)$data->project->releases->release[0]->version ?? null,
'recommended' => $recommended ? (string)$recommended : null,
'composer_name' => 'drupal/' . $module_name,
];
}
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('Error fetching project info from drupal.org for @module: @error', [
'@module' => $module_name,
'@error' => $e->getMessage(),
]);
}
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->warning('Error getting project info for @module: @error', [
'@module' => $module_name,
'@error' => $e->getMessage(),
]);
}
return [];
}
/**
* Analyzes a specific module.
*/
public function analyzeModule($module_name) {
// Get module info
$module_info = $this->getModuleInfo($module_name);
$module_path = $this->moduleExtensionList->get($module_name)->getPath();
// Analyze compatibility
$compatibility = $this->checkCompatibility($module_name);
$compatibility_score = $this->calculateCompatibilityScore($compatibility, []);
// Analyze code quality
$code_quality = $this->analyzeCodeQuality($module_path);
// Collect all issues
$issues = [];
// Add compatibility issues
if (!empty($compatibility['issues'])) {
foreach ($compatibility['issues'] as $issue) {
$issues[] = [
'severity' => $issue['severity'],
'title' => $issue['title'],
'message' => $issue['message'],
'file' => $issue['file'] ?? '',
'line' => $issue['line'] ?? '',
'solution' => $issue['solution'] ?? '',
'type' => 'compatibility',
];
}
}
// Add deprecated code issues
if (!empty($code_quality['deprecated_code'])) {
foreach ($code_quality['deprecated_code'] as $issue) {
$issues[] = [
'severity' => 'warning',
'title' => 'Deprecated Code Usage',
'message' => $issue['message'],
'file' => $issue['file'],
'line' => $issue['line'],
'solution' => $issue['replacement'],
'type' => 'deprecated',
];
}
}
// Add coding standards issues
if (!empty($code_quality['coding_standards'])) {
foreach ($code_quality['coding_standards'] as $issue) {
$issues[] = [
'severity' => 'notice',
'title' => 'Coding Standards Issue',
'message' => $issue['message'],
'file' => $issue['file'],
'line' => $issue['line'],
'solution' => $issue['solution'],
'type' => 'standards',
];
}
}
// Check for security advisories
$security_issues = $this->fetchSecurityAdvisories($module_name);
if (!empty($security_issues)) {
foreach ($security_issues as $issue) {
$issues[] = [
'severity' => 'critical',
'title' => 'Security Advisory',
'message' => $issue['title'],
'solution' => $issue['solution'],
'type' => 'security',
];
}
}
// Generate upgrade commands
$upgrade_commands = $this->generateUpgradeCommands($module_info);
// Generate AI recommendation
$ai_recommendation = $this->generateAIRecommendation($module_name, $compatibility, $issues, $code_quality);
// Return analysis results
return [
'name' => $module_name,
'version' => $module_info['version'],
'recommended_version' => $module_info['recommended_version'],
'status' => $compatibility['status'],
'description' => $module_info['description'],
'project_url' => $module_info['project_url'],
'compatibility_score' => $compatibility_score,
'compatibility_factors' => $this->compatibilityFactors,
'issues' => $issues,
'code_quality' => $code_quality,
'ai_recommendation' => $ai_recommendation,
'upgrade_commands' => $upgrade_commands,
'can_auto_fix' => !empty($code_quality['deprecated_code']) || !empty($code_quality['coding_standards']),
];
}
/**
* Generate upgrade commands for the module.
*/
protected function generateUpgradeCommands($module_info) {
$commands = [];
// Add composer require command if we have a recommended version
if (!empty($module_info['recommended_version'])) {
$commands['composer'] = sprintf(
'composer require %s:%s --update-with-dependencies',
$module_info['composer_name'],
$module_info['recommended_version']
);
}
// Add drush commands
$commands['drush'] = [
'update' => sprintf('drush pm:enable %s -y', $module_info['machine_name']),
'cache' => 'drush cr',
'database' => 'drush updatedb -y',
];
return $commands;
}
/**
* Builds context for code analysis.
*/
protected function buildCodeContext(array $ast, $file) {
return [
'module' => $this->getModuleNameFromPath($file),
'dependencies' => $this->getDependencies($file),
'file_type' => $this->determineFileType($file),
];
}
/**
* Gets the module name from a file path.
*/
protected function getModuleNameFromPath($file_path) {
$parts = explode('/modules/', $file_path);
if (count($parts) < 2) {
return NULL;
}
$module_path = explode('/', $parts[1]);
return $module_path[0];
}
/**
* Determines the type of a file based on its location and content.
*/
protected function determineFileType($file) {
$path_parts = explode('/', $file);
$filename = end($path_parts);
if (strpos($file, '/src/Plugin/') !== FALSE) {
return 'plugin';
}
elseif (strpos($file, '/src/Controller/') !== FALSE) {
return 'controller';
}
elseif (strpos($file, '/src/Form/') !== FALSE) {
return 'form';
}
elseif ($filename === 'module') {
return 'module';
}
elseif ($filename === 'install') {
return 'install';
}
return 'unknown';
}
/**
* Gets dependencies for a file.
*/
public function getDependencies($file) {
$module_name = $this->getModuleNameFromPath($file);
if (!$module_name) {
return [];
}
try {
$module = $this->moduleExtensionList->get($module_name);
$required_by = [];
// Find modules that require this module
foreach ($this->moduleHandler->getModuleList() as $name => $extension) {
if (isset($extension->requires[$module_name])) {
$required_by[] = $name;
}
}
return [
'dependencies' => $module->info['dependencies'] ?? [],
'required_by' => $required_by,
];
}
catch (\Exception $e) {
return [];
}
}
/**
* Analyzes code for security issues.
*/
protected function analyzeCodeSecurity($ast, $file) {
$issues = [];
try {
// Add security analysis logic here
// This is a placeholder for actual security analysis
return $issues;
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error during security analysis of @file: @error', [
'@file' => $file,
'@error' => $e->getMessage(),
]);
return [];
}
}
/**
* Calculates security risk level.
*/
protected function calculateSecurityRiskLevel(array $advisories, array $code_issues) {
$risk_score = 0;
foreach ($advisories as $advisory) {
switch (strtolower($advisory['risk'] ?? 'unknown')) {
case 'critical':
$risk_score += 10;
break;
case 'high':
$risk_score += 7;
break;
case 'moderate':
$risk_score += 4;
break;
case 'low':
$risk_score += 1;
break;
}
}
$risk_score += count($code_issues) * 2;
if ($risk_score >= 10) {
return 'critical';
}
elseif ($risk_score >= 7) {
return 'high';
}
elseif ($risk_score >= 4) {
return 'medium';
}
else {
return 'low';
}
}
/**
* Analyzes code quality and patterns in a module.
*
* @param string $module_name_or_path
* Either the name of the module to analyze or the path to the module.
*
* @return array
* Analysis results with code quality metrics.
*/
protected function analyzeCodeQuality($module_name_or_path) {
$results = [
'deprecated_code' => [],
'coding_standards' => [],
];
// Determine if we were passed a module name or path
$module_path = $module_name_or_path;
if (!is_dir($module_name_or_path)) {
try {
$module = $this->moduleExtensionList->get($module_name_or_path);
$module_path = $module->getPath();
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error(
'Could not find module path for @module: @error',
[
'@module' => $module_name_or_path,
'@error' => $e->getMessage(),
]
);
return $results;
}
}
// Get all PHP files in the module
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($module_path));
$php_files = new \RegexIterator($iterator, '/\.php$/i');
// Common deprecated patterns in Drupal 9
$deprecated_patterns = [
'\Drupal::entityManager\(' => [
'message' => 'entityManager() is deprecated',
'replacement' => 'Use \Drupal::entityTypeManager() instead',
],
'drupal_set_message\(' => [
'message' => 'drupal_set_message() is deprecated',
'replacement' => 'Use \Drupal::messenger()->addMessage() instead',
],
'file_create_url\(' => [
'message' => 'file_create_url() is deprecated',
'replacement' => 'Use \Drupal::service(\'file_url_generator\')->generateAbsoluteString() instead',
],
'format_date\(' => [
'message' => 'format_date() is deprecated',
'replacement' => 'Use \Drupal\Core\Datetime\DrupalDateTime instead',
],
'db_query\(' => [
'message' => 'db_query() is deprecated',
'replacement' => 'Use \Drupal::database()->query() instead',
],
'drupal_add_http_header\(' => [
'message' => 'drupal_add_http_header() is deprecated',
'replacement' => 'Use the Response object to set headers',
],
'url\(' => [
'message' => 'url() is deprecated',
'replacement' => 'Use \Drupal\Core\Url::fromRoute() instead',
],
'file_unmanaged_save_data\(' => [
'message' => 'file_unmanaged_save_data() is deprecated',
'replacement' => 'Use \Drupal::service(\'file.repository\')->writeData() instead',
],
'file_copy\(' => [
'message' => 'file_copy() is deprecated',
'replacement' => 'Use \Drupal::service(\'file.repository\')->copy() instead',
],
'watchdog\(' => [
'message' => 'watchdog() is deprecated',
'replacement' => 'Use the logger.factory service instead',
],
'\$_SESSION' => [
'message' => 'Direct $_SESSION usage is deprecated',
'replacement' => 'Use \Drupal::service(\'tempstore.private\') instead',
],
];
foreach ($php_files as $file) {
$content = file_get_contents($file->getPathname());
$relative_path = str_replace($module_path . '/', '', $file->getPathname());
// Check for deprecated code
foreach ($deprecated_patterns as $pattern => $info) {
if (preg_match('/' . $pattern . '/', $content, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches as $match) {
// Get line number
$line = substr_count(substr($content, 0, $match[1]), "\n") + 1;
$results['deprecated_code'][] = [
'message' => $info['message'],
'file' => $relative_path,
'line' => $line,
'replacement' => $info['replacement'],
];
}
}
}
// Check coding standards
if (strpos($content, '<?php') !== 0) {
$results['coding_standards'][] = [
'message' => 'PHP code must be enclosed by the full PHP tags',
'file' => $relative_path,
'line' => 1,
'solution' => 'Add <?php to the beginning of the file',
];
}
// Check for proper use statements
if (!preg_match('/^namespace.+?;.*?use/s', $content) && strpos($content, 'use ') !== false) {
$results['coding_standards'][] = [
'message' => 'Use statements must be placed after the namespace declaration',
'file' => $relative_path,
'line' => 1,
'solution' => 'Move use statements after the namespace declaration',
];
}
}
return $results;
}
/**
* Performs basic code analysis without AI.
*
* @param array $ast
* The AST of the file.
* @param string $file
* The file path.
*
* @return array
* Basic analysis results.
*/
protected function performBasicAnalysis($ast, $file) {
$results = [
'status' => 'success',
'file' => $file,
'analysis' => [
'deprecated_functions' => [],
'deprecated_hooks' => [],
'deprecated_classes' => [],
'upgrade_suggestions' => [],
],
];
// Traverse AST to find deprecated code
$traverser = new \PhpParser\NodeTraverser();
$visitor = new class extends \PhpParser\NodeVisitorAbstract {
public $deprecated = [];
public function enterNode(\PhpParser\Node $node) {
if ($node instanceof \PhpParser\Node\Stmt\Function_) {
// Check for deprecated functions
foreach ($node->getAttributes() as $attr) {
if (isset($attr['comments'])) {
foreach ($attr['comments'] as $comment) {
if (strpos($comment->getText(), '@deprecated') !== FALSE) {
$this->deprecated['functions'][] = $node->name->toString();
}
}
}
}
}
elseif ($node instanceof \PhpParser\Node\Stmt\Class_) {
// Check for deprecated classes
foreach ($node->getAttributes() as $attr) {
if (isset($attr['comments'])) {
foreach ($attr['comments'] as $comment) {
if (strpos($comment->getText(), '@deprecated') !== FALSE) {
$this->deprecated['classes'][] = $node->name->toString();
}
}
}
}
}
}
};
$traverser->addVisitor($visitor);
$traverser->traverse($ast);
// Add deprecated items to results
if (!empty($visitor->deprecated['functions'])) {
$results['analysis']['deprecated_functions'] = $visitor->deprecated['functions'];
}
if (!empty($visitor->deprecated['classes'])) {
$results['analysis']['deprecated_classes'] = $visitor->deprecated['classes'];
}
// Add basic upgrade suggestions
$results['analysis']['upgrade_suggestions'][] = [
'type' => 'info',
'message' => 'Basic analysis completed. For more detailed analysis, please upgrade to a Pro or Enterprise subscription.',
];
return $results;
}
/**
* Check module compatibility with current Drupal version.
*
* @param string $module_name
* The name of the module to check.
*
* @return array
* Compatibility information.
*/
protected function checkCompatibility($module_name) {
try {
// Get module info using moduleExtensionList
$module_info = $this->moduleExtensionList->getExtensionInfo($module_name);
if (!$module_info) {
return [
'status' => 'UNKNOWN',
'message' => 'Could not get module information',
];
}
// Check core version requirement
$core_requirement = $module_info['core_version_requirement'] ?? $module_info['core'] ?? '';
$current_version = \Drupal::VERSION;
if (empty($core_requirement)) {
return [
'status' => 'UNKNOWN',
'message' => 'No core version requirement specified',
];
}
// Parse version constraint
if (strpos($core_requirement, '^') !== FALSE) {
$min_version = str_replace('^', '', $core_requirement);
if (version_compare($current_version, $min_version, '<')) {
return [
'status' => 'INCOMPATIBLE',
'message' => "Module requires Drupal {$min_version} or higher",
];
}
}
elseif (strpos($core_requirement, '~') !== FALSE) {
$base_version = str_replace('~', '', $core_requirement);
if (version_compare($current_version, $base_version, '<')) {
return [
'status' => 'INCOMPATIBLE',
'message' => "Module requires Drupal {$base_version} or higher",
];
}
}
// Check for deprecated code usage
$code_analysis = $this->analyzeCode($module_name);
$has_deprecations = false;
foreach ($code_analysis as $file_analysis) {
if (!empty($file_analysis['deprecated_code'])) {
$has_deprecations = true;
break;
}
}
if ($has_deprecations) {
return [
'status' => 'NEEDS_UPDATE',
'message' => 'Module uses deprecated code that needs updating',
];
}
return [
'status' => 'COMPATIBLE',
'message' => 'Module is compatible with current Drupal version',
];
}
catch (\Exception $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error('Error checking compatibility for @module: @error', [
'@module' => $module_name,
'@error' => $e->getMessage(),
]);
return [
'status' => 'ERROR',
'message' => $e->getMessage(),
];
}
}
/**
* Check for known issues with a module.
*
* @param string $module_name
* The name of the module to check.
*
* @return array
* Array of known issues.
*/
protected function checkForKnownIssues($module_name) {
$issues = [];
try {
// Check project status from drupal.org
$project_url = "https://www.drupal.org/api/sa/{$module_name}/drupal";
$logger = $this->loggerFactory->get('ai_upgrade_assistant');
$saResponse = $this->httpClient->request('GET', $project_url, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($saResponse->getStatusCode() == 200) {
$saData = json_decode($saResponse->getBody(), TRUE);
if (!empty($saData)) {
foreach ($saData as $advisory) {
$issues[] = [
'title' => $advisory['title'] ?? 'Unknown',
'link' => $advisory['link'] ?? "https://www.drupal.org/sa/{$advisory['sa_id']}",
'severity' => $advisory['risk_level'] ?? 'Unknown',
'version' => $advisory['covered_versions'] ?? 'All',
'date' => isset($advisory['created']) ? strtotime($advisory['created']) : time(),
'description' => $advisory['description'] ?? '',
'solution' => $advisory['solution'] ?? '',
];
}
return $issues;
}
}
// Fallback to the project releases feed
$releaseUrl = "https://www.drupal.org/api/project/{$module_name}/releases";
$logger->debug('Fetching security releases from @url', ['@url' => $releaseUrl]);
$response = $this->httpClient->request('GET', $releaseUrl, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($response->getStatusCode() == 200) {
$data = json_decode($response->getBody(), TRUE);
if (!empty($data)) {
foreach ($data as $release) {
// Only include security releases
if (isset($release['security']['covered']) && $release['security']['covered']) {
$issues[] = [
'title' => $release['title'] ?? 'Unknown',
'link' => $release['link'] ?? '',
'severity' => 'Security',
'version' => $release['version'] ?? 'Unknown',
'date' => isset($release['date']) ? strtotime($release['date']) : time(),
'description' => $release['release_notes'] ?? '',
];
}
}
}
}
// If both methods fail, try the legacy feed as last resort
if (empty($issues)) {
$legacyUrl = "https://www.drupal.org/feeds/project/{$module_name}/sa.json";
$logger->debug('Fetching from legacy feed @url', ['@url' => $legacyUrl]);
$legacyResponse = $this->httpClient->request('GET', $legacyUrl, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($legacyResponse->getStatusCode() == 200) {
$legacyData = json_decode($legacyResponse->getBody(), TRUE);
if (!empty($legacyData)) {
foreach ($legacyData as $advisory) {
$issues[] = [
'title' => $advisory['title'] ?? 'Unknown',
'link' => $advisory['link'] ?? '',
'severity' => $advisory['risk'] ?? 'Unknown',
'version' => $advisory['version'] ?? 'All',
'date' => isset($advisory['created']) ? strtotime($advisory['created']) : time(),
'description' => $advisory['description'] ?? '',
];
}
}
}
}
return $issues;
}
catch (\Exception $e) {
$logger->error(
'Error fetching security advisories for @module: @error',
[
'@module' => $module_name,
'@error' => $e->getMessage(),
]
);
return [];
}
}
/**
* Get severity level number for sorting.
*/
protected function getSeverityLevel($severity) {
$levels = [
'CRITICAL' => 4,
'ERROR' => 3,
'WARNING' => 2,
'NOTICE' => 1,
];
return $levels[strtoupper($severity)] ?? 0;
}
/**
* Get severity icon name.
*/
protected function getSeverityIcon($severity) {
$icons = [
'CRITICAL' => 'error',
'ERROR' => 'warning',
'WARNING' => 'info',
'NOTICE' => 'info_outline',
];
return $icons[strtoupper($severity)] ?? 'help_outline';
}
/**
* Calculate compatibility score with detailed factors.
*/
protected function calculateCompatibilityScore($compatibility, $issues) {
$base_score = 100;
$deductions = [];
// Deduct for compatibility status
if ($compatibility['status'] === 'INCOMPATIBLE') {
$base_score -= 50;
$deductions[] = $this->t('Module is marked as incompatible (-50%)');
}
elseif ($compatibility['status'] === 'NEEDS_UPDATE') {
$base_score -= 25;
$deductions[] = $this->t('Module needs updates (-25%)');
}
// Deduct for issues
foreach ($issues as $issue) {
switch ($issue['severity']) {
case 'critical':
$base_score -= 15;
$deductions[] = $this->t('Critical issue found: @message (-15%)', ['@message' => $issue['message']]);
break;
case 'error':
$base_score -= 10;
$deductions[] = $this->t('Error found: @message (-10%)', ['@message' => $issue['message']]);
break;
case 'warning':
$base_score -= 5;
$deductions[] = $this->t('Warning found: @message (-5%)', ['@message' => $issue['message']]);
break;
}
}
// Ensure score doesn't go below 0
$final_score = max(0, $base_score);
// Store factors in static cache for later use in UI
$this->compatibilityFactors = $deductions;
return $final_score;
}
/**
* Determine upgrade complexity and confidence.
*/
protected function determineUpgradeComplexity($compatibility, $issues, $code_quality) {
$score = 0;
// Add points for various complexity factors
if ($compatibility['status'] === 'INCOMPATIBLE') {
$score += 30;
} elseif ($compatibility['status'] === 'NEEDS_UPDATE') {
$score += 15;
}
// Count critical issues
$critical_count = count(array_filter($issues, function($issue) {
return isset($issue['severity']) && $issue['severity'] === 'CRITICAL';
}));
$score += ($critical_count * 10);
// Count deprecated code patterns
$deprecated_count = count($code_quality['deprecated_code'] ?? []);
$score += ($deprecated_count * 5);
// Count security issues
$security_count = count($code_quality['security_issues'] ?? []);
$score += ($security_count * 8);
// Determine complexity level
$level = 'simple';
$confidence = 'high';
if ($score >= 50) {
$level = 'very_complex';
$confidence = 'moderate';
} elseif ($score >= 30) {
$level = 'complex';
$confidence = 'high';
} elseif ($score >= 15) {
$level = 'moderate';
$confidence = 'very_high';
}
return [
'level' => $level,
'confidence' => $confidence,
];
}
/**
* Generate time estimate based on complexity.
*/
protected function generateTimeEstimate($complexity_level, $steps_count) {
$base_hours = [
'simple' => [1, 4],
'moderate' => [4, 8],
'complex' => [8, 16],
'very_complex' => [16, 40],
];
$range = $base_hours[$complexity_level] ?? [1, 4];
$min_hours = $range[0] + ($steps_count * 0.5);
$max_hours = $range[1] + ($steps_count * 2);
return [
'min' => round($min_hours),
'max' => round($max_hours),
];
}
/**
* Finds all PHP files in a directory.
*
* @param string $directory
* Directory to search in.
*
* @return array
* Array of file paths.
*/
protected function findPhpFiles($directory) {
$files = [];
// Ensure we have an absolute path
if (!$this->fileSystem->realpath($directory)) {
// Try prepending DRUPAL_ROOT
$drupal_root = \Drupal::root();
$full_path = $drupal_root . '/' . $directory;
if (!$this->fileSystem->realpath($full_path)) {
$this->loggerFactory->get('ai_upgrade_assistant')->error(
'Directory not found: @dir',
['@dir' => $directory]
);
return [];
}
$directory = $full_path;
}
if (!is_dir($directory)) {
$this->loggerFactory->get('ai_upgrade_assistant')->error(
'Not a directory: @dir',
['@dir' => $directory]
);
return [];
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}
return $files;
}
/**
* Fetches security advisories for a module.
*
* @param string $module_name
* The name of the module to check.
*
* @return array
* Array of security advisories.
*/
protected function fetchSecurityAdvisories($module_name) {
$advisories = [];
$logger = $this->loggerFactory->get('ai_upgrade_assistant');
try {
// Try the security advisory feed first as it's more reliable
$saUrl = "https://www.drupal.org/api/sa/{$module_name}/drupal";
$logger->debug('Fetching security advisories from @url', ['@url' => $saUrl]);
$saResponse = $this->httpClient->request('GET', $saUrl, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($saResponse->getStatusCode() == 200) {
$saData = json_decode($saResponse->getBody(), TRUE);
if (!empty($saData)) {
foreach ($saData as $advisory) {
$advisories[] = [
'title' => $advisory['title'] ?? 'Unknown',
'link' => $advisory['link'] ?? "https://www.drupal.org/sa/{$advisory['sa_id']}",
'severity' => $advisory['risk_level'] ?? 'Unknown',
'version' => $advisory['covered_versions'] ?? 'All',
'date' => isset($advisory['created']) ? strtotime($advisory['created']) : time(),
'description' => $advisory['description'] ?? '',
'solution' => $advisory['solution'] ?? '',
];
}
return $advisories;
}
}
// Fallback to the project releases feed
$releaseUrl = "https://www.drupal.org/api/project/{$module_name}/releases";
$logger->debug('Fetching security releases from @url', ['@url' => $releaseUrl]);
$response = $this->httpClient->request('GET', $releaseUrl, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($response->getStatusCode() == 200) {
$data = json_decode($response->getBody(), TRUE);
if (!empty($data)) {
foreach ($data as $release) {
// Only include security releases
if (isset($release['security']['covered']) && $release['security']['covered']) {
$advisories[] = [
'title' => $release['title'] ?? 'Unknown',
'link' => $release['link'] ?? '',
'severity' => 'Security',
'version' => $release['version'] ?? 'Unknown',
'date' => isset($release['date']) ? strtotime($release['date']) : time(),
'description' => $release['release_notes'] ?? '',
];
}
}
}
}
// If both methods fail, try the legacy feed as last resort
if (empty($advisories)) {
$legacyUrl = "https://www.drupal.org/feeds/project/{$module_name}/sa.json";
$logger->debug('Fetching from legacy feed @url', ['@url' => $legacyUrl]);
$legacyResponse = $this->httpClient->request('GET', $legacyUrl, [
'headers' => ['Accept' => 'application/json'],
'http_errors' => false,
]);
if ($legacyResponse->getStatusCode() == 200) {
$legacyData = json_decode($legacyResponse->getBody(), TRUE);
if (!empty($legacyData)) {
foreach ($legacyData as $advisory) {
$advisories[] = [
'title' => $advisory['title'] ?? 'Unknown',
'link' => $advisory['link'] ?? '',
'severity' => $advisory['risk'] ?? 'Unknown',
'version' => $advisory['version'] ?? 'All',
'date' => isset($advisory['created']) ? strtotime($advisory['created']) : time(),
'description' => $advisory['description'] ?? '',
];
}
}
}
}
return $advisories;
}
catch (\Exception $e) {
$logger->error(
'Error fetching security advisories for @module: @error',
[
'@module' => $module_name,
'@error' => $e->getMessage(),
]
);
return [];
}
}
}
