Updates to future-incompatible reporting.

This commit is contained in:
Eric Huss 2021-06-20 09:32:43 -07:00
parent f2c22a3690
commit c2b02b3926
5 changed files with 372 additions and 173 deletions

View file

@ -73,7 +73,6 @@ itertools = "0.10.0"
# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`
# for more information.
rustc-workspace-hack = "1.0.0"
rand = "0.8.3"
[target.'cfg(windows)'.dependencies]
fwdansi = "1.1.0"

View file

@ -1,8 +1,7 @@
use crate::command_prelude::*;
use anyhow::{anyhow, Context as _};
use cargo::core::compiler::future_incompat::{OnDiskReport, FUTURE_INCOMPAT_FILE};
use cargo::drop_eprint;
use std::io::Read;
use anyhow::anyhow;
use cargo::core::compiler::future_incompat::OnDiskReports;
use cargo::drop_println;
pub fn cli() -> App {
subcommand("report")
@ -17,8 +16,7 @@ pub fn cli() -> App {
"id",
"identifier of the report generated by a Cargo command invocation",
)
.value_name("id")
.required(true),
.value_name("id"),
),
)
}
@ -35,31 +33,11 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliResult {
let ws = args.workspace(config)?;
let report_file = ws.target_dir().open_ro(
FUTURE_INCOMPAT_FILE,
ws.config(),
"Future incompatible report",
)?;
let mut file_contents = String::new();
report_file
.file()
.read_to_string(&mut file_contents)
.with_context(|| "failed to read report")?;
let on_disk_report: OnDiskReport =
serde_json::from_str(&file_contents).with_context(|| "failed to load report")?;
let id = args.value_of("id").unwrap();
if id != on_disk_report.id {
return Err(anyhow!(
"Expected an id of `{}`, but `{}` was provided on the command line. \
Your report may have been overwritten by a different one.",
on_disk_report.id,
id
)
.into());
}
drop_eprint!(config, "{}", on_disk_report.report);
let reports = OnDiskReports::load(&ws)?;
let id = args
.value_of_u32("id")?
.unwrap_or_else(|| reports.last_id());
let report = reports.get_report(id, config)?;
drop_println!(config, "{}", report);
Ok(())
}

View file

