1 //! High-level overview of how `fix` works:
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.
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
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).
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
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:
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
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.
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}
;
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}
;
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}
;
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";
71 pub struct FixOptions
{
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
,
81 pub fn fix(ws
: &Workspace
<'_
>, opts
: &mut FixOptions
) -> CargoResult
<()> {
82 check_version_control(ws
.config(), opts
)?
;
84 check_resolver_change(ws
, opts
)?
;
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()?
;
93 opts
.compile_opts
.build_config
.force_rebuild
= true;
96 wrapper
.env(BROKEN_CODE_ENV
, "1");
100 wrapper
.env(EDITION_ENV
, "1");
103 wrapper
.env(IDIOMS_ENV
, "1");
109 .rustfix_diagnostic_server
110 .borrow_mut() = Some(RustfixDiagnosticServer
::new()?
);
112 if let Some(server
) = opts
115 .rustfix_diagnostic_server
119 server
.configure(&mut wrapper
);
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);
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
);
132 ops
::compile(ws
, &opts
.compile_opts
)?
;
136 fn check_version_control(config
: &Config
, opts
: &FixOptions
) -> CargoResult
<()> {
137 if opts
.allow_no_vcs
{
140 if !existing_vcs_repo(config
.cwd(), config
.cwd()) {
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`"
148 if opts
.allow_dirty
&& opts
.allow_staged
{
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())
171 if !opts
.allow_dirty
{
172 dirty_files
.push(path
.to_string())
180 if dirty_files
.is_empty() && staged_files
.is_empty() {
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");
190 for file
in staged_files
{
191 files_list
.push_str(" * ");
192 files_list
.push_str(&file
);
193 files_list
.push_str(" (staged)\n");
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\
208 fn check_resolver_change(ws
: &Workspace
<'_
>, opts
: &FixOptions
) -> CargoResult
<()> {
209 let root
= ws
.root_maybe();
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.
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.
222 if root_pkg
.manifest().edition() != Edition
::Edition2018
{
223 // V1 to V2 only happens on 2018 to 2021.
227 MaybePackage
::Virtual(_vm
) => {
228 // Virtual workspaces don't have a global edition to set (yet).
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(
240 &opts
.compile_opts
.build_config
.requested_kinds
,
241 &opts
.compile_opts
.cli_features
,
244 crate::core
::resolver
::features
::ForceAllTargets
::No
,
247 let feature_opts
= FeatureOpts
::new_behavior(ResolveBehavior
::V2
, has_dev_units
);
248 let v2_features
= FeatureResolver
::resolve(
251 &ws_resolve
.targeted_resolve
,
253 &opts
.compile_opts
.cli_features
,
255 &opts
.compile_opts
.build_config
.requested_kinds
,
259 let diffs
= v2_features
.compare_legacy(&ws_resolve
.resolved_features
);
260 Ok((ws_resolve
, diffs
))
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.
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();
272 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
276 "This may cause some dependencies to be built with fewer features enabled than previously."
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"
285 "When building the following dependencies, \
286 the given features will no longer be used:\n"
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)");
294 drop_eprint
!(config
, " removed features: ");
295 let joined
: Vec
<_
> = removed
.iter().map(|s
| s
.as_str()).collect();
296 drop_eprintln
!(config
, "{}", joined
.join(", "));
298 drop_eprint
!(config
, "\n");
300 if !without_dev_diffs
.is_empty() {
301 show_diffs(without_dev_diffs
);
303 if !with_dev_diffs
.is_empty() {
306 "The following differences only apply when building with dev-dependencies:\n"
308 show_diffs(with_dev_diffs
);
310 report_maybe_diesel(config
, &ws_resolve
.targeted_resolve
)?
;
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)
319 fn is_broken_diesel_migration(pid
: PackageId
) -> bool
{
320 pid
.name() == "diesel_migrations" && pid
.version().major
<= 1
323 if resolve
.iter().any(is_broken_diesel
) && resolve
.iter().any(is_broken_diesel_migration
) {
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.
336 /// Provide the lock address when running in proxy mode
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()
344 /// Entry point for `cargo` running as a proxy for `rustc`.
346 /// This is called every time `cargo` is run to check if it is in proxy mode.
348 /// If there are warnings or errors, this does not return,
349 /// and the process exits with the corresponding `rustc` exit code.
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
);
356 let workspace_rustc
= std
::env
::var("RUSTC_WORKSPACE_WRAPPER")
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
);
364 trace
!("start rustfixing {:?}", args
.file
);
365 let json_error_rustc
= {
366 let mut cmd
= rustc
.clone();
367 cmd
.arg("--error-format=json");
370 let fixes
= rustfix_crate(&lock_addr
, &json_error_rustc
, &args
.file
, &args
, config
)?
;
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.
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()?
;
384 if output
.status
.success() {
385 for (path
, file
) in fixes
.files
.iter() {
388 fixes
: file
.fixes_applied
,
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() {
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
)?
;
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());
422 log_failed_fix(krate
, &output
.stderr
, output
.status
)?
;
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.
435 debug
!("calling rustc to display remaining diagnostics: {rustc}");
436 exit_with(rustc
.status()?
);
441 files
: HashMap
<String
, FixedFile
>,
445 errors_applying_fixes
: Vec
<String
>,
447 original_code
: String
,
450 /// Attempts to apply fixes to a single crate.
452 /// This runs `rustc` (possibly multiple times) to gather suggestions from the
453 /// compiler and applies them to the files on disk.
456 rustc
: &ProcessBuilder
,
460 ) -> CargoResult
<FixedCrate
> {
461 if !args
.can_run_rustfix(config
)?
{
462 // This fix should not be run. Skipping...
463 return Ok(FixedCrate
::default());
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.
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
475 let _lock
= LockServerClient
::lock(&lock_addr
.parse()?
, "global")?
;
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
482 // This is currently done to handle code like:
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
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
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
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")
514 .and_then(|n
| n
.parse().ok())
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();
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() {
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;
536 if !progress_yet_to_be_made
{
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
{
556 /// Executes `rustc` to apply one round of suggestions to the crate in question.
558 /// This will fill in the `fixes` map with original code, suggestions applied,
559 /// and any errors encountered while fixing files.
561 fixes
: &mut FixedCrate
,
562 rustc
: &ProcessBuilder
,
565 ) -> CargoResult
<()> {
566 // If not empty, filter by these lints.
567 // TODO: implement a way to specify this.
568 let only
= HashSet
::new();
570 debug
!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
571 let output
= rustc
.output()?
;
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() {
580 "rustfixing `{:?}` failed, rustc exited with {:?}",
587 let fix_mode
= env
::var_os("__CARGO_FIX_YOLO")
588 .map(|_
| rustfix
::Filter
::Everything
)
589 .unwrap_or(rustfix
::Filter
::MachineApplicableOnly
);
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")?
;
595 let suggestions
= stderr
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
));
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
617 .flat_map(|s
| s
.replacements
.iter())
618 .map(|r
| &r
.snippet
.file_name
);
620 let file_name
= if let Some(file_name
) = file_names
.clone().next() {
623 trace
!("rejecting as it has no solutions {:?}", suggestion
);
627 // Do not write into registry cache. See rust-lang/cargo#9857.
628 if Path
::new(&file_name
).starts_with(home_path
) {
632 if !file_names
.clone().all(|f
| f
== &file_name
) {
633 trace
!("rejecting as it changes multiple files: {:?}", suggestion
);
637 trace
!("adding suggestion for {:?}: {:?}", file_name
, suggestion
);
640 .or_insert_with(Vec
::new
)
646 "collected {} suggestions for `{}`",
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
655 let code
= match paths
::read(file
.as_ref()) {
658 warn
!("failed to read `{}`: {}", file
, e
);
662 let num_suggestions
= suggestions
.len();
663 debug
!("applying {} fixes to {}", num_suggestions
, file
);
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
672 .or_insert_with(|| FixedFile
{
673 errors_applying_fixes
: Vec
::new(),
675 original_code
: code
.clone(),
677 let mut fixed
= CodeFix
::new(&code
);
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()),
688 let new_code
= fixed
.finish()?
;
689 paths
::write(&file
, new_code
)?
;
695 fn exit_with(status
: ExitStatus
) -> ! {
699 use std
::os
::unix
::prelude
::*;
700 if let Some(signal
) = status
.signal() {
702 std
::io
::stderr().lock(),
703 "child failed with signal `{}`",
709 process
::exit(status
.code().unwrap_or(3));
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")?
;
715 let diagnostics
= stderr
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
);
727 // Include any abnormal messages (like an ICE or whatever).
731 .filter(|x
| !x
.starts_with('
{'
))
732 .map(|x
| x
.to_string()),
735 let files
= files
.into_iter().collect();
736 let abnormal_exit
= if status
.code().map_or(false, is_simple_exit_code
) {
739 Some(exit_status_to_string(status
))
752 /// Various command-line options and settings used when `cargo` is running as
753 /// a proxy for `rustc` during the fix operation.
755 /// This is the `.rs` file that is being fixed.
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.
762 /// The current edition.
764 /// `None` if on 2015.
765 enabled_edition
: Option
<Edition
>,
766 /// Other command-line arguments not reflected by other fields in
768 other
: Vec
<OsString
>,
769 /// Path to the `rustc` executable.
771 /// Console output flags (`--error-format`, `--json`, etc.).
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
>,
780 fn get() -> CargoResult
<FixArgs
> {
781 Self::from_args(env
::args_os())
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();
790 .ok_or_else(|| anyhow
::anyhow
!("expected rustc or `@path` as first argument"))?
;
792 let mut enabled_edition
= None
;
793 let mut other
= Vec
::new();
794 let mut format_args
= Vec
::new();
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() {
802 if let Some(s
) = path
.to_str() {
803 if let Some(edition
) = s
.strip_prefix("--edition=") {
804 enabled_edition
= Some(edition
.parse()?
);
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());
814 other
.push(path
.into());
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");
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
);
830 .ok_or_else(|| anyhow
::anyhow
!("expected rustc as first argument"))?
;
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();
843 let prepare_for_edition
= env
::var(EDITION_ENV
).ok().map(|_
| {
845 .unwrap_or(Edition
::Edition2015
)
860 fn apply(&self, cmd
: &mut ProcessBuilder
) {
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
868 cmd
.arg("--cap-lints=allow");
870 // This allows `cargo fix` to work even if the crate has #[deny(warnings)].
871 cmd
.arg("--cap-lints=warn");
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
));
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
));
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
{
894 return Message
::Fixing
{
895 file
: self.file
.display().to_string(),
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
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
920 return Message
::EditionAlreadyEnabled
{
922 edition
: to_edition
.previous().unwrap(),
925 .and(Ok(false)); // Do not run rustfix for this the edition.
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",
935 Message
::EditionAlreadyEnabled
{
942 file
: self.file
.display().to_string(),
955 use std
::ffi
::OsString
;
956 use std
::io
::Write
as _
;
957 use std
::path
::PathBuf
;
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();
964 let content
= format
!("/path/to/rustc\n{}\nfoobar\n", main_rs
.path().display());
965 temp
.write_all(content
.as_bytes()).unwrap();
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")]);
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();
980 let content
= format
!("/path/to/rustc\n{}\nfoobar\n", main_rs
.path().display());
981 temp
.write_all(content
.as_bytes()).unwrap();
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
!(
988 "argfile `@path` cannot be combined with other arguments"
990 Ok(_
) => panic
!("should fail"),