From 71cb59b6ef83f0f64dc7023a8b2db6545c4f5b13 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 20 Jun 2021 15:58:55 -0700 Subject: [PATCH] Future-incompat report: Add suggestions of newer versions. --- src/bin/cargo/commands/report.rs | 3 +- src/cargo/core/compiler/future_incompat.rs | 96 +++++++++++++++++++++- tests/testsuite/future_incompat_report.rs | 70 +++++++++++++++- 3 files changed, 163 insertions(+), 6 deletions(-) diff --git a/src/bin/cargo/commands/report.rs b/src/bin/cargo/commands/report.rs index bc4e8f7df..ab9944aaf 100644 --- a/src/bin/cargo/commands/report.rs +++ b/src/bin/cargo/commands/report.rs @@ -1,6 +1,6 @@ use crate::command_prelude::*; use anyhow::anyhow; -use cargo::core::compiler::future_incompat::OnDiskReports; +use cargo::core::compiler::future_incompat::{OnDiskReports, REPORT_PREAMBLE}; use cargo::drop_println; pub fn cli() -> App { @@ -39,6 +39,7 @@ fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliR .value_of_u32("id")? .unwrap_or_else(|| reports.last_id()); let report = reports.get_report(id, config)?; + drop_println!(config, "{}", REPORT_PREAMBLE); drop_println!(config, "{}", report); Ok(()) } diff --git a/src/cargo/core/compiler/future_incompat.rs b/src/cargo/core/compiler/future_incompat.rs index 8a4b0e9a3..cab1245f9 100644 --- a/src/cargo/core/compiler/future_incompat.rs +++ b/src/cargo/core/compiler/future_incompat.rs @@ -1,11 +1,26 @@ //! 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 anyhow::{bail, format_err, Context}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fmt::Write as _; 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. #[derive(serde::Deserialize)] pub struct FutureIncompatReport { @@ -90,7 +105,7 @@ impl OnDiskReports { }; let report = OnDiskReport { 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.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(); per_package_reports.sort_by_key(|r| r.package_id); let mut rendered = String::new(); - for per_package in per_package_reports { + for per_package in &per_package_reports { rendered.push_str(&format!( "The package `{}` currently triggers the following future \ incompatibility lints:\n", @@ -198,5 +216,75 @@ fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> String } rendered.push('\n'); } + if let Some(s) = render_suggestions(ws, &per_package_reports) { + rendered.push_str(&s); + } rendered } + +fn render_suggestions( + ws: &Workspace<'_>, + per_package_reports: &[&FutureIncompatReportPackage], +) -> Option { + // 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 + )) + } +} diff --git a/tests/testsuite/future_incompat_report.rs b/tests/testsuite/future_incompat_report.rs index 03b4d3745..f5889e00f 100644 --- a/tests/testsuite/future_incompat_report.rs +++ b/tests/testsuite/future_incompat_report.rs @@ -195,7 +195,11 @@ fn test_multi_crate() { .exec_with_output() .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"] { assert_eq!( &format!( @@ -276,3 +280,67 @@ Available IDs are: 1 ) .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(); +}