]> git.proxmox.com Git - cargo.git/blame - src/cargo/ops/fix.rs
Serialize `cargo fix`
[cargo.git] / src / cargo / ops / fix.rs
CommitLineData
e6efbb1f
EH
1//! High-level overview of how `fix` works:
2//!
3//! The main goal is to run `cargo check` to get rustc to emit JSON
f5ef4939 4//! diagnostics with suggested fixes that can be applied to the files on the
e6efbb1f
EH
5//! filesystem, and validate that those changes didn't break anything.
6//!
7//! Cargo begins by launching a `LockServer` thread in the background to
8//! listen for network connections to coordinate locking when multiple targets
9//! are built simultaneously. It ensures each package has only one fix running
10//! at once.
11//!
12//! The `RustfixDiagnosticServer` is launched in a background thread (in
13//! `JobQueue`) to listen for network connections to coordinate displaying
14//! messages to the user on the console (so that multiple processes don't try
15//! to print at the same time).
16//!
17//! Cargo begins a normal `cargo check` operation with itself set as a proxy
e5098518 18//! for rustc by setting `primary_unit_rustc` in the build config. When
e6efbb1f
EH
19//! cargo launches rustc to check a crate, it is actually launching itself.
20//! The `FIX_ENV` environment variable is set so that cargo knows it is in
247063c3 21//! fix-proxy-mode.
e6efbb1f
EH
22//!
23//! Each proxied cargo-as-rustc detects it is in fix-proxy-mode (via `FIX_ENV`
24//! environment variable in `main`) and does the following:
25//!
26//! - Acquire a lock from the `LockServer` from the master cargo process.
27//! - Launches the real rustc (`rustfix_and_fix`), looking at the JSON output
28//! for suggested fixes.
29//! - Uses the `rustfix` crate to apply the suggestions to the files on the
30//! file system.
31//! - If rustfix fails to apply any suggestions (for example, they are
32//! overlapping), but at least some suggestions succeeded, it will try the
33//! previous two steps up to 4 times as long as some suggestions succeed.
34//! - Assuming there's at least one suggestion applied, and the suggestions
35//! applied cleanly, rustc is run again to verify the suggestions didn't
36//! break anything. The change will be backed out if it fails (unless
37//! `--broken-code` is used).
38//! - If there are any warnings or errors, rustc will be run one last time to
39//! show them to the user.
40
92485f8c 41use std::collections::{BTreeSet, HashMap, HashSet};
b02ba377 42use std::env;
fa7a3877 43use std::ffi::OsString;
fa7a3877 44use std::path::{Path, PathBuf};
b02ba377
AC
45use std::process::{self, Command, ExitStatus};
46use std::str;
47
b3d865e1 48use anyhow::{bail, Context, Error};
1dae5acb 49use cargo_util::{paths, ProcessBuilder};
9ed82b57 50use log::{debug, trace, warn};
b02ba377 51use rustfix::diagnostics::Diagnostic;
876a5036 52use rustfix::{self, CodeFix};
b02ba377 53
501499c5 54use crate::core::compiler::RustcTargetData;
85854b18 55use crate::core::resolver::features::{FeatureOpts, FeatureResolver};
cbc9da47 56use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior};
501499c5 57use crate::core::{Edition, MaybePackage, Workspace};
04ddd4d0
DW
58use crate::ops::{self, CompileOptions};
59use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
60use crate::util::errors::CargoResult;
1dae5acb 61use crate::util::Config;
04ddd4d0 62use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
501499c5 63use crate::{drop_eprint, drop_eprintln};
b02ba377
AC
64
65const FIX_ENV: &str = "__CARGO_FIX_PLZ";
66const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
fa7a3877 67const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
80f9d318
AC
68const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS";
69
c3032137 70pub struct FixOptions {
b2b120e9 71 pub edition: bool,
80f9d318 72 pub idioms: bool,
e4918c45 73 pub compile_opts: CompileOptions,
b02ba377
AC
74 pub allow_dirty: bool,
75 pub allow_no_vcs: bool,
6a616cb7 76 pub allow_staged: bool,
b02ba377
AC
77 pub broken_code: bool,
78}
79
c3032137 80pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> {
e4918c45 81 check_version_control(ws.config(), opts)?;
501499c5
EH
82 if opts.edition {
83 check_resolver_change(ws, opts)?;
84 }
b02ba377 85
f7c91ba6 86 // Spin up our lock server, which our subprocesses will use to synchronize fixes.
b02ba377 87 let lock_server = LockServer::new()?;
88810035 88 let mut wrapper = ProcessBuilder::new(env::current_exe()?);
842da62a 89 wrapper.env(FIX_ENV, lock_server.addr().to_string());
b02ba377
AC
90 let _started = lock_server.start()?;
91
616e0ad3
PH
92 opts.compile_opts.build_config.force_rebuild = true;
93
b02ba377 94 if opts.broken_code {
842da62a 95 wrapper.env(BROKEN_CODE_ENV, "1");
b02ba377
AC
96 }
97
b2b120e9 98 if opts.edition {
842da62a 99 wrapper.env(EDITION_ENV, "1");
b02ba377 100 }
80f9d318 101 if opts.idioms {
842da62a 102 wrapper.env(IDIOMS_ENV, "1");
80f9d318 103 }
842da62a 104
92485f8c
E
105 *opts
106 .compile_opts
107 .build_config
108 .rustfix_diagnostic_server
109 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
abd91493
JL
110
111 if let Some(server) = opts
112 .compile_opts
113 .build_config
114 .rustfix_diagnostic_server
115 .borrow()
116 .as_ref()
117 {
118 server.configure(&mut wrapper);
119 }
120
e4918c45 121 let rustc = ws.config().load_global_rustc(Some(ws))?;
79480cc2
JL
122 wrapper.arg(&rustc.path);
123
03feb61f
JL
124 // primary crates are compiled using a cargo subprocess to do extra work of applying fixes and
125 // repeating build until there are no more changes to be applied
79480cc2 126 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
b02ba377
AC
127
128 ops::compile(ws, &opts.compile_opts)?;
129 Ok(())
130}
131
c3032137 132fn check_version_control(config: &Config, opts: &FixOptions) -> CargoResult<()> {
b02ba377 133 if opts.allow_no_vcs {
92485f8c 134 return Ok(());
b02ba377 135 }
b02ba377 136 if !existing_vcs_repo(config.cwd(), config.cwd()) {
b3d865e1 137 bail!(
92485f8c
E
138 "no VCS found for this package and `cargo fix` can potentially \
139 perform destructive changes; if you'd like to suppress this \
140 error pass `--allow-no-vcs`"
141 )
b02ba377
AC
142 }
143
6a616cb7 144 if opts.allow_dirty && opts.allow_staged {
92485f8c 145 return Ok(());
b02ba377
AC
146 }
147
148 let mut dirty_files = Vec::new();
6a616cb7 149 let mut staged_files = Vec::new();
b02ba377 150 if let Ok(repo) = git2::Repository::discover(config.cwd()) {
6a616cb7
JJ
151 let mut repo_opts = git2::StatusOptions::new();
152 repo_opts.include_ignored(false);
153 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
154 if let Some(path) = status.path() {
155 match status.status() {
156 git2::Status::CURRENT => (),
92485f8c
E
157 git2::Status::INDEX_NEW
158 | git2::Status::INDEX_MODIFIED
159 | git2::Status::INDEX_DELETED
160 | git2::Status::INDEX_RENAMED
161 | git2::Status::INDEX_TYPECHANGE => {
6a616cb7
JJ
162 if !opts.allow_staged {
163 staged_files.push(path.to_string())
92485f8c
E
164 }
165 }
166 _ => {
6a616cb7
JJ
167 if !opts.allow_dirty {
168 dirty_files.push(path.to_string())
92485f8c
E
169 }
170 }
6a616cb7 171 };
b02ba377 172 }
b02ba377
AC
173 }
174 }
175
6a616cb7 176 if dirty_files.is_empty() && staged_files.is_empty() {
92485f8c 177 return Ok(());
b02ba377
AC
178 }
179
180 let mut files_list = String::new();
181 for file in dirty_files {
182 files_list.push_str(" * ");
183 files_list.push_str(&file);
4539ff21 184 files_list.push_str(" (dirty)\n");
b02ba377 185 }
6a616cb7
JJ
186 for file in staged_files {
187 files_list.push_str(" * ");
188 files_list.push_str(&file);
189 files_list.push_str(" (staged)\n");
190 }
b02ba377 191
b3d865e1 192 bail!(
92485f8c
E
193 "the working directory of this package has uncommitted changes, and \
194 `cargo fix` can potentially perform destructive changes; if you'd \
195 like to suppress this error pass `--allow-dirty`, `--allow-staged`, \
196 or commit the changes to these files:\n\
197 \n\
198 {}\n\
199 ",
200 files_list
201 );
b02ba377
AC
202}
203
501499c5
EH
204fn check_resolver_change(ws: &Workspace<'_>, opts: &FixOptions) -> CargoResult<()> {
205 let root = ws.root_maybe();
206 match root {
207 MaybePackage::Package(root_pkg) => {
208 if root_pkg.manifest().resolve_behavior().is_some() {
209 // If explicitly specified by the user, no need to check.
210 return Ok(());
211 }
212 // Only trigger if updating the root package from 2018.
213 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
214 if !pkgs.iter().any(|&pkg| pkg == root_pkg) {
215 // The root is not being migrated.
216 return Ok(());
217 }
218 if root_pkg.manifest().edition() != Edition::Edition2018 {
219 // V1 to V2 only happens on 2018 to 2021.
220 return Ok(());
221 }
222 }
223 MaybePackage::Virtual(_vm) => {
224 // Virtual workspaces don't have a global edition to set (yet).
225 return Ok(());
226 }
227 }
501499c5
EH
228 // 2018 without `resolver` set must be V1
229 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
230 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
501499c5
EH
231 let target_data = RustcTargetData::new(ws, &opts.compile_opts.build_config.requested_kinds)?;
232 // HasDevUnits::No because that may uncover more differences.
233 // This is not the same as what `cargo fix` is doing, since it is doing
234 // `--all-targets` which includes dev dependencies.
235 let ws_resolve = ops::resolve_ws_with_opts(
236 ws,
237 &target_data,
238 &opts.compile_opts.build_config.requested_kinds,
85854b18 239 &opts.compile_opts.cli_features,
501499c5
EH
240 &specs,
241 HasDevUnits::No,
242 crate::core::resolver::features::ForceAllTargets::No,
243 )?;
244
245 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, HasDevUnits::No);
246 let v2_features = FeatureResolver::resolve(
247 ws,
248 &target_data,
249 &ws_resolve.targeted_resolve,
250 &ws_resolve.pkg_set,
85854b18 251 &opts.compile_opts.cli_features,
501499c5
EH
252 &specs,
253 &opts.compile_opts.build_config.requested_kinds,
254 feature_opts,
255 )?;
256
257 let differences = v2_features.compare_legacy(&ws_resolve.resolved_features);
5559e028 258 if differences.is_empty() {
501499c5
EH
259 // Nothing is different, nothing to report.
260 return Ok(());
261 }
262 let config = ws.config();
263 config.shell().note(
264 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
265 )?;
266 drop_eprintln!(
267 config,
5559e028 268 "This may cause some dependencies to be built with fewer features enabled than previously."
501499c5
EH
269 );
270 drop_eprintln!(
271 config,
272 "More information about the resolver changes may be found \
5559e028 273 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
501499c5
EH
274 );
275 drop_eprintln!(
276 config,
5559e028
EH
277 "When building the following dependencies, \
278 the given features will no longer be used:\n"
501499c5 279 );
5559e028
EH
280 for ((pkg_id, for_host), removed) in differences {
281 drop_eprint!(config, " {}", pkg_id);
282 if for_host {
283 drop_eprint!(config, " (as host dependency)");
501499c5 284 }
5559e028
EH
285 drop_eprint!(config, ": ");
286 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
287 drop_eprintln!(config, "{}", joined.join(", "));
288 }
501499c5 289 drop_eprint!(config, "\n");
cbc9da47
EH
290 report_maybe_diesel(config, &ws_resolve.targeted_resolve)?;
291 Ok(())
292}
293
294fn report_maybe_diesel(config: &Config, resolve: &Resolve) -> CargoResult<()> {
295 if resolve
296 .iter()
297 .any(|pid| pid.name() == "diesel" && pid.version().major == 1)
298 && resolve.iter().any(|pid| pid.name() == "diesel_migrations")
299 {
300 config.shell().note(
301 "\
302This project appears to use both diesel and diesel_migrations. These packages have
303a known issue where the build may fail due to the version 2 resolver preventing
304feature unification between those two packages. See
305<https://github.com/rust-lang/cargo/issues/9450> for some potential workarounds.
306",
307 )?;
308 }
501499c5
EH
309 Ok(())
310}
311
b3d865e1
EH
312/// Entry point for `cargo` running as a proxy for `rustc`.
313///
314/// This is called every time `cargo` is run to check if it is in proxy mode.
315///
316/// Returns `false` if `fix` is not being run (not in proxy mode). Returns
317/// `true` if in `fix` proxy mode, and the fix was complete without any
318/// warnings or errors. If there are warnings or errors, this does not return,
319/// and the process exits with the corresponding `rustc` exit code.
4b096bea 320pub fn fix_maybe_exec_rustc(config: &Config) -> CargoResult<bool> {
b02ba377
AC
321 let lock_addr = match env::var(FIX_ENV) {
322 Ok(s) => s,
323 Err(_) => return Ok(false),
324 };
325
a836b92e 326 let args = FixArgs::get()?;
fa7a3877 327 trace!("cargo-fix as rustc got file {:?}", args.file);
b0351e4d 328
b0351e4d
JL
329 let workspace_rustc = std::env::var("RUSTC_WORKSPACE_WRAPPER")
330 .map(PathBuf::from)
331 .ok();
88810035 332 let rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
b02ba377 333
b3d865e1 334 trace!("start rustfixing {:?}", args.file);
4b096bea 335 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, config)?;
b02ba377
AC
336
337 // Ok now we have our final goal of testing out the changes that we applied.
338 // If these changes went awry and actually started to cause the crate to
339 // *stop* compiling then we want to back them out and continue to print
340 // warnings to the user.
341 //
f7c91ba6 342 // If we didn't actually make any changes then we can immediately execute the
b02ba377
AC
343 // new rustc, and otherwise we capture the output to hide it in the scenario
344 // that we have to back it all out.
876a5036 345 if !fixes.files.is_empty() {
b0351e4d 346 let mut cmd = rustc.build_command();
5825206a 347 args.apply(&mut cmd, config);
fa7a3877 348 cmd.arg("--error-format=json");
b02ba377
AC
349 let output = cmd.output().context("failed to spawn rustc")?;
350
351 if output.status.success() {
876a5036 352 for (path, file) in fixes.files.iter() {
820537c7 353 Message::Fixed {
876a5036
AC
354 file: path.clone(),
355 fixes: file.fixes_applied,
92485f8c
E
356 }
357 .post()?;
b02ba377
AC
358 }
359 }
360
361 // If we succeeded then we'll want to commit to the changes we made, if
362 // any. If stderr is empty then there's no need for the final exec at
363 // the end, we just bail out here.
385b54b3 364 if output.status.success() && output.stderr.is_empty() {
b02ba377
AC
365 return Ok(true);
366 }
367
f7c91ba6 368 // Otherwise, if our rustc just failed, then that means that we broke the
b02ba377
AC
369 // user's code with our changes. Back out everything and fall through
370 // below to recompile again.
371 if !output.status.success() {
08dc6da0
AC
372 if env::var_os(BROKEN_CODE_ENV).is_none() {
373 for (path, file) in fixes.files.iter() {
ce86e866 374 paths::write(path, &file.original_code)?;
08dc6da0 375 }
b02ba377
AC
376 }
377 log_failed_fix(&output.stderr)?;
378 }
379 }
380
247063c3 381 // This final fall-through handles multiple cases;
247063c3
EH
382 // - If the fix failed, show the original warnings and suggestions.
383 // - If `--broken-code`, show the error messages.
384 // - If the fix succeeded, show any remaining warnings.
b0351e4d 385 let mut cmd = rustc.build_command();
5825206a 386 args.apply(&mut cmd, config);
91a59e60
EH
387 for arg in args.format_args {
388 // Add any json/error format arguments that Cargo wants. This allows
389 // things like colored output to work correctly.
390 cmd.arg(arg);
391 }
b02ba377
AC
392 exit_with(cmd.status().context("failed to spawn rustc")?);
393}
394
395#[derive(Default)]
396struct FixedCrate {
876a5036
AC
397 files: HashMap<String, FixedFile>,
398}
399
400struct FixedFile {
401 errors_applying_fixes: Vec<String>,
402 fixes_applied: u32,
403 original_code: String,
b02ba377
AC
404}
405
b3d865e1
EH
406/// Attempts to apply fixes to a single crate.
407///
408/// This runs `rustc` (possibly multiple times) to gather suggestions from the
409/// compiler and applies them to the files on disk.
92485f8c
E
410fn rustfix_crate(
411 lock_addr: &str,
b0351e4d 412 rustc: &ProcessBuilder,
92485f8c
E
413 filename: &Path,
414 args: &FixArgs,
4b096bea 415 config: &Config,
92485f8c 416) -> Result<FixedCrate, Error> {
4b096bea 417 args.check_edition_and_send_status(config)?;
fa7a3877 418
f7c91ba6 419 // First up, we want to make sure that each crate is only checked by one
b02ba377
AC
420 // process at a time. If two invocations concurrently check a crate then
421 // it's likely to corrupt it.
422 //
c8e4f8e4
EH
423 // Historically this used per-source-file locking, then per-package
424 // locking. It now uses a single, global lock as some users do things like
425 // #[path] or include!() of shared files between packages. Serializing
426 // makes it slower, but is the only safe way to prevent concurrent
427 // modification.
428 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
b02ba377 429
f7c91ba6 430 // Next up, this is a bit suspicious, but we *iteratively* execute rustc and
876a5036
AC
431 // collect suggestions to feed to rustfix. Once we hit our limit of times to
432 // execute rustc or we appear to be reaching a fixed point we stop running
433 // rustc.
434 //
435 // This is currently done to handle code like:
436 //
437 // ::foo::<::Bar>();
438 //
439 // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
440 // The spans for these two suggestions are overlapping and its difficult in
f7c91ba6 441 // the compiler to **not** have overlapping spans here. As a result, a naive
876a5036
AC
442 // implementation would feed the two compiler suggestions for the above fix
443 // into `rustfix`, but one would be rejected because it overlaps with the
444 // other.
445 //
446 // In this case though, both suggestions are valid and can be automatically
447 // applied! To handle this case we execute rustc multiple times, collecting
448 // fixes each time we do so. Along the way we discard any suggestions that
449 // failed to apply, assuming that they can be fixed the next time we run
450 // rustc.
451 //
f7c91ba6 452 // Naturally, we want a few protections in place here though to avoid looping
876a5036
AC
453 // forever or otherwise losing data. To that end we have a few termination
454 // conditions:
455 //
456 // * Do this whole process a fixed number of times. In theory we probably
457 // need an infinite number of times to apply fixes, but we're not gonna
458 // sit around waiting for that.
459 // * If it looks like a fix genuinely can't be applied we need to bail out.
460 // Detect this when a fix fails to get applied *and* no suggestions
461 // successfully applied to the same file. In that case looks like we
462 // definitely can't make progress, so bail out.
463 let mut fixes = FixedCrate::default();
464 let mut last_fix_counts = HashMap::new();
465 let iterations = env::var("CARGO_FIX_MAX_RETRIES")
466 .ok()
467 .and_then(|n| n.parse().ok())
468 .unwrap_or(4);
469 for _ in 0..iterations {
470 last_fix_counts.clear();
471 for (path, file) in fixes.files.iter_mut() {
472 last_fix_counts.insert(path.clone(), file.fixes_applied);
f7c91ba6
AR
473 // We'll generate new errors below.
474 file.errors_applying_fixes.clear();
876a5036 475 }
5825206a 476 rustfix_and_fix(&mut fixes, rustc, filename, args, config)?;
876a5036
AC
477 let mut progress_yet_to_be_made = false;
478 for (path, file) in fixes.files.iter_mut() {
8798bf0d 479 if file.errors_applying_fixes.is_empty() {
92485f8c 480 continue;
876a5036
AC
481 }
482 // If anything was successfully fixed *and* there's at least one
483 // error, then assume the error was spurious and we'll try again on
484 // the next iteration.
485 if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
486 progress_yet_to_be_made = true;
487 }
488 }
489 if !progress_yet_to_be_made {
92485f8c 490 break;
876a5036
AC
491 }
492 }
493
494 // Any errors still remaining at this point need to be reported as probably
495 // bugs in Cargo and/or rustfix.
496 for (path, file) in fixes.files.iter_mut() {
497 for error in file.errors_applying_fixes.drain(..) {
498 Message::ReplaceFailed {
499 file: path.clone(),
500 message: error,
92485f8c
E
501 }
502 .post()?;
876a5036
AC
503 }
504 }
505
506 Ok(fixes)
507}
508
f7c91ba6 509/// Executes `rustc` to apply one round of suggestions to the crate in question.
876a5036
AC
510///
511/// This will fill in the `fixes` map with original code, suggestions applied,
512/// and any errors encountered while fixing files.
92485f8c
E
513fn rustfix_and_fix(
514 fixes: &mut FixedCrate,
b0351e4d 515 rustc: &ProcessBuilder,
92485f8c
E
516 filename: &Path,
517 args: &FixArgs,
5825206a 518 config: &Config,
92485f8c 519) -> Result<(), Error> {
f7c91ba6
AR
520 // If not empty, filter by these lints.
521 // TODO: implement a way to specify this.
876a5036
AC
522 let only = HashSet::new();
523
b0351e4d 524 let mut cmd = rustc.build_command();
fa7a3877 525 cmd.arg("--error-format=json");
5825206a 526 args.apply(&mut cmd, config);
b0351e4d
JL
527 let output = cmd.output().with_context(|| {
528 format!(
529 "failed to execute `{}`",
530 rustc.get_program().to_string_lossy()
531 )
532 })?;
b02ba377
AC
533
534 // If rustc didn't succeed for whatever reasons then we're very likely to be
535 // looking at otherwise broken code. Let's not make things accidentally
536 // worse by applying fixes where a bug could cause *more* broken code.
537 // Instead, punt upwards which will reexec rustc over the original code,
538 // displaying pretty versions of the diagnostics we just read out.
539 if !output.status.success() && env::var_os(BROKEN_CODE_ENV).is_none() {
540 debug!(
541 "rustfixing `{:?}` failed, rustc exited with {:?}",
542 filename,
543 output.status.code()
544 );
6dd73398 545 return Ok(());
b02ba377
AC
546 }
547
548 let fix_mode = env::var_os("__CARGO_FIX_YOLO")
549 .map(|_| rustfix::Filter::Everything)
550 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
551
f7c91ba6 552 // Sift through the output of the compiler to look for JSON messages.
b02ba377 553 // indicating fixes that we can apply.
f7c91ba6 554 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
b02ba377 555
92485f8c
E
556 let suggestions = stderr
557 .lines()
b02ba377
AC
558 .filter(|x| !x.is_empty())
559 .inspect(|y| trace!("line: {}", y))
f7c91ba6 560 // Parse each line of stderr, ignoring errors, as they may not all be JSON.
b02ba377 561 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
f7c91ba6 562 // From each diagnostic, try to extract suggestions from rustc.
b02ba377
AC
563 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
564
565 // Collect suggestions by file so we can apply them one at a time later.
566 let mut file_map = HashMap::new();
567 let mut num_suggestion = 0;
568 for suggestion in suggestions {
569 trace!("suggestion");
570 // Make sure we've got a file associated with this suggestion and all
59b3b3ec
PA
571 // snippets point to the same file. Right now it's not clear what
572 // we would do with multiple files.
9ed82b57
AC
573 let file_names = suggestion
574 .solutions
575 .iter()
a9f64b20
PA
576 .flat_map(|s| s.replacements.iter())
577 .map(|r| &r.snippet.file_name);
578
579 let file_name = if let Some(file_name) = file_names.clone().next() {
580 file_name.clone()
581 } else {
582 trace!("rejecting as it has no solutions {:?}", suggestion);
583 continue;
b02ba377 584 };
a9f64b20
PA
585
586 if !file_names.clone().all(|f| f == &file_name) {
59b3b3ec 587 trace!("rejecting as it changes multiple files: {:?}", suggestion);
b02ba377
AC
588 continue;
589 }
590
591 file_map
592 .entry(file_name)
593 .or_insert_with(Vec::new)
594 .push(suggestion);
595 num_suggestion += 1;
596 }
597
598 debug!(
599 "collected {} suggestions for `{}`",
fa7a3877
AC
600 num_suggestion,
601 filename.display(),
b02ba377
AC
602 );
603
b02ba377
AC
604 for (file, suggestions) in file_map {
605 // Attempt to read the source code for this file. If this fails then
606 // that'd be pretty surprising, so log a message and otherwise keep
607 // going.
1dae5acb 608 let code = match paths::read(file.as_ref()) {
b02ba377
AC
609 Ok(s) => s,
610 Err(e) => {
611 warn!("failed to read `{}`: {}", file, e);
612 continue;
613 }
614 };
615 let num_suggestions = suggestions.len();
616 debug!("applying {} fixes to {}", num_suggestions, file);
617
876a5036
AC
618 // If this file doesn't already exist then we just read the original
619 // code, so save it. If the file already exists then the original code
620 // doesn't need to be updated as we've just read an interim state with
621 // some fixes but perhaps not all.
92485f8c
E
622 let fixed_file = fixes
623 .files
624 .entry(file.clone())
625 .or_insert_with(|| FixedFile {
626 errors_applying_fixes: Vec::new(),
627 fixes_applied: 0,
628 original_code: code.clone(),
876a5036
AC
629 });
630 let mut fixed = CodeFix::new(&code);
631
632 // As mentioned above in `rustfix_crate`, we don't immediately warn
633 // about suggestions that fail to apply here, and instead we save them
634 // off for later processing.
635 for suggestion in suggestions.iter().rev() {
636 match fixed.apply(suggestion) {
637 Ok(()) => fixed_file.fixes_applied += 1,
638 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
b02ba377
AC
639 }
640 }
876a5036 641 let new_code = fixed.finish()?;
ce86e866 642 paths::write(&file, new_code)?;
b02ba377
AC
643 }
644
876a5036 645 Ok(())
b02ba377
AC
646}
647
648fn exit_with(status: ExitStatus) -> ! {
649 #[cfg(unix)]
650 {
5a096da9 651 use std::io::Write;
b02ba377
AC
652 use std::os::unix::prelude::*;
653 if let Some(signal) = status.signal() {
62711bd3
EH
654 drop(writeln!(
655 std::io::stderr().lock(),
656 "child failed with signal `{}`",
657 signal
658 ));
b02ba377
AC
659 process::exit(2);
660 }
661 }
662 process::exit(status.code().unwrap_or(3));
663}
664
665fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {
666 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
667
668 let diagnostics = stderr
669 .lines()
670 .filter(|x| !x.is_empty())
671 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
672 let mut files = BTreeSet::new();
fffb05d8 673 let mut errors = Vec::new();
b02ba377 674 for diagnostic in diagnostics {
fffb05d8 675 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
b02ba377
AC
676 for span in diagnostic.spans.into_iter() {
677 files.insert(span.file_name);
678 }
679 }
680 let mut krate = None;
681 let mut prev_dash_dash_krate_name = false;
682 for arg in env::args() {
683 if prev_dash_dash_krate_name {
684 krate = Some(arg.clone());
685 }
686
687 if arg == "--crate-name" {
688 prev_dash_dash_krate_name = true;
689 } else {
690 prev_dash_dash_krate_name = false;
691 }
692 }
693
694 let files = files.into_iter().collect();
fffb05d8
EH
695 Message::FixFailed {
696 files,
697 krate,
698 errors,
699 }
700 .post()?;
b02ba377
AC
701
702 Ok(())
703}
fa7a3877 704
b3d865e1
EH
705/// Various command-line options and settings used when `cargo` is running as
706/// a proxy for `rustc` during the fix operation.
fa7a3877 707struct FixArgs {
b3d865e1
EH
708 /// This is the `.rs` file that is being fixed.
709 file: PathBuf,
710 /// If `--edition` is used to migrate to the next edition, this is the
711 /// edition we are migrating towards.
712 prepare_for_edition: Option<Edition>,
713 /// `true` if `--edition-idioms` is enabled.
80f9d318 714 idioms: bool,
b3d865e1
EH
715 /// The current edition.
716 ///
717 /// `None` if on 2015.
a836b92e 718 enabled_edition: Option<Edition>,
b3d865e1
EH
719 /// Other command-line arguments not reflected by other fields in
720 /// `FixArgs`.
fa7a3877 721 other: Vec<OsString>,
b3d865e1
EH
722 /// Path to the `rustc` executable.
723 rustc: PathBuf,
724 /// Console output flags (`--error-format`, `--json`, etc.).
725 ///
726 /// The normal fix procedure always uses `--json`, so it overrides what
727 /// Cargo normally passes when applying fixes. When displaying warnings or
728 /// errors, it will use these flags.
91a59e60 729 format_args: Vec<String>,
fa7a3877
AC
730}
731
732impl FixArgs {
a836b92e 733 fn get() -> Result<FixArgs, Error> {
b3d865e1
EH
734 let rustc = env::args_os()
735 .nth(1)
736 .map(PathBuf::from)
737 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
738 let mut file = None;
739 let mut enabled_edition = None;
740 let mut other = Vec::new();
741 let mut format_args = Vec::new();
0a943be3 742
bccc0ee4 743 for arg in env::args_os().skip(2) {
fa7a3877 744 let path = PathBuf::from(arg);
92485f8c 745 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
b3d865e1 746 file = Some(path);
92485f8c 747 continue;
fa7a3877
AC
748 }
749 if let Some(s) = path.to_str() {
d5541331 750 if let Some(edition) = s.strip_prefix("--edition=") {
b3d865e1 751 enabled_edition = Some(edition.parse()?);
92485f8c 752 continue;
fa7a3877 753 }
daa1bce2 754 if s.starts_with("--error-format=") || s.starts_with("--json=") {
dcd4999d
EH
755 // Cargo may add error-format in some cases, but `cargo
756 // fix` wants to add its own.
b3d865e1 757 format_args.push(s.to_string());
dcd4999d
EH
758 continue;
759 }
fa7a3877 760 }
b3d865e1 761 other.push(path.into());
fa7a3877 762 }
b3d865e1
EH
763 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
764 let idioms = env::var(IDIOMS_ENV).is_ok();
765
c3032137
EH
766 let prepare_for_edition = env::var(EDITION_ENV).ok().map(|_| {
767 enabled_edition
768 .unwrap_or(Edition::Edition2015)
769 .saturating_next()
770 });
0a943be3 771
b3d865e1
EH
772 Ok(FixArgs {
773 file,
774 prepare_for_edition,
775 idioms,
776 enabled_edition,
777 other,
778 rustc,
779 format_args,
780 })
fa7a3877
AC
781 }
782
5825206a 783 fn apply(&self, cmd: &mut Command, config: &Config) {
b3d865e1 784 cmd.arg(&self.file);
92485f8c 785 cmd.args(&self.other).arg("--cap-lints=warn");
a836b92e
MB
786 if let Some(edition) = self.enabled_edition {
787 cmd.arg("--edition").arg(edition.to_string());
aa61976c
EH
788 if self.idioms && edition.supports_idiom_lint() {
789 cmd.arg(format!("-Wrust-{}-idioms", edition));
80f9d318 790 }
fa7a3877 791 }
03feb61f 792
b3d865e1 793 if let Some(edition) = self.prepare_for_edition {
28850225 794 if edition.supports_compat_lint() {
5825206a
EH
795 if config.nightly_features_allowed {
796 cmd.arg("--force-warns")
797 .arg(format!("rust-{}-compatibility", edition))
798 .arg("-Zunstable-options");
799 } else {
800 cmd.arg("-W").arg(format!("rust-{}-compatibility", edition));
801 }
28850225 802 }
fa7a3877
AC
803 }
804 }
805
3f2f7e30
EH
806 /// Validates the edition, and sends a message indicating what is being
807 /// done.
4b096bea 808 fn check_edition_and_send_status(&self, config: &Config) -> CargoResult<()> {
3f2f7e30 809 let to_edition = match self.prepare_for_edition {
35f745ac 810 Some(s) => s,
820537c7
EH
811 None => {
812 return Message::Fixing {
813 file: self.file.display().to_string(),
814 }
815 .post();
816 }
fa7a3877 817 };
28850225
EH
818 // Unfortunately determining which cargo targets are being built
819 // isn't easy, and each target can be a different edition. The
820 // cargo-as-rustc fix wrapper doesn't know anything about the
821 // workspace, so it can't check for the `cargo-features` unstable
822 // opt-in. As a compromise, this just restricts to the nightly
823 // toolchain.
824 //
825 // Unfortunately this results in a pretty poor error message when
826 // multiple jobs run in parallel (the error appears multiple
827 // times). Hopefully this doesn't happen often in practice.
a5720117 828 if !to_edition.is_stable() && !config.nightly_features_allowed {
28850225
EH
829 bail!(
830 "cannot migrate {} to edition {to_edition}\n\
831 Edition {to_edition} is unstable and not allowed in this release, \
832 consider trying the nightly release channel.",
833 self.file.display(),
834 to_edition = to_edition
835 );
836 }
3f2f7e30
EH
837 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
838 if from_edition == to_edition {
839 Message::EditionAlreadyEnabled {
840 file: self.file.display().to_string(),
841 edition: to_edition,
842 }
28850225 843 .post()
3f2f7e30
EH
844 } else {
845 Message::Migrating {
846 file: self.file.display().to_string(),
847 from_edition,
848 to_edition,
849 }
850 .post()
92485f8c 851 }
fa7a3877 852 }
fa7a3877 853}