]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/fix.rs
Print environment note for json format, too.
[cargo.git] / src / cargo / ops / fix.rs
1 //! High-level overview of how `fix` works:
2 //!
3 //! The main goal is to run `cargo check` to get rustc to emit JSON
4 //! diagnostics with suggested fixes that can be applied to the files on the
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
18 //! for rustc by setting `primary_unit_rustc` in the build config. When
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
21 //! fix-proxy-mode.
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
41 use std::collections::{BTreeSet, HashMap, HashSet};
42 use std::env;
43 use std::ffi::OsString;
44 use std::path::{Path, PathBuf};
45 use std::process::{self, Command, ExitStatus};
46 use std::str;
47
48 use anyhow::{bail, Context, Error};
49 use cargo_util::{paths, ProcessBuilder};
50 use log::{debug, trace, warn};
51 use rustfix::diagnostics::Diagnostic;
52 use rustfix::{self, CodeFix};
53
54 use crate::core::compiler::RustcTargetData;
55 use crate::core::resolver::features::{FeatureOpts, FeatureResolver};
56 use crate::core::resolver::{HasDevUnits, ResolveBehavior};
57 use crate::core::{Edition, MaybePackage, Workspace};
58 use crate::ops::{self, CompileOptions};
59 use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
60 use crate::util::errors::CargoResult;
61 use crate::util::Config;
62 use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
63 use crate::{drop_eprint, drop_eprintln};
64
65 const FIX_ENV: &str = "__CARGO_FIX_PLZ";
66 const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
67 const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
68 const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS";
69
70 pub struct FixOptions {
71 pub edition: bool,
72 pub idioms: bool,
73 pub compile_opts: CompileOptions,
74 pub allow_dirty: bool,
75 pub allow_no_vcs: bool,
76 pub allow_staged: bool,
77 pub broken_code: bool,
78 }
79
80 pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> {
81 check_version_control(ws.config(), opts)?;
82 if opts.edition {
83 check_resolver_change(ws, opts)?;
84 }
85
86 // Spin up our lock server, which our subprocesses will use to synchronize fixes.
87 let lock_server = LockServer::new()?;
88 let mut wrapper = ProcessBuilder::new(env::current_exe()?);
89 wrapper.env(FIX_ENV, lock_server.addr().to_string());
90 let _started = lock_server.start()?;
91
92 opts.compile_opts.build_config.force_rebuild = true;
93
94 if opts.broken_code {
95 wrapper.env(BROKEN_CODE_ENV, "1");
96 }
97
98 if opts.edition {
99 wrapper.env(EDITION_ENV, "1");
100 }
101 if opts.idioms {
102 wrapper.env(IDIOMS_ENV, "1");
103 }
104
105 *opts
106 .compile_opts
107 .build_config
108 .rustfix_diagnostic_server
109 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
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
121 let rustc = ws.config().load_global_rustc(Some(ws))?;
122 wrapper.arg(&rustc.path);
123
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
126 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
127
128 ops::compile(ws, &opts.compile_opts)?;
129 Ok(())
130 }
131
132 fn check_version_control(config: &Config, opts: &FixOptions) -> CargoResult<()> {
133 if opts.allow_no_vcs {
134 return Ok(());
135 }
136 if !existing_vcs_repo(config.cwd(), config.cwd()) {
137 bail!(
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 )
142 }
143
144 if opts.allow_dirty && opts.allow_staged {
145 return Ok(());
146 }
147
148 let mut dirty_files = Vec::new();
149 let mut staged_files = Vec::new();
150 if let Ok(repo) = git2::Repository::discover(config.cwd()) {
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 => (),
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 => {
162 if !opts.allow_staged {
163 staged_files.push(path.to_string())
164 }
165 }
166 _ => {
167 if !opts.allow_dirty {
168 dirty_files.push(path.to_string())
169 }
170 }
171 };
172 }
173 }
174 }
175
176 if dirty_files.is_empty() && staged_files.is_empty() {
177 return Ok(());
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);
184 files_list.push_str(" (dirty)\n");
185 }
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 }
191
192 bail!(
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 );
202 }
203
204 fn 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 }
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)?;
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,
239 &opts.compile_opts.cli_features,
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,
251 &opts.compile_opts.cli_features,
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);
258 if differences.features.is_empty() && differences.optional_deps.is_empty() {
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,
268 "This may cause dependencies to resolve with a different set of features."
269 );
270 drop_eprintln!(
271 config,
272 "More information about the resolver changes may be found \
273 at https://doc.rust-lang.org/cargo/reference/features.html#feature-resolver-version-2"
274 );
275 drop_eprintln!(
276 config,
277 "The following differences were detected with the current configuration:\n"
278 );
279 let report = |changes: crate::core::resolver::features::DiffMap, what| {
280 for ((pkg_id, for_host), removed) in changes {
281 drop_eprint!(config, " {}", pkg_id);
282 if for_host {
283 drop_eprint!(config, " (as build dependency)");
284 }
285 if !removed.is_empty() {
286 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
287 drop_eprint!(config, " removed {} `{}`", what, joined.join(","));
288 }
289 drop_eprint!(config, "\n");
290 }
291 };
292 report(differences.features, "features");
293 report(differences.optional_deps, "optional dependency");
294 drop_eprint!(config, "\n");
295 Ok(())
296 }
297
298 /// Entry point for `cargo` running as a proxy for `rustc`.
299 ///
300 /// This is called every time `cargo` is run to check if it is in proxy mode.
301 ///
302 /// Returns `false` if `fix` is not being run (not in proxy mode). Returns
303 /// `true` if in `fix` proxy mode, and the fix was complete without any
304 /// warnings or errors. If there are warnings or errors, this does not return,
305 /// and the process exits with the corresponding `rustc` exit code.
306 pub fn fix_maybe_exec_rustc(config: &Config) -> CargoResult<bool> {
307 let lock_addr = match env::var(FIX_ENV) {
308 Ok(s) => s,
309 Err(_) => return Ok(false),
310 };
311
312 let args = FixArgs::get()?;
313 trace!("cargo-fix as rustc got file {:?}", args.file);
314
315 let workspace_rustc = std::env::var("RUSTC_WORKSPACE_WRAPPER")
316 .map(PathBuf::from)
317 .ok();
318 let rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
319
320 trace!("start rustfixing {:?}", args.file);
321 let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, config)?;
322
323 // Ok now we have our final goal of testing out the changes that we applied.
324 // If these changes went awry and actually started to cause the crate to
325 // *stop* compiling then we want to back them out and continue to print
326 // warnings to the user.
327 //
328 // If we didn't actually make any changes then we can immediately execute the
329 // new rustc, and otherwise we capture the output to hide it in the scenario
330 // that we have to back it all out.
331 if !fixes.files.is_empty() {
332 let mut cmd = rustc.build_command();
333 args.apply(&mut cmd);
334 cmd.arg("--error-format=json");
335 let output = cmd.output().context("failed to spawn rustc")?;
336
337 if output.status.success() {
338 for (path, file) in fixes.files.iter() {
339 Message::Fixed {
340 file: path.clone(),
341 fixes: file.fixes_applied,
342 }
343 .post()?;
344 }
345 }
346
347 // If we succeeded then we'll want to commit to the changes we made, if
348 // any. If stderr is empty then there's no need for the final exec at
349 // the end, we just bail out here.
350 if output.status.success() && output.stderr.is_empty() {
351 return Ok(true);
352 }
353
354 // Otherwise, if our rustc just failed, then that means that we broke the
355 // user's code with our changes. Back out everything and fall through
356 // below to recompile again.
357 if !output.status.success() {
358 if env::var_os(BROKEN_CODE_ENV).is_none() {
359 for (path, file) in fixes.files.iter() {
360 paths::write(path, &file.original_code)?;
361 }
362 }
363 log_failed_fix(&output.stderr)?;
364 }
365 }
366
367 // This final fall-through handles multiple cases;
368 // - If the fix failed, show the original warnings and suggestions.
369 // - If `--broken-code`, show the error messages.
370 // - If the fix succeeded, show any remaining warnings.
371 let mut cmd = rustc.build_command();
372 args.apply(&mut cmd);
373 for arg in args.format_args {
374 // Add any json/error format arguments that Cargo wants. This allows
375 // things like colored output to work correctly.
376 cmd.arg(arg);
377 }
378 exit_with(cmd.status().context("failed to spawn rustc")?);
379 }
380
381 #[derive(Default)]
382 struct FixedCrate {
383 files: HashMap<String, FixedFile>,
384 }
385
386 struct FixedFile {
387 errors_applying_fixes: Vec<String>,
388 fixes_applied: u32,
389 original_code: String,
390 }
391
392 /// Attempts to apply fixes to a single crate.
393 ///
394 /// This runs `rustc` (possibly multiple times) to gather suggestions from the
395 /// compiler and applies them to the files on disk.
396 fn rustfix_crate(
397 lock_addr: &str,
398 rustc: &ProcessBuilder,
399 filename: &Path,
400 args: &FixArgs,
401 config: &Config,
402 ) -> Result<FixedCrate, Error> {
403 args.check_edition_and_send_status(config)?;
404
405 // First up, we want to make sure that each crate is only checked by one
406 // process at a time. If two invocations concurrently check a crate then
407 // it's likely to corrupt it.
408 //
409 // We currently do this by assigning the name on our lock to the manifest
410 // directory.
411 let dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is missing?");
412 let _lock = LockServerClient::lock(&lock_addr.parse()?, dir)?;
413
414 // Next up, this is a bit suspicious, but we *iteratively* execute rustc and
415 // collect suggestions to feed to rustfix. Once we hit our limit of times to
416 // execute rustc or we appear to be reaching a fixed point we stop running
417 // rustc.
418 //
419 // This is currently done to handle code like:
420 //
421 // ::foo::<::Bar>();
422 //
423 // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
424 // The spans for these two suggestions are overlapping and its difficult in
425 // the compiler to **not** have overlapping spans here. As a result, a naive
426 // implementation would feed the two compiler suggestions for the above fix
427 // into `rustfix`, but one would be rejected because it overlaps with the
428 // other.
429 //
430 // In this case though, both suggestions are valid and can be automatically
431 // applied! To handle this case we execute rustc multiple times, collecting
432 // fixes each time we do so. Along the way we discard any suggestions that
433 // failed to apply, assuming that they can be fixed the next time we run
434 // rustc.
435 //
436 // Naturally, we want a few protections in place here though to avoid looping
437 // forever or otherwise losing data. To that end we have a few termination
438 // conditions:
439 //
440 // * Do this whole process a fixed number of times. In theory we probably
441 // need an infinite number of times to apply fixes, but we're not gonna
442 // sit around waiting for that.
443 // * If it looks like a fix genuinely can't be applied we need to bail out.
444 // Detect this when a fix fails to get applied *and* no suggestions
445 // successfully applied to the same file. In that case looks like we
446 // definitely can't make progress, so bail out.
447 let mut fixes = FixedCrate::default();
448 let mut last_fix_counts = HashMap::new();
449 let iterations = env::var("CARGO_FIX_MAX_RETRIES")
450 .ok()
451 .and_then(|n| n.parse().ok())
452 .unwrap_or(4);
453 for _ in 0..iterations {
454 last_fix_counts.clear();
455 for (path, file) in fixes.files.iter_mut() {
456 last_fix_counts.insert(path.clone(), file.fixes_applied);
457 // We'll generate new errors below.
458 file.errors_applying_fixes.clear();
459 }
460 rustfix_and_fix(&mut fixes, rustc, filename, args)?;
461 let mut progress_yet_to_be_made = false;
462 for (path, file) in fixes.files.iter_mut() {
463 if file.errors_applying_fixes.is_empty() {
464 continue;
465 }
466 // If anything was successfully fixed *and* there's at least one
467 // error, then assume the error was spurious and we'll try again on
468 // the next iteration.
469 if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
470 progress_yet_to_be_made = true;
471 }
472 }
473 if !progress_yet_to_be_made {
474 break;
475 }
476 }
477
478 // Any errors still remaining at this point need to be reported as probably
479 // bugs in Cargo and/or rustfix.
480 for (path, file) in fixes.files.iter_mut() {
481 for error in file.errors_applying_fixes.drain(..) {
482 Message::ReplaceFailed {
483 file: path.clone(),
484 message: error,
485 }
486 .post()?;
487 }
488 }
489
490 Ok(fixes)
491 }
492
493 /// Executes `rustc` to apply one round of suggestions to the crate in question.
494 ///
495 /// This will fill in the `fixes` map with original code, suggestions applied,
496 /// and any errors encountered while fixing files.
497 fn rustfix_and_fix(
498 fixes: &mut FixedCrate,
499 rustc: &ProcessBuilder,
500 filename: &Path,
501 args: &FixArgs,
502 ) -> Result<(), Error> {
503 // If not empty, filter by these lints.
504 // TODO: implement a way to specify this.
505 let only = HashSet::new();
506
507 let mut cmd = rustc.build_command();
508 cmd.arg("--error-format=json");
509 args.apply(&mut cmd);
510 let output = cmd.output().with_context(|| {
511 format!(
512 "failed to execute `{}`",
513 rustc.get_program().to_string_lossy()
514 )
515 })?;
516
517 // If rustc didn't succeed for whatever reasons then we're very likely to be
518 // looking at otherwise broken code. Let's not make things accidentally
519 // worse by applying fixes where a bug could cause *more* broken code.
520 // Instead, punt upwards which will reexec rustc over the original code,
521 // displaying pretty versions of the diagnostics we just read out.
522 if !output.status.success() && env::var_os(BROKEN_CODE_ENV).is_none() {
523 debug!(
524 "rustfixing `{:?}` failed, rustc exited with {:?}",
525 filename,
526 output.status.code()
527 );
528 return Ok(());
529 }
530
531 let fix_mode = env::var_os("__CARGO_FIX_YOLO")
532 .map(|_| rustfix::Filter::Everything)
533 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
534
535 // Sift through the output of the compiler to look for JSON messages.
536 // indicating fixes that we can apply.
537 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
538
539 let suggestions = stderr
540 .lines()
541 .filter(|x| !x.is_empty())
542 .inspect(|y| trace!("line: {}", y))
543 // Parse each line of stderr, ignoring errors, as they may not all be JSON.
544 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
545 // From each diagnostic, try to extract suggestions from rustc.
546 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
547
548 // Collect suggestions by file so we can apply them one at a time later.
549 let mut file_map = HashMap::new();
550 let mut num_suggestion = 0;
551 for suggestion in suggestions {
552 trace!("suggestion");
553 // Make sure we've got a file associated with this suggestion and all
554 // snippets point to the same file. Right now it's not clear what
555 // we would do with multiple files.
556 let file_names = suggestion
557 .solutions
558 .iter()
559 .flat_map(|s| s.replacements.iter())
560 .map(|r| &r.snippet.file_name);
561
562 let file_name = if let Some(file_name) = file_names.clone().next() {
563 file_name.clone()
564 } else {
565 trace!("rejecting as it has no solutions {:?}", suggestion);
566 continue;
567 };
568
569 if !file_names.clone().all(|f| f == &file_name) {
570 trace!("rejecting as it changes multiple files: {:?}", suggestion);
571 continue;
572 }
573
574 file_map
575 .entry(file_name)
576 .or_insert_with(Vec::new)
577 .push(suggestion);
578 num_suggestion += 1;
579 }
580
581 debug!(
582 "collected {} suggestions for `{}`",
583 num_suggestion,
584 filename.display(),
585 );
586
587 for (file, suggestions) in file_map {
588 // Attempt to read the source code for this file. If this fails then
589 // that'd be pretty surprising, so log a message and otherwise keep
590 // going.
591 let code = match paths::read(file.as_ref()) {
592 Ok(s) => s,
593 Err(e) => {
594 warn!("failed to read `{}`: {}", file, e);
595 continue;
596 }
597 };
598 let num_suggestions = suggestions.len();
599 debug!("applying {} fixes to {}", num_suggestions, file);
600
601 // If this file doesn't already exist then we just read the original
602 // code, so save it. If the file already exists then the original code
603 // doesn't need to be updated as we've just read an interim state with
604 // some fixes but perhaps not all.
605 let fixed_file = fixes
606 .files
607 .entry(file.clone())
608 .or_insert_with(|| FixedFile {
609 errors_applying_fixes: Vec::new(),
610 fixes_applied: 0,
611 original_code: code.clone(),
612 });
613 let mut fixed = CodeFix::new(&code);
614
615 // As mentioned above in `rustfix_crate`, we don't immediately warn
616 // about suggestions that fail to apply here, and instead we save them
617 // off for later processing.
618 for suggestion in suggestions.iter().rev() {
619 match fixed.apply(suggestion) {
620 Ok(()) => fixed_file.fixes_applied += 1,
621 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
622 }
623 }
624 let new_code = fixed.finish()?;
625 paths::write(&file, new_code)?;
626 }
627
628 Ok(())
629 }
630
631 fn exit_with(status: ExitStatus) -> ! {
632 #[cfg(unix)]
633 {
634 use std::io::Write;
635 use std::os::unix::prelude::*;
636 if let Some(signal) = status.signal() {
637 drop(writeln!(
638 std::io::stderr().lock(),
639 "child failed with signal `{}`",
640 signal
641 ));
642 process::exit(2);
643 }
644 }
645 process::exit(status.code().unwrap_or(3));
646 }
647
648 fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {
649 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
650
651 let diagnostics = stderr
652 .lines()
653 .filter(|x| !x.is_empty())
654 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
655 let mut files = BTreeSet::new();
656 let mut errors = Vec::new();
657 for diagnostic in diagnostics {
658 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
659 for span in diagnostic.spans.into_iter() {
660 files.insert(span.file_name);
661 }
662 }
663 let mut krate = None;
664 let mut prev_dash_dash_krate_name = false;
665 for arg in env::args() {
666 if prev_dash_dash_krate_name {
667 krate = Some(arg.clone());
668 }
669
670 if arg == "--crate-name" {
671 prev_dash_dash_krate_name = true;
672 } else {
673 prev_dash_dash_krate_name = false;
674 }
675 }
676
677 let files = files.into_iter().collect();
678 Message::FixFailed {
679 files,
680 krate,
681 errors,
682 }
683 .post()?;
684
685 Ok(())
686 }
687
688 /// Various command-line options and settings used when `cargo` is running as
689 /// a proxy for `rustc` during the fix operation.
690 struct FixArgs {
691 /// This is the `.rs` file that is being fixed.
692 file: PathBuf,
693 /// If `--edition` is used to migrate to the next edition, this is the
694 /// edition we are migrating towards.
695 prepare_for_edition: Option<Edition>,
696 /// `true` if `--edition-idioms` is enabled.
697 idioms: bool,
698 /// The current edition.
699 ///
700 /// `None` if on 2015.
701 enabled_edition: Option<Edition>,
702 /// Other command-line arguments not reflected by other fields in
703 /// `FixArgs`.
704 other: Vec<OsString>,
705 /// Path to the `rustc` executable.
706 rustc: PathBuf,
707 /// Console output flags (`--error-format`, `--json`, etc.).
708 ///
709 /// The normal fix procedure always uses `--json`, so it overrides what
710 /// Cargo normally passes when applying fixes. When displaying warnings or
711 /// errors, it will use these flags.
712 format_args: Vec<String>,
713 }
714
715 impl FixArgs {
716 fn get() -> Result<FixArgs, Error> {
717 let rustc = env::args_os()
718 .nth(1)
719 .map(PathBuf::from)
720 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
721 let mut file = None;
722 let mut enabled_edition = None;
723 let mut other = Vec::new();
724 let mut format_args = Vec::new();
725
726 for arg in env::args_os().skip(2) {
727 let path = PathBuf::from(arg);
728 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
729 file = Some(path);
730 continue;
731 }
732 if let Some(s) = path.to_str() {
733 if let Some(edition) = s.strip_prefix("--edition=") {
734 enabled_edition = Some(edition.parse()?);
735 continue;
736 }
737 if s.starts_with("--error-format=") || s.starts_with("--json=") {
738 // Cargo may add error-format in some cases, but `cargo
739 // fix` wants to add its own.
740 format_args.push(s.to_string());
741 continue;
742 }
743 }
744 other.push(path.into());
745 }
746 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
747 let idioms = env::var(IDIOMS_ENV).is_ok();
748
749 let prepare_for_edition = env::var(EDITION_ENV).ok().map(|_| {
750 enabled_edition
751 .unwrap_or(Edition::Edition2015)
752 .saturating_next()
753 });
754
755 Ok(FixArgs {
756 file,
757 prepare_for_edition,
758 idioms,
759 enabled_edition,
760 other,
761 rustc,
762 format_args,
763 })
764 }
765
766 fn apply(&self, cmd: &mut Command) {
767 cmd.arg(&self.file);
768 cmd.args(&self.other).arg("--cap-lints=warn");
769 if let Some(edition) = self.enabled_edition {
770 cmd.arg("--edition").arg(edition.to_string());
771 if self.idioms && edition.supports_idiom_lint() {
772 cmd.arg(format!("-Wrust-{}-idioms", edition));
773 }
774 }
775
776 if let Some(edition) = self.prepare_for_edition {
777 if edition.supports_compat_lint() {
778 cmd.arg("-W").arg(format!("rust-{}-compatibility", edition));
779 }
780 }
781 }
782
783 /// Validates the edition, and sends a message indicating what is being
784 /// done.
785 fn check_edition_and_send_status(&self, config: &Config) -> CargoResult<()> {
786 let to_edition = match self.prepare_for_edition {
787 Some(s) => s,
788 None => {
789 return Message::Fixing {
790 file: self.file.display().to_string(),
791 }
792 .post();
793 }
794 };
795 // Unfortunately determining which cargo targets are being built
796 // isn't easy, and each target can be a different edition. The
797 // cargo-as-rustc fix wrapper doesn't know anything about the
798 // workspace, so it can't check for the `cargo-features` unstable
799 // opt-in. As a compromise, this just restricts to the nightly
800 // toolchain.
801 //
802 // Unfortunately this results in a pretty poor error message when
803 // multiple jobs run in parallel (the error appears multiple
804 // times). Hopefully this doesn't happen often in practice.
805 if !to_edition.is_stable() && !config.nightly_features_allowed {
806 bail!(
807 "cannot migrate {} to edition {to_edition}\n\
808 Edition {to_edition} is unstable and not allowed in this release, \
809 consider trying the nightly release channel.",
810 self.file.display(),
811 to_edition = to_edition
812 );
813 }
814 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
815 if from_edition == to_edition {
816 Message::EditionAlreadyEnabled {
817 file: self.file.display().to_string(),
818 edition: to_edition,
819 }
820 .post()
821 } else {
822 Message::Migrating {
823 file: self.file.display().to_string(),
824 from_edition,
825 to_edition,
826 }
827 .post()
828 }
829 }
830 }