]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/fix.rs
5fb35b8905d28562b280b9b4e6171ece51b15246
[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::ffi::OsString;
43 use std::path::{Path, PathBuf};
44 use std::process::{self, ExitStatus};
45 use std::{env, fs, str};
46
47 use anyhow::{bail, Context as _};
48 use cargo_util::{exit_status_to_string, is_simple_exit_code, paths, ProcessBuilder};
49 use log::{debug, trace, warn};
50 use rustfix::diagnostics::Diagnostic;
51 use rustfix::{self, CodeFix};
52 use semver::Version;
53
54 use crate::core::compiler::RustcTargetData;
55 use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor};
56 use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior};
57 use crate::core::{Edition, MaybePackage, PackageId, Workspace};
58 use crate::ops::resolve::WorkspaceResolve;
59 use crate::ops::{self, CompileOptions};
60 use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
61 use crate::util::errors::CargoResult;
62 use crate::util::Config;
63 use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
64 use crate::{drop_eprint, drop_eprintln};
65
66 const FIX_ENV: &str = "__CARGO_FIX_PLZ";
67 const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
68 const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
69 const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS";
70
71 pub struct FixOptions {
72 pub edition: bool,
73 pub idioms: bool,
74 pub compile_opts: CompileOptions,
75 pub allow_dirty: bool,
76 pub allow_no_vcs: bool,
77 pub allow_staged: bool,
78 pub broken_code: bool,
79 }
80
81 pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> {
82 check_version_control(ws.config(), opts)?;
83 if opts.edition {
84 check_resolver_change(ws, opts)?;
85 }
86
87 // Spin up our lock server, which our subprocesses will use to synchronize fixes.
88 let lock_server = LockServer::new()?;
89 let mut wrapper = ProcessBuilder::new(env::current_exe()?);
90 wrapper.env(FIX_ENV, lock_server.addr().to_string());
91 let _started = lock_server.start()?;
92
93 opts.compile_opts.build_config.force_rebuild = true;
94
95 if opts.broken_code {
96 wrapper.env(BROKEN_CODE_ENV, "1");
97 }
98
99 if opts.edition {
100 wrapper.env(EDITION_ENV, "1");
101 }
102 if opts.idioms {
103 wrapper.env(IDIOMS_ENV, "1");
104 }
105
106 *opts
107 .compile_opts
108 .build_config
109 .rustfix_diagnostic_server
110 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
111
112 if let Some(server) = opts
113 .compile_opts
114 .build_config
115 .rustfix_diagnostic_server
116 .borrow()
117 .as_ref()
118 {
119 server.configure(&mut wrapper);
120 }
121
122 let rustc = ws.config().load_global_rustc(Some(ws))?;
123 wrapper.arg(&rustc.path);
124 // This is calling rustc in cargo fix-proxy-mode, so it also need to retry.
125 // The argfile handling are located at `FixArgs::from_args`.
126 wrapper.retry_with_argfile(true);
127
128 // primary crates are compiled using a cargo subprocess to do extra work of applying fixes and
129 // repeating build until there are no more changes to be applied
130 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
131
132 ops::compile(ws, &opts.compile_opts)?;
133 Ok(())
134 }
135
136 fn check_version_control(config: &Config, opts: &FixOptions) -> CargoResult<()> {
137 if opts.allow_no_vcs {
138 return Ok(());
139 }
140 if !existing_vcs_repo(config.cwd(), config.cwd()) {
141 bail!(
142 "no VCS found for this package and `cargo fix` can potentially \
143 perform destructive changes; if you'd like to suppress this \
144 error pass `--allow-no-vcs`"
145 )
146 }
147
148 if opts.allow_dirty && opts.allow_staged {
149 return Ok(());
150 }
151
152 let mut dirty_files = Vec::new();
153 let mut staged_files = Vec::new();
154 if let Ok(repo) = git2::Repository::discover(config.cwd()) {
155 let mut repo_opts = git2::StatusOptions::new();
156 repo_opts.include_ignored(false);
157 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
158 if let Some(path) = status.path() {
159 match status.status() {
160 git2::Status::CURRENT => (),
161 git2::Status::INDEX_NEW
162 | git2::Status::INDEX_MODIFIED
163 | git2::Status::INDEX_DELETED
164 | git2::Status::INDEX_RENAMED
165 | git2::Status::INDEX_TYPECHANGE => {
166 if !opts.allow_staged {
167 staged_files.push(path.to_string())
168 }
169 }
170 _ => {
171 if !opts.allow_dirty {
172 dirty_files.push(path.to_string())
173 }
174 }
175 };
176 }
177 }
178 }
179
180 if dirty_files.is_empty() && staged_files.is_empty() {
181 return Ok(());
182 }
183
184 let mut files_list = String::new();
185 for file in dirty_files {
186 files_list.push_str(" * ");
187 files_list.push_str(&file);
188 files_list.push_str(" (dirty)\n");
189 }
190 for file in staged_files {
191 files_list.push_str(" * ");
192 files_list.push_str(&file);
193 files_list.push_str(" (staged)\n");
194 }
195
196 bail!(
197 "the working directory of this package has uncommitted changes, and \
198 `cargo fix` can potentially perform destructive changes; if you'd \
199 like to suppress this error pass `--allow-dirty`, `--allow-staged`, \
200 or commit the changes to these files:\n\
201 \n\
202 {}\n\
203 ",
204 files_list
205 );
206 }
207
208 fn check_resolver_change(ws: &Workspace<'_>, opts: &FixOptions) -> CargoResult<()> {
209 let root = ws.root_maybe();
210 match root {
211 MaybePackage::Package(root_pkg) => {
212 if root_pkg.manifest().resolve_behavior().is_some() {
213 // If explicitly specified by the user, no need to check.
214 return Ok(());
215 }
216 // Only trigger if updating the root package from 2018.
217 let pkgs = opts.compile_opts.spec.get_packages(ws)?;
218 if !pkgs.iter().any(|&pkg| pkg == root_pkg) {
219 // The root is not being migrated.
220 return Ok(());
221 }
222 if root_pkg.manifest().edition() != Edition::Edition2018 {
223 // V1 to V2 only happens on 2018 to 2021.
224 return Ok(());
225 }
226 }
227 MaybePackage::Virtual(_vm) => {
228 // Virtual workspaces don't have a global edition to set (yet).
229 return Ok(());
230 }
231 }
232 // 2018 without `resolver` set must be V1
233 assert_eq!(ws.resolve_behavior(), ResolveBehavior::V1);
234 let specs = opts.compile_opts.spec.to_package_id_specs(ws)?;
235 let target_data = RustcTargetData::new(ws, &opts.compile_opts.build_config.requested_kinds)?;
236 let resolve_differences = |has_dev_units| -> CargoResult<(WorkspaceResolve<'_>, DiffMap)> {
237 let ws_resolve = ops::resolve_ws_with_opts(
238 ws,
239 &target_data,
240 &opts.compile_opts.build_config.requested_kinds,
241 &opts.compile_opts.cli_features,
242 &specs,
243 has_dev_units,
244 crate::core::resolver::features::ForceAllTargets::No,
245 )?;
246
247 let feature_opts = FeatureOpts::new_behavior(ResolveBehavior::V2, has_dev_units);
248 let v2_features = FeatureResolver::resolve(
249 ws,
250 &target_data,
251 &ws_resolve.targeted_resolve,
252 &ws_resolve.pkg_set,
253 &opts.compile_opts.cli_features,
254 &specs,
255 &opts.compile_opts.build_config.requested_kinds,
256 feature_opts,
257 )?;
258
259 let diffs = v2_features.compare_legacy(&ws_resolve.resolved_features);
260 Ok((ws_resolve, diffs))
261 };
262 let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?;
263 let (ws_resolve, mut with_dev_diffs) = resolve_differences(HasDevUnits::Yes)?;
264 if without_dev_diffs.is_empty() && with_dev_diffs.is_empty() {
265 // Nothing is different, nothing to report.
266 return Ok(());
267 }
268 // Only display unique changes with dev-dependencies.
269 with_dev_diffs.retain(|k, vals| without_dev_diffs.get(k) != Some(vals));
270 let config = ws.config();
271 config.shell().note(
272 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
273 )?;
274 drop_eprintln!(
275 config,
276 "This may cause some dependencies to be built with fewer features enabled than previously."
277 );
278 drop_eprintln!(
279 config,
280 "More information about the resolver changes may be found \
281 at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html"
282 );
283 drop_eprintln!(
284 config,
285 "When building the following dependencies, \
286 the given features will no longer be used:\n"
287 );
288 let show_diffs = |differences: DiffMap| {
289 for ((pkg_id, features_for), removed) in differences {
290 drop_eprint!(config, " {}", pkg_id);
291 if let FeaturesFor::HostDep = features_for {
292 drop_eprint!(config, " (as host dependency)");
293 }
294 drop_eprint!(config, " removed features: ");
295 let joined: Vec<_> = removed.iter().map(|s| s.as_str()).collect();
296 drop_eprintln!(config, "{}", joined.join(", "));
297 }
298 drop_eprint!(config, "\n");
299 };
300 if !without_dev_diffs.is_empty() {
301 show_diffs(without_dev_diffs);
302 }
303 if !with_dev_diffs.is_empty() {
304 drop_eprintln!(
305 config,
306 "The following differences only apply when building with dev-dependencies:\n"
307 );
308 show_diffs(with_dev_diffs);
309 }
310 report_maybe_diesel(config, &ws_resolve.targeted_resolve)?;
311 Ok(())
312 }
313
314 fn report_maybe_diesel(config: &Config, resolve: &Resolve) -> CargoResult<()> {
315 fn is_broken_diesel(pid: PackageId) -> bool {
316 pid.name() == "diesel" && pid.version() < &Version::new(1, 4, 8)
317 }
318
319 fn is_broken_diesel_migration(pid: PackageId) -> bool {
320 pid.name() == "diesel_migrations" && pid.version().major <= 1
321 }
322
323 if resolve.iter().any(is_broken_diesel) && resolve.iter().any(is_broken_diesel_migration) {
324 config.shell().note(
325 "\
326 This project appears to use both diesel and diesel_migrations. These packages have
327 a known issue where the build may fail due to the version 2 resolver preventing
328 feature unification between those two packages. Please update to at least diesel 1.4.8
329 to prevent this issue from happening.
330 ",
331 )?;
332 }
333 Ok(())
334 }
335
336 /// Provide the lock address when running in proxy mode
337 ///
338 /// Returns `None` if `fix` is not being run (not in proxy mode). Returns
339 /// `Some(...)` if in `fix` proxy mode
340 pub fn fix_get_proxy_lock_addr() -> Option<String> {
341 env::var(FIX_ENV).ok()
342 }
343
344 /// Entry point for `cargo` running as a proxy for `rustc`.
345 ///
346 /// This is called every time `cargo` is run to check if it is in proxy mode.
347 ///
348 /// If there are warnings or errors, this does not return,
349 /// and the process exits with the corresponding `rustc` exit code.
350 ///
351 /// See [`fix_proxy_lock_addr`]
352 pub fn fix_exec_rustc(config: &Config, lock_addr: &str) -> CargoResult<()> {
353 let args = FixArgs::get()?;
354 trace!("cargo-fix as rustc got file {:?}", args.file);
355
356 let workspace_rustc = std::env::var("RUSTC_WORKSPACE_WRAPPER")
357 .map(PathBuf::from)
358 .ok();
359 let mut rustc = ProcessBuilder::new(&args.rustc).wrapped(workspace_rustc.as_ref());
360 rustc.retry_with_argfile(true);
361 rustc.env_remove(FIX_ENV);
362 args.apply(&mut rustc);
363
364 trace!("start rustfixing {:?}", args.file);
365 let json_error_rustc = {
366 let mut cmd = rustc.clone();
367 cmd.arg("--error-format=json");
368 cmd
369 };
370 let fixes = rustfix_crate(&lock_addr, &json_error_rustc, &args.file, &args, config)?;
371
372 // Ok now we have our final goal of testing out the changes that we applied.
373 // If these changes went awry and actually started to cause the crate to
374 // *stop* compiling then we want to back them out and continue to print
375 // warnings to the user.
376 //
377 // If we didn't actually make any changes then we can immediately execute the
378 // new rustc, and otherwise we capture the output to hide it in the scenario
379 // that we have to back it all out.
380 if !fixes.files.is_empty() {
381 debug!("calling rustc for final verification: {json_error_rustc}");
382 let output = json_error_rustc.output()?;
383
384 if output.status.success() {
385 for (path, file) in fixes.files.iter() {
386 Message::Fixed {
387 file: path.clone(),
388 fixes: file.fixes_applied,
389 }
390 .post()?;
391 }
392 }
393
394 // If we succeeded then we'll want to commit to the changes we made, if
395 // any. If stderr is empty then there's no need for the final exec at
396 // the end, we just bail out here.
397 if output.status.success() && output.stderr.is_empty() {
398 return Ok(());
399 }
400
401 // Otherwise, if our rustc just failed, then that means that we broke the
402 // user's code with our changes. Back out everything and fall through
403 // below to recompile again.
404 if !output.status.success() {
405 if env::var_os(BROKEN_CODE_ENV).is_none() {
406 for (path, file) in fixes.files.iter() {
407 debug!("reverting {:?} due to errors", path);
408 paths::write(path, &file.original_code)?;
409 }
410 }
411
412 let krate = {
413 let mut iter = json_error_rustc.get_args();
414 let mut krate = None;
415 while let Some(arg) = iter.next() {
416 if arg == "--crate-name" {
417 krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
418 }
419 }
420 krate
421 };
422 log_failed_fix(krate, &output.stderr, output.status)?;
423 }
424 }
425
426 // This final fall-through handles multiple cases;
427 // - If the fix failed, show the original warnings and suggestions.
428 // - If `--broken-code`, show the error messages.
429 // - If the fix succeeded, show any remaining warnings.
430 for arg in args.format_args {
431 // Add any json/error format arguments that Cargo wants. This allows
432 // things like colored output to work correctly.
433 rustc.arg(arg);
434 }
435 debug!("calling rustc to display remaining diagnostics: {rustc}");
436 exit_with(rustc.status()?);
437 }
438
439 #[derive(Default)]
440 struct FixedCrate {
441 files: HashMap<String, FixedFile>,
442 }
443
444 struct FixedFile {
445 errors_applying_fixes: Vec<String>,
446 fixes_applied: u32,
447 original_code: String,
448 }
449
450 /// Attempts to apply fixes to a single crate.
451 ///
452 /// This runs `rustc` (possibly multiple times) to gather suggestions from the
453 /// compiler and applies them to the files on disk.
454 fn rustfix_crate(
455 lock_addr: &str,
456 rustc: &ProcessBuilder,
457 filename: &Path,
458 args: &FixArgs,
459 config: &Config,
460 ) -> CargoResult<FixedCrate> {
461 if !args.can_run_rustfix(config)? {
462 // This fix should not be run. Skipping...
463 return Ok(FixedCrate::default());
464 }
465
466 // First up, we want to make sure that each crate is only checked by one
467 // process at a time. If two invocations concurrently check a crate then
468 // it's likely to corrupt it.
469 //
470 // Historically this used per-source-file locking, then per-package
471 // locking. It now uses a single, global lock as some users do things like
472 // #[path] or include!() of shared files between packages. Serializing
473 // makes it slower, but is the only safe way to prevent concurrent
474 // modification.
475 let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
476
477 // Next up, this is a bit suspicious, but we *iteratively* execute rustc and
478 // collect suggestions to feed to rustfix. Once we hit our limit of times to
479 // execute rustc or we appear to be reaching a fixed point we stop running
480 // rustc.
481 //
482 // This is currently done to handle code like:
483 //
484 // ::foo::<::Bar>();
485 //
486 // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
487 // The spans for these two suggestions are overlapping and its difficult in
488 // the compiler to **not** have overlapping spans here. As a result, a naive
489 // implementation would feed the two compiler suggestions for the above fix
490 // into `rustfix`, but one would be rejected because it overlaps with the
491 // other.
492 //
493 // In this case though, both suggestions are valid and can be automatically
494 // applied! To handle this case we execute rustc multiple times, collecting
495 // fixes each time we do so. Along the way we discard any suggestions that
496 // failed to apply, assuming that they can be fixed the next time we run
497 // rustc.
498 //
499 // Naturally, we want a few protections in place here though to avoid looping
500 // forever or otherwise losing data. To that end we have a few termination
501 // conditions:
502 //
503 // * Do this whole process a fixed number of times. In theory we probably
504 // need an infinite number of times to apply fixes, but we're not gonna
505 // sit around waiting for that.
506 // * If it looks like a fix genuinely can't be applied we need to bail out.
507 // Detect this when a fix fails to get applied *and* no suggestions
508 // successfully applied to the same file. In that case looks like we
509 // definitely can't make progress, so bail out.
510 let mut fixes = FixedCrate::default();
511 let mut last_fix_counts = HashMap::new();
512 let iterations = env::var("CARGO_FIX_MAX_RETRIES")
513 .ok()
514 .and_then(|n| n.parse().ok())
515 .unwrap_or(4);
516 for _ in 0..iterations {
517 last_fix_counts.clear();
518 for (path, file) in fixes.files.iter_mut() {
519 last_fix_counts.insert(path.clone(), file.fixes_applied);
520 // We'll generate new errors below.
521 file.errors_applying_fixes.clear();
522 }
523 rustfix_and_fix(&mut fixes, rustc, filename, config)?;
524 let mut progress_yet_to_be_made = false;
525 for (path, file) in fixes.files.iter_mut() {
526 if file.errors_applying_fixes.is_empty() {
527 continue;
528 }
529 // If anything was successfully fixed *and* there's at least one
530 // error, then assume the error was spurious and we'll try again on
531 // the next iteration.
532 if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
533 progress_yet_to_be_made = true;
534 }
535 }
536 if !progress_yet_to_be_made {
537 break;
538 }
539 }
540
541 // Any errors still remaining at this point need to be reported as probably
542 // bugs in Cargo and/or rustfix.
543 for (path, file) in fixes.files.iter_mut() {
544 for error in file.errors_applying_fixes.drain(..) {
545 Message::ReplaceFailed {
546 file: path.clone(),
547 message: error,
548 }
549 .post()?;
550 }
551 }
552
553 Ok(fixes)
554 }
555
556 /// Executes `rustc` to apply one round of suggestions to the crate in question.
557 ///
558 /// This will fill in the `fixes` map with original code, suggestions applied,
559 /// and any errors encountered while fixing files.
560 fn rustfix_and_fix(
561 fixes: &mut FixedCrate,
562 rustc: &ProcessBuilder,
563 filename: &Path,
564 config: &Config,
565 ) -> CargoResult<()> {
566 // If not empty, filter by these lints.
567 // TODO: implement a way to specify this.
568 let only = HashSet::new();
569
570 debug!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
571 let output = rustc.output()?;
572
573 // If rustc didn't succeed for whatever reasons then we're very likely to be
574 // looking at otherwise broken code. Let's not make things accidentally
575 // worse by applying fixes where a bug could cause *more* broken code.
576 // Instead, punt upwards which will reexec rustc over the original code,
577 // displaying pretty versions of the diagnostics we just read out.
578 if !output.status.success() && env::var_os(BROKEN_CODE_ENV).is_none() {
579 debug!(
580 "rustfixing `{:?}` failed, rustc exited with {:?}",
581 filename,
582 output.status.code()
583 );
584 return Ok(());
585 }
586
587 let fix_mode = env::var_os("__CARGO_FIX_YOLO")
588 .map(|_| rustfix::Filter::Everything)
589 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
590
591 // Sift through the output of the compiler to look for JSON messages.
592 // indicating fixes that we can apply.
593 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
594
595 let suggestions = stderr
596 .lines()
597 .filter(|x| !x.is_empty())
598 .inspect(|y| trace!("line: {}", y))
599 // Parse each line of stderr, ignoring errors, as they may not all be JSON.
600 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
601 // From each diagnostic, try to extract suggestions from rustc.
602 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
603
604 // Collect suggestions by file so we can apply them one at a time later.
605 let mut file_map = HashMap::new();
606 let mut num_suggestion = 0;
607 // It's safe since we won't read any content under home dir.
608 let home_path = config.home().as_path_unlocked();
609 for suggestion in suggestions {
610 trace!("suggestion");
611 // Make sure we've got a file associated with this suggestion and all
612 // snippets point to the same file. Right now it's not clear what
613 // we would do with multiple files.
614 let file_names = suggestion
615 .solutions
616 .iter()
617 .flat_map(|s| s.replacements.iter())
618 .map(|r| &r.snippet.file_name);
619
620 let file_name = if let Some(file_name) = file_names.clone().next() {
621 file_name.clone()
622 } else {
623 trace!("rejecting as it has no solutions {:?}", suggestion);
624 continue;
625 };
626
627 // Do not write into registry cache. See rust-lang/cargo#9857.
628 if Path::new(&file_name).starts_with(home_path) {
629 continue;
630 }
631
632 if !file_names.clone().all(|f| f == &file_name) {
633 trace!("rejecting as it changes multiple files: {:?}", suggestion);
634 continue;
635 }
636
637 trace!("adding suggestion for {:?}: {:?}", file_name, suggestion);
638 file_map
639 .entry(file_name)
640 .or_insert_with(Vec::new)
641 .push(suggestion);
642 num_suggestion += 1;
643 }
644
645 debug!(
646 "collected {} suggestions for `{}`",
647 num_suggestion,
648 filename.display(),
649 );
650
651 for (file, suggestions) in file_map {
652 // Attempt to read the source code for this file. If this fails then
653 // that'd be pretty surprising, so log a message and otherwise keep
654 // going.
655 let code = match paths::read(file.as_ref()) {
656 Ok(s) => s,
657 Err(e) => {
658 warn!("failed to read `{}`: {}", file, e);
659 continue;
660 }
661 };
662 let num_suggestions = suggestions.len();
663 debug!("applying {} fixes to {}", num_suggestions, file);
664
665 // If this file doesn't already exist then we just read the original
666 // code, so save it. If the file already exists then the original code
667 // doesn't need to be updated as we've just read an interim state with
668 // some fixes but perhaps not all.
669 let fixed_file = fixes
670 .files
671 .entry(file.clone())
672 .or_insert_with(|| FixedFile {
673 errors_applying_fixes: Vec::new(),
674 fixes_applied: 0,
675 original_code: code.clone(),
676 });
677 let mut fixed = CodeFix::new(&code);
678
679 // As mentioned above in `rustfix_crate`, we don't immediately warn
680 // about suggestions that fail to apply here, and instead we save them
681 // off for later processing.
682 for suggestion in suggestions.iter().rev() {
683 match fixed.apply(suggestion) {
684 Ok(()) => fixed_file.fixes_applied += 1,
685 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
686 }
687 }
688 let new_code = fixed.finish()?;
689 paths::write(&file, new_code)?;
690 }
691
692 Ok(())
693 }
694
695 fn exit_with(status: ExitStatus) -> ! {
696 #[cfg(unix)]
697 {
698 use std::io::Write;
699 use std::os::unix::prelude::*;
700 if let Some(signal) = status.signal() {
701 drop(writeln!(
702 std::io::stderr().lock(),
703 "child failed with signal `{}`",
704 signal
705 ));
706 process::exit(2);
707 }
708 }
709 process::exit(status.code().unwrap_or(3));
710 }
711
712 fn log_failed_fix(krate: Option<String>, stderr: &[u8], status: ExitStatus) -> CargoResult<()> {
713 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
714
715 let diagnostics = stderr
716 .lines()
717 .filter(|x| !x.is_empty())
718 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
719 let mut files = BTreeSet::new();
720 let mut errors = Vec::new();
721 for diagnostic in diagnostics {
722 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
723 for span in diagnostic.spans.into_iter() {
724 files.insert(span.file_name);
725 }
726 }
727 // Include any abnormal messages (like an ICE or whatever).
728 errors.extend(
729 stderr
730 .lines()
731 .filter(|x| !x.starts_with('{'))
732 .map(|x| x.to_string()),
733 );
734
735 let files = files.into_iter().collect();
736 let abnormal_exit = if status.code().map_or(false, is_simple_exit_code) {
737 None
738 } else {
739 Some(exit_status_to_string(status))
740 };
741 Message::FixFailed {
742 files,
743 krate,
744 errors,
745 abnormal_exit,
746 }
747 .post()?;
748
749 Ok(())
750 }
751
752 /// Various command-line options and settings used when `cargo` is running as
753 /// a proxy for `rustc` during the fix operation.
754 struct FixArgs {
755 /// This is the `.rs` file that is being fixed.
756 file: PathBuf,
757 /// If `--edition` is used to migrate to the next edition, this is the
758 /// edition we are migrating towards.
759 prepare_for_edition: Option<Edition>,
760 /// `true` if `--edition-idioms` is enabled.
761 idioms: bool,
762 /// The current edition.
763 ///
764 /// `None` if on 2015.
765 enabled_edition: Option<Edition>,
766 /// Other command-line arguments not reflected by other fields in
767 /// `FixArgs`.
768 other: Vec<OsString>,
769 /// Path to the `rustc` executable.
770 rustc: PathBuf,
771 /// Console output flags (`--error-format`, `--json`, etc.).
772 ///
773 /// The normal fix procedure always uses `--json`, so it overrides what
774 /// Cargo normally passes when applying fixes. When displaying warnings or
775 /// errors, it will use these flags.
776 format_args: Vec<String>,
777 }
778
779 impl FixArgs {
780 fn get() -> CargoResult<FixArgs> {
781 Self::from_args(env::args_os())
782 }
783
784 // This is a separate function so that we can use it in tests.
785 fn from_args(argv: impl IntoIterator<Item = OsString>) -> CargoResult<Self> {
786 let mut argv = argv.into_iter();
787 let mut rustc = argv
788 .nth(1)
789 .map(PathBuf::from)
790 .ok_or_else(|| anyhow::anyhow!("expected rustc or `@path` as first argument"))?;
791 let mut file = None;
792 let mut enabled_edition = None;
793 let mut other = Vec::new();
794 let mut format_args = Vec::new();
795
796 let mut handle_arg = |arg: OsString| -> CargoResult<()> {
797 let path = PathBuf::from(arg);
798 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
799 file = Some(path);
800 return Ok(());
801 }
802 if let Some(s) = path.to_str() {
803 if let Some(edition) = s.strip_prefix("--edition=") {
804 enabled_edition = Some(edition.parse()?);
805 return Ok(());
806 }
807 if s.starts_with("--error-format=") || s.starts_with("--json=") {
808 // Cargo may add error-format in some cases, but `cargo
809 // fix` wants to add its own.
810 format_args.push(s.to_string());
811 return Ok(());
812 }
813 }
814 other.push(path.into());
815 Ok(())
816 };
817
818 if let Some(argfile_path) = rustc.to_str().unwrap_or_default().strip_prefix("@") {
819 // Because cargo in fix-proxy-mode might hit the command line size limit,
820 // cargo fix need handle `@path` argfile for this special case.
821 if argv.next().is_some() {
822 bail!("argfile `@path` cannot be combined with other arguments");
823 }
824 let contents = fs::read_to_string(argfile_path)
825 .with_context(|| format!("failed to read argfile at `{argfile_path}`"))?;
826 let mut iter = contents.lines().map(OsString::from);
827 rustc = iter
828 .next()
829 .map(PathBuf::from)
830 .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?;
831 for arg in iter {
832 handle_arg(arg)?;
833 }
834 } else {
835 for arg in argv {
836 handle_arg(arg)?;
837 }
838 }
839
840 let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?;
841 let idioms = env::var(IDIOMS_ENV).is_ok();
842
843 let prepare_for_edition = env::var(EDITION_ENV).ok().map(|_| {
844 enabled_edition
845 .unwrap_or(Edition::Edition2015)
846 .saturating_next()
847 });
848
849 Ok(FixArgs {
850 file,
851 prepare_for_edition,
852 idioms,
853 enabled_edition,
854 other,
855 rustc,
856 format_args,
857 })
858 }
859
860 fn apply(&self, cmd: &mut ProcessBuilder) {
861 cmd.arg(&self.file);
862 cmd.args(&self.other);
863 if self.prepare_for_edition.is_some() {
864 // When migrating an edition, we don't want to fix other lints as
865 // they can sometimes add suggestions that fail to apply, causing
866 // the entire migration to fail. But those lints aren't needed to
867 // migrate.
868 cmd.arg("--cap-lints=allow");
869 } else {
870 // This allows `cargo fix` to work even if the crate has #[deny(warnings)].
871 cmd.arg("--cap-lints=warn");
872 }
873 if let Some(edition) = self.enabled_edition {
874 cmd.arg("--edition").arg(edition.to_string());
875 if self.idioms && edition.supports_idiom_lint() {
876 cmd.arg(format!("-Wrust-{}-idioms", edition));
877 }
878 }
879
880 if let Some(edition) = self.prepare_for_edition {
881 if edition.supports_compat_lint() {
882 cmd.arg("--force-warn")
883 .arg(format!("rust-{}-compatibility", edition));
884 }
885 }
886 }
887
888 /// Validates the edition, and sends a message indicating what is being
889 /// done. Returns a flag indicating whether this fix should be run.
890 fn can_run_rustfix(&self, config: &Config) -> CargoResult<bool> {
891 let to_edition = match self.prepare_for_edition {
892 Some(s) => s,
893 None => {
894 return Message::Fixing {
895 file: self.file.display().to_string(),
896 }
897 .post()
898 .and(Ok(true));
899 }
900 };
901 // Unfortunately determining which cargo targets are being built
902 // isn't easy, and each target can be a different edition. The
903 // cargo-as-rustc fix wrapper doesn't know anything about the
904 // workspace, so it can't check for the `cargo-features` unstable
905 // opt-in. As a compromise, this just restricts to the nightly
906 // toolchain.
907 //
908 // Unfortunately this results in a pretty poor error message when
909 // multiple jobs run in parallel (the error appears multiple
910 // times). Hopefully this doesn't happen often in practice.
911 if !to_edition.is_stable() && !config.nightly_features_allowed {
912 let message = format!(
913 "`{file}` is on the latest edition, but trying to \
914 migrate to edition {to_edition}.\n\
915 Edition {to_edition} is unstable and not allowed in \
916 this release, consider trying the nightly release channel.",
917 file = self.file.display(),
918 to_edition = to_edition
919 );
920 return Message::EditionAlreadyEnabled {
921 message,
922 edition: to_edition.previous().unwrap(),
923 }
924 .post()
925 .and(Ok(false)); // Do not run rustfix for this the edition.
926 }
927 let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015);
928 if from_edition == to_edition {
929 let message = format!(
930 "`{}` is already on the latest edition ({}), \
931 unable to migrate further",
932 self.file.display(),
933 to_edition
934 );
935 Message::EditionAlreadyEnabled {
936 message,
937 edition: to_edition,
938 }
939 .post()
940 } else {
941 Message::Migrating {
942 file: self.file.display().to_string(),
943 from_edition,
944 to_edition,
945 }
946 .post()
947 }
948 .and(Ok(true))
949 }
950 }
951
952 #[cfg(test)]
953 mod tests {
954 use super::FixArgs;
955 use std::ffi::OsString;
956 use std::io::Write as _;
957 use std::path::PathBuf;
958
959 #[test]
960 fn get_fix_args_from_argfile() {
961 let mut temp = tempfile::Builder::new().tempfile().unwrap();
962 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
963
964 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
965 temp.write_all(content.as_bytes()).unwrap();
966
967 let argfile = format!("@{}", temp.path().display());
968 let args = ["cargo", &argfile];
969 let fix_args = FixArgs::from_args(args.map(|x| x.into())).unwrap();
970 assert_eq!(fix_args.rustc, PathBuf::from("/path/to/rustc"));
971 assert_eq!(fix_args.file, main_rs.path());
972 assert_eq!(fix_args.other, vec![OsString::from("foobar")]);
973 }
974
975 #[test]
976 fn get_fix_args_from_argfile_with_extra_arg() {
977 let mut temp = tempfile::Builder::new().tempfile().unwrap();
978 let main_rs = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
979
980 let content = format!("/path/to/rustc\n{}\nfoobar\n", main_rs.path().display());
981 temp.write_all(content.as_bytes()).unwrap();
982
983 let argfile = format!("@{}", temp.path().display());
984 let args = ["cargo", &argfile, "boo!"];
985 match FixArgs::from_args(args.map(|x| x.into())) {
986 Err(e) => assert_eq!(
987 e.to_string(),
988 "argfile `@path` cannot be combined with other arguments"
989 ),
990 Ok(_) => panic!("should fail"),
991 }
992 }
993 }