use crate::audit::issue::{AuditIssue, IssueCategory, IssueSeverity};
use crate::config::PackageToolsConfig;
use crate::error::{AuditError, AuditResult};
use crate::upgrade::{DependencyUpgrade, DetectionOptions, UpgradeManager, UpgradeType};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeAuditSection {
pub total_upgrades: usize,
pub major_upgrades: usize,
pub minor_upgrades: usize,
pub patch_upgrades: usize,
pub deprecated_packages: Vec<DeprecatedPackage>,
pub upgrades_by_package: HashMap<String, Vec<DependencyUpgrade>>,
pub issues: Vec<AuditIssue>,
}
impl UpgradeAuditSection {
#[must_use]
pub fn empty() -> Self {
Self {
total_upgrades: 0,
major_upgrades: 0,
minor_upgrades: 0,
patch_upgrades: 0,
deprecated_packages: Vec::new(),
upgrades_by_package: HashMap::new(),
issues: Vec::new(),
}
}
#[must_use]
pub fn has_upgrades(&self) -> bool {
self.total_upgrades > 0
}
#[must_use]
pub fn has_deprecated_packages(&self) -> bool {
!self.deprecated_packages.is_empty()
}
#[must_use]
pub fn critical_issue_count(&self) -> usize {
self.issues.iter().filter(|issue| issue.is_critical()).count()
}
#[must_use]
pub fn warning_issue_count(&self) -> usize {
self.issues.iter().filter(|issue| issue.is_warning()).count()
}
#[must_use]
pub fn info_issue_count(&self) -> usize {
self.issues.iter().filter(|issue| issue.is_info()).count()
}
#[must_use]
pub fn upgrades_for_package(&self, package_name: &str) -> &[DependencyUpgrade] {
self.upgrades_by_package.get(package_name).map(|v| v.as_slice()).unwrap_or(&[])
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeprecatedPackage {
pub name: String,
pub current_version: String,
pub deprecation_message: String,
pub alternative: Option<String>,
}
pub async fn audit_upgrades(
upgrade_manager: &UpgradeManager,
config: &PackageToolsConfig,
) -> AuditResult<UpgradeAuditSection> {
if !config.audit.sections.upgrades {
return Err(AuditError::SectionDisabled { section: "upgrades".to_string() });
}
let options = build_detection_options(config);
let preview = upgrade_manager.detect_upgrades(options).await.map_err(|e| {
AuditError::UpgradeDetectionFailed { reason: format!("Failed to detect upgrades: {}", e) }
})?;
let mut major_count = 0;
let mut minor_count = 0;
let mut patch_count = 0;
let mut deprecated_packages = Vec::new();
let mut upgrades_by_package: HashMap<String, Vec<DependencyUpgrade>> = HashMap::new();
let mut issues = Vec::new();
for package_upgrades in preview.packages {
let package_name = package_upgrades.package_name;
let package_upgrades_list = package_upgrades.upgrades;
if package_upgrades_list.is_empty() {
continue;
}
upgrades_by_package.insert(package_name.clone(), package_upgrades_list.clone());
for upgrade in &package_upgrades_list {
match upgrade.upgrade_type {
UpgradeType::Major => major_count += 1,
UpgradeType::Minor => minor_count += 1,
UpgradeType::Patch => patch_count += 1,
}
if let Some(deprecation_msg) = &upgrade.version_info.deprecated {
let deprecated = DeprecatedPackage {
name: upgrade.name.clone(),
current_version: upgrade.current_version.clone(),
deprecation_message: deprecation_msg.clone(),
alternative: extract_alternative(deprecation_msg),
};
deprecated_packages.push(deprecated.clone());
let mut issue = AuditIssue::new(
IssueSeverity::Critical,
IssueCategory::Upgrades,
format!("Deprecated package: {}", upgrade.name),
format!(
"Package '{}' (v{}) is deprecated. {}",
upgrade.name, upgrade.current_version, deprecation_msg
),
);
issue.add_affected_package(package_name.clone());
issue.add_metadata("package".to_string(), upgrade.name.clone());
issue.add_metadata("current_version".to_string(), upgrade.current_version.clone());
issue.add_metadata("deprecation_message".to_string(), deprecation_msg.clone());
if let Some(alt) = &deprecated.alternative {
issue.set_suggestion(format!("Consider migrating to '{}'", alt));
issue.add_metadata("alternative".to_string(), alt.clone());
}
issues.push(issue);
} else {
let (severity, title, description, suggestion) = match upgrade.upgrade_type {
UpgradeType::Major => (
IssueSeverity::Warning,
format!("Major upgrade available: {}", upgrade.name),
format!(
"Package '{}' has a major version upgrade available ({} → {}). \
This may include breaking changes.",
upgrade.name, upgrade.current_version, upgrade.latest_version
),
Some(
"Review the changelog for breaking changes before upgrading"
.to_string(),
),
),
UpgradeType::Minor => (
IssueSeverity::Info,
format!("Minor upgrade available: {}", upgrade.name),
format!(
"Package '{}' has a minor version upgrade available ({} → {}). \
This should be backward compatible.",
upgrade.name, upgrade.current_version, upgrade.latest_version
),
Some("Consider upgrading to get new features".to_string()),
),
UpgradeType::Patch => (
IssueSeverity::Info,
format!("Patch upgrade available: {}", upgrade.name),
format!(
"Package '{}' has a patch version upgrade available ({} → {}). \
This contains bug fixes.",
upgrade.name, upgrade.current_version, upgrade.latest_version
),
Some("Consider upgrading to get bug fixes".to_string()),
),
};
let mut issue =
AuditIssue::new(severity, IssueCategory::Upgrades, title, description);
issue.add_affected_package(package_name.clone());
issue.add_metadata("package".to_string(), upgrade.name.clone());
issue.add_metadata("current_version".to_string(), upgrade.current_version.clone());
issue.add_metadata("latest_version".to_string(), upgrade.latest_version.clone());
issue.add_metadata(
"upgrade_type".to_string(),
format!("{:?}", upgrade.upgrade_type),
);
if let Some(sugg) = suggestion {
issue.set_suggestion(sugg);
}
issues.push(issue);
}
}
}
let total_upgrades = major_count + minor_count + patch_count;
Ok(UpgradeAuditSection {
total_upgrades,
major_upgrades: major_count,
minor_upgrades: minor_count,
patch_upgrades: patch_count,
deprecated_packages,
upgrades_by_package,
issues,
})
}
fn build_detection_options(_config: &PackageToolsConfig) -> DetectionOptions {
DetectionOptions {
include_dependencies: true,
include_dev_dependencies: true,
include_peer_dependencies: true,
include_optional_dependencies: true,
package_filter: None,
dependency_filter: None,
include_prereleases: false,
concurrency: 10,
}
}
pub(crate) fn extract_alternative(message: &str) -> Option<String> {
let message_lower = message.to_lowercase();
let patterns = ["use ", "migrate to ", "replaced by ", "use the ", "switch to "];
for pattern in &patterns {
if let Some(start_idx) = message_lower.find(pattern) {
let start = start_idx + pattern.len();
let remaining = &message[start..];
if let Some(name) = remaining
.split(|c: char| c.is_whitespace() || c == ',' || c == '.' || c == '!')
.next()
{
let trimmed = name.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
}
None
}