@ -1,4 +1,10 @@
//! Support for future-incompatible warning reporting.
use crate::core::{PackageId, Workspace};
use crate::util::{iter_join, CargoResult, Config};
use anyhow::{bail, format_err, Context};
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
/// The future incompatibility report, emitted by the compiler as a JSON message.
#[derive(serde::Deserialize)]
@ -6,6 +12,13 @@ pub struct FutureIncompatReport {
pub future_incompat_report: Vec<FutureBreakageItem>,
}
/// Structure used for collecting reports in-memory.
pub struct FutureIncompatReportPackage {
pub package_id: PackageId,
pub items: Vec<FutureBreakageItem>,
}
/// A single future-incompatible warning emitted by rustc.
#[derive(Serialize, Deserialize)]
pub struct FutureBreakageItem {
/// The date at which this lint will become an error.
@ -24,13 +37,166 @@ pub struct Diagnostic {
/// The filename in the top-level `target` directory where we store
/// the report
pub const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json";
/// Max number of reports to save on disk.
const MAX_REPORTS: usize = 5;
/// The structure saved to disk containing the reports.
#[derive(Serialize, Deserialize)]
pub struct OnDiskReport {
// A Cargo-generated id used to detect when a report has been overwritten
pub id: String,
// Cannot be a &str, since Serde needs
// to be able to un-escape the JSON
pub report: String,
pub struct OnDiskReports {
/// A schema version number, to handle older cargo's from trying to read
/// something that they don't understand.
version: u32,
/// The report ID to use for the next report to save.
next_id: u32,
/// Available reports.
reports: Vec<OnDiskReport>,
}
/// A single report for a given compilation session.
#[derive(Serialize, Deserialize)]
struct OnDiskReport {
/// Unique reference to the report for the `--id` CLI flag.
id: u32,
/// Report, suitable for printing to the console.
report: String,
}
impl Default for OnDiskReports {
fn default() -> OnDiskReports {
OnDiskReports {
version: 0,
next_id: 1,
reports: Vec::new(),
}
}
}
impl OnDiskReports {
/// Saves a new report.
pub fn save_report(
ws: &Workspace<'_>,
per_package_reports: &[FutureIncompatReportPackage],
) -> OnDiskReports {
let mut current_reports = match Self::load(ws) {
Ok(r) => r,
Err(e) => {
log::debug!(
"saving future-incompatible reports failed to load current reports: {:?}",
e
);
OnDiskReports::default()
}
};
let report = OnDiskReport {
id: current_reports.next_id,
report: render_report(per_package_reports),
};
current_reports.next_id += 1;
current_reports.reports.push(report);
if current_reports.reports.len() > MAX_REPORTS {
current_reports.reports.remove(0);
}
let on_disk = serde_json::to_vec(&current_reports).unwrap();
if let Err(e) = ws
.target_dir()
.open_rw(
FUTURE_INCOMPAT_FILE,
ws.config(),
"Future incompatibility report",
)
.and_then(|file| {
let mut file = file.file();
file.set_len(0)?;
file.write_all(&on_disk)?;
Ok(())
})
{
crate::display_warning_with_error(
"failed to write on-disk future incompatible report",
&e,
&mut ws.config().shell(),
);
}
current_reports
}
/// Loads the on-disk reports.
pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
let report_file = match ws.target_dir().open_ro(
FUTURE_INCOMPAT_FILE,
ws.config(),
"Future incompatible report",
) {
Ok(r) => r,
Err(e) => {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
if io_err.kind() == std::io::ErrorKind::NotFound {
bail!("no reports are currently available");
}
}
return Err(e);
}
};
let mut file_contents = String::new();
report_file
.file()
.read_to_string(&mut file_contents)
.with_context(|| "failed to read report")?;
let on_disk_reports: OnDiskReports =
serde_json::from_str(&file_contents).with_context(|| "failed to load report")?;
if on_disk_reports.version != 0 {
bail!("unable to read reports; reports were saved from a future version of Cargo");
}
Ok(on_disk_reports)
}
/// Returns the most recent report ID.
pub fn last_id(&self) -> u32 {
self.reports.last().map(|r| r.id).unwrap()
}
pub fn get_report(&self, id: u32, config: &Config) -> 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!(
"could not find report with ID {}\n\
Available IDs are: {}",
id,
available
)
})?;
let report = if config.shell().err_supports_color() {
report.report.clone()
} else {
strip_ansi_escapes::strip(&report.report)
.map(|v| String::from_utf8(v).expect("utf8"))
.expect("strip should never fail")
};
Ok(report)
}
}
fn render_report(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 {
rendered.push_str(&format!(
"The package `{}` currently triggers the following future \
incompatibility lints:\n",
per_package.package_id
));
for item in &per_package.items {
rendered.extend(
item.diagnostic
.rendered
.lines()
.map(|l| format!("> {}\n", l)),
);
}
rendered.push('\n');
}
rendered
}

View file

