Future-incompat report: Add suggestions of newer versions.

This commit is contained in:
Eric Huss 2021-06-20 15:58:55 -07:00
parent 1f7141f892
commit 71cb59b6ef
3 changed files with 163 additions and 6 deletions

View File

@ -1,6 +1,6 @@
use crate::command_prelude::*; use crate::command_prelude::*;
use anyhow::anyhow; use anyhow::anyhow;
use cargo::core::compiler::future_incompat::OnDiskReports; use cargo::core::compiler::future_incompat::{OnDiskReports, REPORT_PREAMBLE};
use cargo::drop_println; use cargo::drop_println;
pub fn cli() -> App { pub fn cli() -> App {
@ -39,6 +39,7 @@ fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliR
.value_of_u32("id")? .value_of_u32("id")?
.unwrap_or_else(|| reports.last_id()); .unwrap_or_else(|| reports.last_id());
let report = reports.get_report(id, config)?; let report = reports.get_report(id, config)?;
drop_println!(config, "{}", REPORT_PREAMBLE);
drop_println!(config, "{}", report); drop_println!(config, "{}", report);
Ok(()) Ok(())
} }

View File

@ -1,11 +1,26 @@
//! Support for future-incompatible warning reporting. //! Support for future-incompatible warning reporting.
use crate::core::{PackageId, Workspace}; use crate::core::{Dependency, PackageId, Workspace};
use crate::sources::SourceConfigMap;
use crate::util::{iter_join, CargoResult, Config}; use crate::util::{iter_join, CargoResult, Config};
use anyhow::{bail, format_err, Context}; use anyhow::{bail, format_err, Context};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::Write as _;
use std::io::{Read, Write}; use std::io::{Read, Write};
pub const REPORT_PREAMBLE: &str = "\
The following warnings were discovered during the build. These warnings are an
indication that the packages contain code that will become an error in a
future release of Rust. These warnings typically cover changes to close
soundness problems, unintended or undocumented behavior, or critical problems
that cannot be fixed in a backwards-compatible fashion, and are not expected
to be in wide use.
Each warning should contain a link for more information on what the warning
means and how to resolve it.
";
/// The future incompatibility report, emitted by the compiler as a JSON message. /// The future incompatibility report, emitted by the compiler as a JSON message.
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct FutureIncompatReport { pub struct FutureIncompatReport {
@ -90,7 +105,7 @@ impl OnDiskReports {
}; };
let report = OnDiskReport { let report = OnDiskReport {
id: current_reports.next_id, id: current_reports.next_id,
report: render_report(per_package_reports), report: render_report(ws, per_package_reports),
}; };
current_reports.next_id += 1; current_reports.next_id += 1;
current_reports.reports.push(report); current_reports.reports.push(report);
@ -178,11 +193,14 @@ impl OnDiskReports {
} }
} }
fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> String { fn render_report(
ws: &Workspace<'_>,
per_package_reports: &[FutureIncompatReportPackage],
) -> String {
let mut per_package_reports: Vec<_> = per_package_reports.iter().collect(); let mut per_package_reports: Vec<_> = per_package_reports.iter().collect();
per_package_reports.sort_by_key(|r| r.package_id); per_package_reports.sort_by_key(|r| r.package_id);
let mut rendered = String::new(); let mut rendered = String::new();
for per_package in per_package_reports { for per_package in &per_package_reports {
rendered.push_str(&format!( rendered.push_str(&format!(
"The package `{}` currently triggers the following future \ "The package `{}` currently triggers the following future \
incompatibility lints:\n", incompatibility lints:\n",
@ -198,5 +216,75 @@ fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> String
} }
rendered.push('\n'); rendered.push('\n');
} }
if let Some(s) = render_suggestions(ws, &per_package_reports) {
rendered.push_str(&s);
}
rendered rendered
} }
fn render_suggestions(
ws: &Workspace<'_>,
per_package_reports: &[&FutureIncompatReportPackage],
) -> Option<String> {
// This in general ignores all errors since this is opportunistic.
let _lock = ws.config().acquire_package_cache_lock().ok()?;
// Create a set of updated registry sources.
let map = SourceConfigMap::new(ws.config()).ok()?;
let package_ids: BTreeSet<_> = per_package_reports
.iter()
.map(|r| r.package_id)
.filter(|pkg_id| pkg_id.source_id().is_registry())
.collect();
let source_ids: HashSet<_> = package_ids
.iter()
.map(|pkg_id| pkg_id.source_id())
.collect();
let mut sources: HashMap<_, _> = source_ids
.into_iter()
.filter_map(|sid| {
let unlocked = sid.clone().with_precise(None);
let mut source = map.load(unlocked, &HashSet::new()).ok()?;
// Ignore errors updating.
if let Err(e) = source.update() {
log::debug!("failed to update source: {:?}", e);
}
Some((sid, source))
})
.collect();
// Query the sources for new versions.
let mut suggestions = String::new();
for pkg_id in package_ids {
let source = match sources.get_mut(&pkg_id.source_id()) {
Some(s) => s,
None => continue,
};
let dep = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()).ok()?;
let summaries = source.query_vec(&dep).ok()?;
let versions = itertools::sorted(
summaries
.iter()
.map(|summary| summary.version())
.filter(|version| *version > pkg_id.version()),
);
let versions = versions.map(|version| version.to_string());
let versions = iter_join(versions, ", ");
if !versions.is_empty() {
writeln!(
suggestions,
"{} has the following newer versions available: {}",
pkg_id, versions
)
.unwrap();
}
}
if suggestions.is_empty() {
None
} else {
Some(format!(
"The following packages appear to have newer versions available.\n\
You may want to consider updating them to a newer version to see if the \
issue has been fixed.\n\n{}",
suggestions
))
}
}

View File

@ -195,7 +195,11 @@ fn test_multi_crate() {
.exec_with_output() .exec_with_output()
.unwrap(); .unwrap();
let output = std::str::from_utf8(&output.stdout).unwrap(); let output = std::str::from_utf8(&output.stdout).unwrap();
let mut lines = output.lines(); assert!(output.starts_with("The following warnings were discovered"));
let mut lines = output
.lines()
// Skip the beginning of the per-package information.
.skip_while(|line| !line.starts_with("The package"));
for expected in &["first-dep v0.0.1", "second-dep v0.0.2"] { for expected in &["first-dep v0.0.1", "second-dep v0.0.2"] {
assert_eq!( assert_eq!(
&format!( &format!(
@ -276,3 +280,67 @@ Available IDs are: 1
) )
.run(); .run();
} }
#[cargo_test]
fn suggestions_for_updates() {
if !is_nightly() {
return;
}
Package::new("with_updates", "1.0.0")
.file("src/lib.rs", FUTURE_EXAMPLE)
.publish();
Package::new("big_update", "1.0.0")
.file("src/lib.rs", FUTURE_EXAMPLE)
.publish();
Package::new("without_updates", "1.0.0")
.file("src/lib.rs", FUTURE_EXAMPLE)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
with_updates = "1"
big_update = "1"
without_updates = "1"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("generate-lockfile").run();
Package::new("with_updates", "1.0.1")
.file("src/lib.rs", "")
.publish();
Package::new("with_updates", "1.0.2")
.file("src/lib.rs", "")
.publish();
Package::new("big_update", "2.0.0")
.file("src/lib.rs", "")
.publish();
p.cargo("check -Zfuture-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[..]cargo report future-incompatibilities --id 1[..]")
.run();
p.cargo("report future-incompatibilities")
.masquerade_as_nightly_cargo()
.with_stdout_contains(
"\
The following packages appear to have newer versions available.
You may want to consider updating them to a newer version to see if the issue has been fixed.
big_update v1.0.0 has the following newer versions available: 2.0.0
with_updates v1.0.0 has the following newer versions available: 1.0.1, 1.0.2
",
)
.run();
}