Skip to content

Commit

Permalink
Add more diagnostics to smooth edition transition
Browse files Browse the repository at this point in the history
This commit adds two diagnostics in particular to ease the transition into the
2018 edition. The current transition process is pretty particular and must be
done carefully, so let's try to automate things to make it as painless as
possible! Notably the new diagnostics are:

* If you `cargo fix --prepare-for 2018` a crate which already has the 2018
  edition enabled, then an error is generated. This is because the compiler
  can't prepare for the 2018 edition if you're already in the 2018 edition, the
  lints won't have a chance to fire. You can only execute `--prepare-for 2018`
  over crates in the 2015 edition.

* If you `cargo fix --prepare-for 2018` and have forgotten the
  `rust_2018_preview` feature, a warning is issued. The lints don't fire unless
  the feature is enabled, so this is intended to warn in this situation to
  ensure that lints fire as much as they can.

After this commit if `cargo fix --prepare-for` exits successfully with zero
warnings then crates should be guaranteed to be compatible!

Closes #5778
  • Loading branch information
alexcrichton committed Jul 31, 2018
1 parent 7691deb commit fa7a387
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 46 deletions.
5 changes: 3 additions & 2 deletions src/cargo/core/compiler/job_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use handle_error;
use util::{internal, profile, CargoResult, CargoResultExt, ProcessBuilder};
use util::{Config, DependencyQueue, Dirty, Fresh, Freshness};
use util::{Progress, ProgressStyle};
use util::diagnostic_server;
use util::diagnostic_server::{self, DiagnosticPrinter};

use super::job::Job;
use super::{BuildContext, BuildPlan, CompileMode, Context, Kind, Unit};
Expand Down Expand Up @@ -200,6 +200,7 @@ impl<'a> JobQueue<'a> {
let mut tokens = Vec::new();
let mut queue = Vec::new();
let build_plan = cx.bcx.build_config.build_plan;
let mut print = DiagnosticPrinter::new(cx.bcx.config);
trace!("queue: {:#?}", self.queue);

// Iteratively execute the entire dependency graph. Each turn of the
Expand Down Expand Up @@ -311,7 +312,7 @@ impl<'a> JobQueue<'a> {
}
}
Message::FixDiagnostic(msg) => {
msg.print_to(cx.bcx.config)?;
print.print(&msg)?;
}
Message::Finish(key, result) => {
info!("end: {:?}", key);
Expand Down
163 changes: 133 additions & 30 deletions src/cargo/ops/fix.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::{HashMap, HashSet, BTreeSet};
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{self, Command, ExitStatus};
use std::str;

Expand All @@ -21,6 +21,7 @@ use util::paths;

const FIX_ENV: &str = "__CARGO_FIX_PLZ";
const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
const EDITION_ENV: &str = "__CARGO_FIX_EDITION";

pub struct FixOptions<'a> {
pub edition: Option<&'a str>,
Expand Down Expand Up @@ -48,9 +49,10 @@ pub fn fix(ws: &Workspace, opts: &mut FixOptions) -> CargoResult<()> {
}

if let Some(edition) = opts.edition {
opts.compile_opts.build_config.extra_rustc_args.push("-W".to_string());
let lint_name = format!("rust-{}-compatibility", edition);
opts.compile_opts.build_config.extra_rustc_args.push(lint_name);
opts.compile_opts.build_config.extra_rustc_env.push((
EDITION_ENV.to_string(),
edition.to_string(),
));
}
opts.compile_opts.build_config.cargo_as_rustc_wrapper = true;
*opts.compile_opts.build_config.rustfix_diagnostic_server.borrow_mut() =
Expand Down Expand Up @@ -115,14 +117,8 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
Err(_) => return Ok(false),
};

// Try to figure out what we're compiling by looking for a rust-like file
// that exists.
let filename = env::args()
.skip(1)
.filter(|s| s.ends_with(".rs"))
.find(|s| Path::new(s).exists());

trace!("cargo-fix as rustc got file {:?}", filename);
let args = FixArgs::get();
trace!("cargo-fix as rustc got file {:?}", args.file);
let rustc = env::var_os("RUSTC").expect("failed to find RUSTC env var");

