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