Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make future-incompat-report output more user-friendly #9953

Merged
merged 13 commits into from
Oct 19, 2021
6 changes: 4 additions & 2 deletions src/bin/cargo/commands/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ pub fn cli() -> App {
"identifier of the report generated by a Cargo command invocation",
)
.value_name("id"),
),
)
.arg_package("Package to display a report for"),
)
}

Expand All @@ -38,7 +39,8 @@ fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliR
let id = args
.value_of_u32("id")?
.unwrap_or_else(|| reports.last_id());
let report = reports.get_report(id, config)?;
let krate = args.value_of("package");
let report = reports.get_report(id, config, krate)?;
drop_println!(config, "{}", REPORT_PREAMBLE);
drop(config.shell().print_ansi_stdout(report.as_bytes()));
Ok(())
Expand Down
251 changes: 205 additions & 46 deletions src/cargo/core/compiler/future_incompat.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Support for future-incompatible warning reporting.

use crate::core::compiler::BuildContext;
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::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt::Write as _;
use std::io::{Read, Write};

Expand Down Expand Up @@ -77,8 +78,14 @@ pub struct OnDiskReports {
struct OnDiskReport {
/// Unique reference to the report for the `--id` CLI flag.
id: u32,
/// A (possibly empty) message describing which affected
/// packages have newer versions available
update_message: String,
/// Report, suitable for printing to the console.
report: String,
/// Maps package names to the corresponding report
/// We use a `BTreeMap` so that the iteration order
/// is stable across multiple runs of `cargo`
per_package: BTreeMap<String, String>,
}

impl Default for OnDiskReports {
Expand All @@ -95,6 +102,7 @@ impl OnDiskReports {
/// Saves a new report.
pub fn save_report(
ws: &Workspace<'_>,
update_message: String,
per_package_reports: &[FutureIncompatReportPackage],
) -> OnDiskReports {
let mut current_reports = match Self::load(ws) {
Expand All @@ -109,7 +117,8 @@ impl OnDiskReports {
};
let report = OnDiskReport {
id: current_reports.next_id,
report: render_report(ws, per_package_reports),
update_message,
per_package: render_report(per_package_reports),
};
current_reports.next_id += 1;
current_reports.reports.push(report);
Expand Down Expand Up @@ -176,7 +185,12 @@ impl OnDiskReports {
self.reports.last().map(|r| r.id).unwrap()
}

pub fn get_report(&self, id: u32, config: &Config) -> CargoResult<String> {
pub fn get_report(
&self,
id: u32,
config: &Config,
package: Option<&str>,
) -> CargoResult<String> {
let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| {
let available = iter_join(self.reports.iter().map(|r| r.id.to_string()), ", ");
format_err!(
Expand All @@ -186,25 +200,53 @@ impl OnDiskReports {
available
)
})?;
let report = if config.shell().err_supports_color() {
report.report.clone()

let mut to_display = report.update_message.clone();

let package_report = if let Some(package) = package {
report
.per_package
.get(package)
.ok_or_else(|| {
format_err!(
"could not find package with ID `{}`\n
Available packages are: {}\n
Omit the `--package` flag to display a report for all packages",
package,
iter_join(report.per_package.keys(), ", ")
)
})?
.to_string()
} else {
report
.per_package
.values()
.cloned()
.collect::<Vec<_>>()
.join("\n")
};
to_display += &package_report;

let to_display = if config.shell().err_supports_color() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed a small bug while reading this: #9960 (no need to fix it now, just making a note)

to_display
} else {
strip_ansi_escapes::strip(&report.report)
strip_ansi_escapes::strip(&to_display)
.map(|v| String::from_utf8(v).expect("utf8"))
.expect("strip should never fail")
};
Ok(report)
Ok(to_display)
}
}

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 {
fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMap<String, String> {
let mut report: BTreeMap<String, String> = BTreeMap::new();
for per_package in per_package_reports {
let package_spec = format!(
"{}:{}",
per_package.package_id.name(),
per_package.package_id.version()
);
let rendered = report.entry(package_spec).or_default();
rendered.push_str(&format!(
"The package `{}` currently triggers the following future \
incompatibility lints:\n",
Expand All @@ -218,25 +260,20 @@ fn render_report(
.map(|l| format!("> {}\n", l)),
);
}
rendered.push('\n');
}
if let Some(s) = render_suggestions(ws, &per_package_reports) {
rendered.push_str(&s);
}
rendered
report
}

fn render_suggestions(
ws: &Workspace<'_>,
per_package_reports: &[&FutureIncompatReportPackage],
) -> Option<String> {
// Returns a pair (compatible_updates, incompatible_updates),
// of semver-compatible and semver-incompatible update versions,
// respectively.
fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> 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
let package_ids: BTreeSet<_> = package_ids
.iter()
.map(|r| r.package_id)
.filter(|pkg_id| pkg_id.source_id().is_registry())
.collect();
let source_ids: HashSet<_> = package_ids
Expand All @@ -251,39 +288,161 @@ fn render_suggestions(
})
.collect();
// Query the sources for new versions.
let mut suggestions = String::new();
let mut updates = 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 mut updated_versions: Vec<_> = summaries
.iter()
.map(|summary| summary.version())
.filter(|version| *version > pkg_id.version())
.collect();
updated_versions.sort();

let updated_versions = iter_join(
updated_versions
.into_iter()
.map(|version| version.to_string()),
", ",
);
let versions = versions.map(|version| version.to_string());
let versions = iter_join(versions, ", ");
if !versions.is_empty() {

if !updated_versions.is_empty() {
writeln!(
suggestions,
updates,
"{} has the following newer versions available: {}",
pkg_id, versions
pkg_id, updated_versions
)
.unwrap();
}
}
if suggestions.is_empty() {
None
Some(updates)
}

pub fn render_message(
bcx: &BuildContext<'_, '_>,
per_package_future_incompat_reports: &[FutureIncompatReportPackage],
) {
if !bcx.config.cli_unstable().future_incompat_report {
return;
}
let should_display_message = match bcx.config.future_incompat_config() {
Ok(config) => config.should_display_message(),
Err(e) => {
crate::display_warning_with_error(
"failed to read future-incompat config from disk",
&e,
&mut bcx.config.shell(),
);
true
}
};

if per_package_future_incompat_reports.is_empty() {
// Explicitly passing a command-line flag overrides
// `should_display_message` from the config file
if bcx.build_config.future_incompat_report {
drop(
bcx.config
.shell()
.note("0 dependencies had future-incompatible warnings"),
);
}
return;
}

// Get a list of unique and sorted package name/versions.
let package_ids: BTreeSet<_> = per_package_future_incompat_reports
.iter()
.map(|r| r.package_id)
.collect();
let package_vers: Vec<_> = package_ids.iter().map(|pid| pid.to_string()).collect();

if should_display_message || bcx.build_config.future_incompat_report {
drop(bcx.config.shell().warn(&format!(
"the following packages contain code that will be rejected by a future \
version of Rust: {}",
package_vers.join(", ")
)));
}

let updated_versions = get_updates(bcx.ws, &package_ids).unwrap_or(String::new());

let update_message = if !updated_versions.is_empty() {
format!(
"
- Some affected dependencies have newer versions available.
You may want to consider updating them to a newer version to see if the issue has been fixed.

{updated_versions}\n",
updated_versions = updated_versions
)
} 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
))
String::new()
};

let on_disk_reports = OnDiskReports::save_report(
bcx.ws,
update_message.clone(),
per_package_future_incompat_reports,
);
let report_id = on_disk_reports.last_id();

if bcx.build_config.future_incompat_report {
let upstream_info = package_ids
.iter()
.map(|package_id| {
let manifest = bcx.packages.get_one(*package_id).unwrap().manifest();
format!(
"
- {name}
- Repository: {url}
- Detailed warning command: `cargo report future-incompatibilities --id {id} --crate \"{name}\"",
name = format!("{}:{}", package_id.name(), package_id.version()),
url = manifest
.metadata()
.repository
.as_deref()
.unwrap_or("<not found>"),
id = report_id,
)
})
.collect::<Vec<_>>()
.join("\n");
drop(bcx.config.shell().note(&format!(
"
To solve this problem, you can try the following approaches:

{update_message}
- If the issue is not solved by updating the dependencies, a fix has to be
implemented by those dependencies. You can help with that by notifying the
maintainers of this problem (e.g. by creating a bug report) or by proposing a
fix to the maintainers (e.g. by creating a pull request):
{upstream_info}

- If waiting for an upstream fix is not an option, you can use the `[patch]`
section in `Cargo.toml` to use your own version of the dependency. For more
information, see:
https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section
",
upstream_info = upstream_info,
update_message = update_message,
)));

drop(bcx.config.shell().note(&format!(
"this report can be shown with `cargo report \
future-incompatibilities -Z future-incompat-report --id {}`",
report_id
)));
} else if should_display_message {
drop(bcx.config.shell().note(&format!(
"to see what the problems were, use the option \
`--future-incompat-report`, or run `cargo report \
future-incompatibilities --id {}`",
report_id
)));
}
}
Loading