// Our goal is to fix only the crates that the end user is interested in.
Expand All @@ -133,10 +129,10 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
// compiling a Rust file and it *doesn't* have an absolute filename. That's
// not the best heuristic but matches what Cargo does today at least.
let mut fixes = FixedCrate::default();
if let Some(path) = filename {
if let Some(path) = &args.file {
if env::var("CARGO_PRIMARY_PACKAGE").is_ok() {
trace!("start rustfixing {:?}", path);
fixes = rustfix_crate(&lock_addr, rustc.as_ref(), &path)?;
fixes = rustfix_crate(&lock_addr, rustc.as_ref(), path, &args)?;
}
}

Expand All @@ -148,11 +144,10 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
// If we didn't actually make any changes then we can immediately exec the
// new rustc, and otherwise we capture the output to hide it in the scenario
// that we have to back it all out.
let mut cmd = Command::new(&rustc);
cmd.args(env::args().skip(1));
cmd.arg("--cap-lints=warn");
cmd.arg("--error-format=json");
if !fixes.original_files.is_empty() {
let mut cmd = Command::new(&rustc);
args.apply(&mut cmd);
cmd.arg("--error-format=json");
let output = cmd.output().context("failed to spawn rustc")?;

if output.status.success() {
Expand All @@ -173,17 +168,15 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
// below to recompile again.
if !output.status.success() {
for (k, v) in fixes.original_files {
File::create(&k)
.and_then(|mut f| f.write_all(v.as_bytes()))
fs::write(&k, v)
.with_context(|_| format!("failed to write file `{}`", k))?;
}
log_failed_fix(&output.stderr)?;
}
}

let mut cmd = Command::new(&rustc);
cmd.args(env::args().skip(1));
cmd.arg("--cap-lints=warn");
args.apply(&mut cmd);
exit_with(cmd.status().context("failed to spawn rustc")?);
}

Expand All @@ -193,9 +186,12 @@ struct FixedCrate {
original_files: HashMap<String, String>,
}

fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
-> Result<FixedCrate, Error>
{
args.verify_not_preparing_for_enabled_edition()?;
args.warn_if_preparing_probably_inert()?;

// If not empty, filter by these lints
//
// TODO: Implement a way to specify this
Expand All @@ -210,8 +206,8 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
let _lock = LockServerClient::lock(&lock_addr.parse()?, filename)?;

let mut cmd = Command::new(&rustc);
cmd.args(env::args().skip(1));
cmd.arg("--error-format=json").arg("--cap-lints=warn");
cmd.arg("--error-format=json");
args.apply(&mut cmd);
let output = cmd.output()
.with_context(|_| format!("failed to execute `{}`", rustc.display()))?;

Expand Down Expand Up @@ -280,7 +276,8 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)

debug!(
"collected {} suggestions for `{}`",
num_suggestion, filename
num_suggestion,
filename.display(),
);

let mut original_files = HashMap::with_capacity(file_map.len());
Expand Down Expand Up @@ -311,8 +308,7 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
continue;
}
Ok(new_code) => {
File::create(&file)
.and_then(|mut f| f.write_all(new_code.as_bytes()))
fs::write(&file, new_code)
.with_context(|_| format!("failed to write file `{}`", file))?;
original_files.insert(file, code);
}
Expand Down Expand Up @@ -369,3 +365,110 @@ fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {

Ok(())
}

#[derive(Default)]
struct FixArgs {
file: Option<PathBuf>,
prepare_for_edition: Option<String>,
enabled_edition: Option<String>,
other: Vec<OsString>,
}

impl FixArgs {
fn get() -> FixArgs {
let mut ret = FixArgs::default();
for arg in env::args_os().skip(1) {
let path = PathBuf::from(arg);
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
if path.exists() {
ret.file = Some(path);
continue
}
}
if let Some(s) = path.to_str() {
let prefix = "--edition=";
if s.starts_with(prefix) {
ret.enabled_edition = Some(s[prefix.len()..].to_string());
continue
}
}
ret.other.push(path.into());
}
if let Ok(s) = env::var(EDITION_ENV) {
ret.prepare_for_edition = Some(s);
}
return ret
}

fn apply(&self, cmd: &mut Command) {
if let Some(path) = &self.file {
cmd.arg(path);
}
cmd.args(&self.other)
.arg("--cap-lints=warn");
if let Some(edition) = &self.enabled_edition {
cmd.arg("--edition").arg(edition);
}
if let Some(prepare_for) = &self.prepare_for_edition {
cmd.arg("-W").arg(format!("rust-{}-compatibility", prepare_for));
}
}

/// Verify that we're not both preparing for an enabled edition and enabling
/// the edition.
///
/// This indicates that `cargo fix --prepare-for` is being executed out of
/// order with enabling the edition itself, meaning that we wouldn't
/// actually be able to fix anything! If it looks like this is happening
/// then yield an error to the user, indicating that this is happening.
fn verify_not_preparing_for_enabled_edition(&self) -> CargoResult<()> {
let edition = match &self.prepare_for_edition {
Some(s) => s,
None => return Ok(()),
};
let enabled = match &self.enabled_edition {
Some(s) => s,
None => return Ok(()),
};
if edition != enabled {
return Ok(())
}
let path = match &self.file {
Some(s) => s,
None => return Ok(()),
};

Message::EditionAlreadyEnabled {
file: path.display().to_string(),
edition: edition.to_string(),
}.post()?;

process::exit(1);
}

fn warn_if_preparing_probably_inert(&self) -> CargoResult<()> {
let edition = match &self.prepare_for_edition {
Some(s) => s,
None => return Ok(()),
};
let path = match &self.file {
Some(s) => s,
None => return Ok(()),
};
let contents = match fs::read_to_string(path) {
Ok(s) => s,
Err(_) => return Ok(())
};

let feature_name = format!("rust_{}_preview", edition);
if contents.contains(&feature_name) {
return Ok(())
}
Message::PreviewNotFound {
file: path.display().to_string(),
edition: edition.to_string(),
}.post()?;

Ok(())
}
}
Loading

0 comments on commit fa7a387

Please sign in to comment.