@ -50,8 +50,7 @@
//! improved.
use std::cell::Cell;
use std::collections::BTreeSet;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::io;
use std::marker;
use std::sync::Arc;
@ -62,8 +61,6 @@ use cargo_util::ProcessBuilder;
use crossbeam_utils::thread::Scope;
use jobserver::{Acquired, Client, HelperThread};
use log::{debug, info, trace};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use super::context::OutputFile;
use super::job::{
@ -73,7 +70,7 @@ use super::job::{
use super::timings::Timings;
use super::{BuildContext, BuildPlan, CompileMode, Context, Unit};
use crate::core::compiler::future_incompat::{
FutureBreakageItem, OnDiskReport, FUTURE_INCOMPAT_FILE,
FutureBreakageItem, FutureIncompatReportPackage, OnDiskReports,
};
use crate::core::resolver::ResolveBehavior;
use crate::core::{FeatureValue, PackageId, Shell, TargetKind};
@ -161,7 +158,7 @@ struct DrainState<'cfg> {
/// How many jobs we've finished
finished: usize,
per_crate_future_incompat_reports: Vec<FutureIncompatReportCrate>,
per_package_future_incompat_reports: Vec<FutureIncompatReportPackage>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@ -173,11 +170,6 @@ impl std::fmt::Display for JobId {
}
}
struct FutureIncompatReportCrate {
package_id: PackageId,
report: Vec<FutureBreakageItem>,
}
/// A `JobState` is constructed by `JobQueue::run` and passed to `Job::run`. It includes everything
/// necessary to communicate between the main thread and the execution of the job.
///
@ -432,7 +424,7 @@ impl<'cfg> JobQueue<'cfg> {
pending_queue: Vec::new(),
print: DiagnosticPrinter::new(cx.bcx.config),
finished: 0,
per_crate_future_incompat_reports: Vec::new(),
per_package_future_incompat_reports: Vec::new(),
};
// Create a helper thread for acquiring jobserver tokens
@ -615,10 +607,10 @@ impl<'cfg> DrainState<'cfg> {
}
}
}
Message::FutureIncompatReport(id, report) => {
Message::FutureIncompatReport(id, items) => {
let package_id = self.active[&id].pkg.package_id();
self.per_crate_future_incompat_reports
.push(FutureIncompatReportCrate { package_id, report });
self.per_package_future_incompat_reports
.push(FutureIncompatReportPackage { package_id, items });
}
Message::Token(acquired_token) => {
let token = acquired_token.with_context(|| "failed to acquire jobserver token")?;
@ -801,7 +793,7 @@ impl<'cfg> DrainState<'cfg> {
if !cx.bcx.build_config.build_plan {
// It doesn't really matter if this fails.
drop(cx.bcx.config.shell().status("Finished", message));
self.emit_future_incompat(cx);
self.emit_future_incompat(cx.bcx);
}
None
@ -811,93 +803,57 @@ impl<'cfg> DrainState<'cfg> {
}
}
fn emit_future_incompat(&mut self, cx: &mut Context<'_, '_>) {
if cx.bcx.config.cli_unstable().future_incompat_report {
if self.per_crate_future_incompat_reports.is_empty() {
fn emit_future_incompat(&mut self, bcx: &BuildContext<'_, '_>) {
if !bcx.config.cli_unstable().future_incompat_report {
return;
}
if self.per_package_future_incompat_reports.is_empty() {
if bcx.build_config.future_incompat_report {
drop(
cx.bcx
.config
bcx.config
.shell()
.note("0 dependencies had future-incompat warnings"),
.note("0 dependencies had future-incompatible warnings"),
);
return;
}
self.per_crate_future_incompat_reports
.sort_by_key(|r| r.package_id);
return;
}
let crates_and_versions = self
.per_crate_future_incompat_reports
.iter()
.map(|r| r.package_id.to_string())
.collect::<Vec<_>>()
.join(", ");
// Get a list of unique and sorted package name/versions.
let package_vers: BTreeSet<_> = self
.per_package_future_incompat_reports
.iter()
.map(|r| r.package_id)
.collect();
let package_vers: Vec<_> = package_vers
.into_iter()
.map(|pid| pid.to_string())
.collect();
drop(cx.bcx.config.shell().warn(&format!(
"the following crates contain code that will be rejected by a future version of Rust: {}",
crates_and_versions
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 on_disk_reports =
OnDiskReports::save_report(bcx.ws, &self.per_package_future_incompat_reports);
let report_id = on_disk_reports.last_id();
if bcx.build_config.future_incompat_report {
let rendered = on_disk_reports.get_report(report_id, bcx.config).unwrap();
drop_eprint!(bcx.config, "{}", rendered);
drop(bcx.config.shell().note(&format!(
"this report can be shown with `cargo report \
future-incompatibilities -Z future-incompat-report --id {}`",
report_id
)));
} else {
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
)));
let mut full_report = String::new();
let mut rng = thread_rng();
// Generate a short ID to allow detecting if a report gets overwritten
let id: String = std::iter::repeat(())
.map(|()| char::from(rng.sample(Alphanumeric)))
.take(4)
.collect();
for report in std::mem::take(&mut self.per_crate_future_incompat_reports) {
full_report.push_str(&format!(
"The crate `{}` currently triggers the following future incompatibility lints:\n",
report.package_id
));
for item in report.report {
let rendered = if cx.bcx.config.shell().err_supports_color() {
item.diagnostic.rendered
} else {
strip_ansi_escapes::strip(&item.diagnostic.rendered)
.map(|v| String::from_utf8(v).expect("utf8"))
.expect("strip should never fail")
};
for line in rendered.lines() {
full_report.push_str(&format!("> {}\n", line));
}
}
}
let report_file = cx.bcx.ws.target_dir().open_rw(
FUTURE_INCOMPAT_FILE,
cx.bcx.config,
"Future incompatibility report",
);
let err = report_file
.and_then(|report_file| {
let on_disk_report = OnDiskReport {
id: id.clone(),
report: full_report.clone(),
};
serde_json::to_writer(report_file, &on_disk_report).map_err(|e| e.into())
})
.err();
if let Some(e) = err {
crate::display_warning_with_error(
"failed to write on-disk future incompat report",
&e,
&mut cx.bcx.config.shell(),
);
}
if cx.bcx.build_config.future_incompat_report {
drop_eprint!(cx.bcx.config, "{}", full_report);
drop(cx.bcx.config.shell().note(
&format!("this report can be shown with `cargo report future-incompatibilities -Z future-incompat-report --id {}`", id)
));
} else {
drop(cx.bcx.config.shell().note(
&format!("to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id {}`", id)
));
}
}
}

View file

@ -1,27 +1,33 @@
//! Tests for future-incompat-report messages
use cargo_test_support::registry::Package;
use cargo_test_support::{basic_manifest, is_nightly, project};
use cargo_test_support::{basic_manifest, is_nightly, project, Project};
// An arbitrary lint (array_into_iter) that triggers a report.
const FUTURE_EXAMPLE: &'static str = "fn main() { [true].into_iter(); }";
// Some text that will be displayed when the lint fires.
const FUTURE_OUTPUT: &'static str = "[..]array_into_iter[..]";
fn simple_project() -> Project {
project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/main.rs", FUTURE_EXAMPLE)
.build()
}
#[cargo_test]
fn no_output_on_stable() {
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/main.rs", "fn main() { [true].into_iter(); }")
.build();
let p = simple_project();
p.cargo("build")
.with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default")
.with_stderr_does_not_contain("[..]crates[..]")
.with_stderr_contains(FUTURE_OUTPUT)
.with_stderr_does_not_contain("[..]cargo report[..]")
.run();
}
#[cargo_test]
fn gate_future_incompat_report() {
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/main.rs", "fn main() { [true].into_iter(); }")
.build();
let p = simple_project();
p.cargo("build --future-incompat-report")
.with_stderr_contains("error: the `--future-incompat-report` flag is unstable[..]")
@ -60,9 +66,25 @@ fn test_zero_future_incompat() {
.file("src/main.rs", "fn main() {}")
.build();
// No note if --future-incompat-report is not specified.
p.cargo("build -Z future-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[COMPILING] foo v0.0.0 [..]
[FINISHED] [..]
",
)
.run();
p.cargo("build --future-incompat-report -Z unstable-options -Z future-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_contains("note: 0 dependencies had future-incompat warnings")
.with_stderr(
"\
[FINISHED] [..]
note: 0 dependencies had future-incompatible warnings
",
)
.run();
}
@ -72,24 +94,21 @@ fn test_single_crate() {
return;
}
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/main.rs", "fn main() { [true].into_iter(); }")
.build();
let p = simple_project();
for command in &["build", "check", "rustc", "test"] {
p.cargo(command).arg("-Zfuture-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default")
.with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]")
.with_stderr_contains(FUTURE_OUTPUT)
.with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]")
.with_stderr_does_not_contain("[..]incompatibility[..]")
.run();
p.cargo(command).arg("-Zfuture-incompat-report").arg("-Zunstable-options").arg("--future-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default")
.with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]")
.with_stderr_contains("The crate `foo v0.0.0 ([..])` currently triggers the following future incompatibility lints:")
.with_stderr_contains(FUTURE_OUTPUT)
.with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]")
.with_stderr_contains("The package `foo v0.0.0 ([..])` currently triggers the following future incompatibility lints:")
.run();
}
}
@ -101,10 +120,10 @@ fn test_multi_crate() {
}
Package::new("first-dep", "0.0.1")
.file("src/lib.rs", "fn foo() { [25].into_iter(); }")
.file("src/lib.rs", FUTURE_EXAMPLE)
.publish();
Package::new("second-dep", "0.0.2")
.file("src/lib.rs", "fn foo() { ['a'].into_iter(); }")
.file("src/lib.rs", FUTURE_EXAMPLE)
.publish();
let p = project()
@ -126,24 +145,17 @@ fn test_multi_crate() {
for command in &["build", "check", "rustc", "test"] {
p.cargo(command).arg("-Zfuture-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_does_not_contain("[..]array_into_iter[..]")
.with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2")
.with_stderr_does_not_contain(FUTURE_OUTPUT)
.with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2")
// Check that we don't have the 'triggers' message shown at the bottom of this loop
.with_stderr_does_not_contain("[..]triggers[..]")
.run();
p.cargo("report future-incompatibilities -Z future-incompat-report --id bad-id")
.masquerade_as_nightly_cargo()
.with_stderr_contains("error: Expected an id of [..]")
.with_stderr_does_not_contain("[..]triggers[..]")
.with_status(101)
.run();
p.cargo(command).arg("-Zunstable-options").arg("-Zfuture-incompat-report").arg("--future-incompat-report")
.masquerade_as_nightly_cargo()
.with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2")
.with_stderr_contains("The crate `first-dep v0.0.1` currently triggers the following future incompatibility lints:")
.with_stderr_contains("The crate `second-dep v0.0.2` currently triggers the following future incompatibility lints:")
.with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2")
.with_stderr_contains("The package `first-dep v0.0.1` currently triggers the following future incompatibility lints:")
.with_stderr_contains("The package `second-dep v0.0.2` currently triggers the following future incompatibility lints:")
.run();
}
@ -172,7 +184,95 @@ fn test_multi_crate() {
p.cargo(&format!("report future-incompatibilities -Z future-incompat-report --id {}", id))
.masquerade_as_nightly_cargo()
.with_stderr_contains("The crate `first-dep v0.0.1` currently triggers the following future incompatibility lints:")
.with_stderr_contains("The crate `second-dep v0.0.2` currently triggers the following future incompatibility lints:")
.with_stdout_contains("The package `first-dep v0.0.1` currently triggers the following future incompatibility lints:")
.with_stdout_contains("The package `second-dep v0.0.2` currently triggers the following future incompatibility lints:")
.run();
// Test without --id, and also the full output of the report.
let output = p
.cargo("report future-incompatibilities -Z future-incompat-report")
.masquerade_as_nightly_cargo()
.exec_with_output()
.unwrap();
let output = std::str::from_utf8(&output.stdout).unwrap();
let mut lines = output.lines();
for expected in &["first-dep v0.0.1", "second-dep v0.0.2"] {
assert_eq!(
&format!(
"The package `{}` currently triggers the following future incompatibility lints:",
expected
),
lines.next().unwrap()
);
let mut count = 0;
while let Some(line) = lines.next() {
if line.is_empty() {
break;
}
count += 1;
}
assert!(count > 0);
}
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
}
#[cargo_test]
fn color() {
if !is_nightly() {
return;
}
let p = simple_project();
p.cargo("check -Zfuture-incompat-report")
.masquerade_as_nightly_cargo()
.run();
p.cargo("report future-incompatibilities -Z future-incompat-report")
.masquerade_as_nightly_cargo()
.with_stdout_does_not_contain("[..]\x1b[[..]")
.run();
p.cargo("report future-incompatibilities -Z future-incompat-report")
.masquerade_as_nightly_cargo()
.env("CARGO_TERM_COLOR", "always")
.with_stdout_contains("[..]\x1b[[..]")
.run();
}
#[cargo_test]
fn bad_ids() {
if !is_nightly() {
return;
}
let p = simple_project();
p.cargo("report future-incompatibilities -Z future-incompat-report --id 1")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr("error: no reports are currently available")
.run();
p.cargo("check -Zfuture-incompat-report")
.masquerade_as_nightly_cargo()
.run();
p.cargo("report future-incompatibilities -Z future-incompat-report --id foo")
.masquerade_as_nightly_cargo()
.with_status(1)
.with_stderr("error: Invalid value: could not parse `foo` as a number")
.run();
p.cargo("report future-incompatibilities -Z future-incompat-report --id 7")
.masquerade_as_nightly_cargo()
.with_status(101)
.with_stderr(
"\
error: could not find report with ID 7
Available IDs are: 1
",
)
.run();
}