use aube_lockfile::LockfileGraph;
use aube_settings::resolved::DeprecationWarnings;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use crate::commands::install::InstallOutputLevel;
#[derive(Debug, Clone)]
pub struct DeprecationRecord {
pub name: String,
pub version: String,
pub dep_path: String,
pub message: Arc<str>,
}
pub fn classify<'a>(
records: &'a [DeprecationRecord],
graph: &LockfileGraph,
) -> (Vec<&'a DeprecationRecord>, Vec<&'a DeprecationRecord>) {
let direct_keys: BTreeSet<(&str, &str)> = graph
.importers
.values()
.flat_map(|deps| deps.iter())
.filter_map(|d| graph.packages.get(&d.dep_path))
.map(|pkg| (pkg.name.as_str(), pkg.version.as_str()))
.collect();
let mut direct = Vec::new();
let mut transitive = Vec::new();
for r in records {
if direct_keys.contains(&(r.name.as_str(), r.version.as_str())) {
direct.push(r);
} else {
transitive.push(r);
}
}
(direct, transitive)
}
pub fn retain_in_graph(records: &mut Vec<DeprecationRecord>, graph: &LockfileGraph) {
let present: BTreeSet<(&str, &str)> = graph
.packages
.values()
.map(|p| (p.name.as_str(), p.version.as_str()))
.collect();
records.retain(|r| present.contains(&(r.name.as_str(), r.version.as_str())));
}
pub fn dedupe(records: Vec<DeprecationRecord>) -> Vec<DeprecationRecord> {
let mut seen: BTreeMap<(String, String), DeprecationRecord> = BTreeMap::new();
for r in records {
seen.entry((r.name.clone(), r.version.clone())).or_insert(r);
}
seen.into_values().collect()
}
pub fn render_install_warnings(
records: &[DeprecationRecord],
graph: &LockfileGraph,
mode: DeprecationWarnings,
) {
if records.is_empty() {
return;
}
let (direct, transitive) = classify(records, graph);
match mode {
DeprecationWarnings::None => {}
DeprecationWarnings::Summary => write_count_line(records.len(), !transitive.is_empty()),
DeprecationWarnings::Direct => {
for r in &direct {
write_warn_line(r);
}
if !transitive.is_empty() {
write_transitive_count_line(transitive.len());
}
}
DeprecationWarnings::All => {
for r in direct.iter().chain(transitive.iter()) {
write_warn_line(r);
}
}
}
}
fn write_warn_line(r: &DeprecationRecord) {
crate::commands::install::control::output(
InstallOutputLevel::Warning,
Some(aube_codes::warnings::WARN_AUBE_DEPRECATED_PACKAGE),
format!("deprecated {}@{}: {}", r.name, r.version, r.message),
);
}
fn write_transitive_count_line(count: usize) {
let pkgs = pluralizer::pluralize("transitive package", count as isize, true);
let verb = if count == 1 { "has" } else { "have" };
let msg = append_command_hint(
format!("{pkgs} {verb} deprecation warnings."),
"aube deprecations --transitive",
is_standalone_aube(),
);
write_summary_line(msg);
}
fn write_count_line(count: usize, has_transitive: bool) {
let pkgs = pluralizer::pluralize("package", count as isize, true);
let verb = if count == 1 { "has" } else { "have" };
let cmd = if has_transitive {
"aube deprecations --transitive"
} else {
"aube deprecations"
};
let msg = append_command_hint(
format!("{pkgs} {verb} deprecation warnings."),
cmd,
is_standalone_aube(),
);
write_summary_line(msg);
}
fn is_standalone_aube() -> bool {
aube_util::embedder().name == aube_util::AUBE.name
}
fn append_command_hint(message: String, command: &str, show_hint: bool) -> String {
if show_hint {
format!("{message} Run `{command}` to see them.")
} else {
message
}
}
fn write_summary_line(message: String) {
crate::commands::install::control::output(
InstallOutputLevel::Warning,
Some(aube_codes::warnings::WARN_AUBE_DEPRECATED_PACKAGE_SUMMARY),
message,
);
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use crate::commands::install::{
InstallControl, InstallEvent, InstallOutputLevel, InstallReporter,
};
use super::*;
#[derive(Default)]
struct RecordingReporter(Mutex<Vec<InstallEvent>>);
impl InstallReporter for RecordingReporter {
fn report(&self, event: InstallEvent) {
self.0.lock().unwrap().push(event);
}
}
#[test]
fn command_hint_is_only_added_for_standalone_aube() {
let message = "1 transitive package has deprecation warnings.".to_string();
assert_eq!(
append_command_hint(message.clone(), "aube deprecations --transitive", true,),
"1 transitive package has deprecation warnings. Run `aube deprecations --transitive` to see them."
);
assert_eq!(
append_command_hint(message.clone(), "aube deprecations --transitive", false),
message
);
}
#[tokio::test]
async fn summary_is_reported_as_a_structured_warning_event() {
let reporter = Arc::new(RecordingReporter::default());
let control = InstallControl::events(reporter.clone());
crate::commands::install::control::scope(control, async {
write_summary_line("1 package has deprecation warnings.".to_string());
})
.await;
let events = reporter.0.lock().unwrap();
assert!(matches!(
events.as_slice(),
[InstallEvent::Output {
level: InstallOutputLevel::Warning,
code: Some(code),
message,
}] if code == aube_codes::warnings::WARN_AUBE_DEPRECATED_PACKAGE_SUMMARY
&& message == "1 package has deprecation warnings."
));
}
}