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 [];
    }
  }
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc