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}
;
43 use std
::ffi
::OsString
;
44 use std
::path
::{Path, PathBuf}
;
45 use std
::process
::{self, ExitStatus}
;
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}
;
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}
;
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";
72 pub struct FixOptions
{
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
,
82 pub fn fix(ws
: &Workspace
<'_
>, opts
: &mut FixOptions
) -> CargoResult
<()> {
83 check_version_control(ws
.config(), opts
)?
;
85 check_resolver_change(ws
, opts
)?
;
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()?
;
94 opts
.compile_opts
.build_config
.force_rebuild
= true;
97 wrapper
.env(BROKEN_CODE_ENV
, "1");
101 wrapper
.env(EDITION_ENV
, "1");
104 wrapper
.env(IDIOMS_ENV
, "1");
110 .rustfix_diagnostic_server
111 .borrow_mut() = Some(RustfixDiagnosticServer
::new()?
);
113 if let Some(server
) = opts
116 .rustfix_diagnostic_server
120 server
.configure(&mut wrapper
);
123 let rustc
= ws
.config().load_global_rustc(Some(ws
))?
;
124 wrapper
.arg(&rustc
.path
);
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
);
130 ops
::compile(ws
, &opts
.compile_opts
)?
;
134 fn check_version_control(config
: &Config
, opts
: &FixOptions
) -> CargoResult
<()> {
135 if opts
.allow_no_vcs
{
138 if !existing_vcs_repo(config
.cwd(), config
.cwd()) {
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`"
146 if opts
.allow_dirty
&& opts
.allow_staged
{
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())
169 if !opts
.allow_dirty
{
170 dirty_files
.push(path
.to_string())
178 if dirty_files
.is_empty() && staged_files
.is_empty() {
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");
188 for file
in staged_files
{
189 files_list
.push_str(" * ");
190 files_list
.push_str(&file
);
191 files_list
.push_str(" (staged)\n");
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\
206 fn check_resolver_change(ws
: &Workspace
<'_
>, opts
: &FixOptions
) -> CargoResult
<()> {
207 let root
= ws
.root_maybe();
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.
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.
220 if root_pkg
.manifest().edition() != Edition
::Edition2018
{
221 // V1 to V2 only happens on 2018 to 2021.
225 MaybePackage
::Virtual(_vm
) => {
226 // Virtual workspaces don't have a global edition to set (yet).
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(
238 &opts
.compile_opts
.build_config
.requested_kinds
,
239 &opts
.compile_opts
.cli_features
,
242 crate::core
::resolver
::features
::ForceAllTargets
::No
,
245 let feature_opts
= FeatureOpts
::new_behavior(ResolveBehavior
::V2
, has_dev_units
);
246 let v2_features
= FeatureResolver
::resolve(
249 &ws_resolve
.targeted_resolve
,
251 &opts
.compile_opts
.cli_features
,
253 &opts
.compile_opts
.build_config
.requested_kinds
,
257 let diffs
= v2_features
.compare_legacy(&ws_resolve
.resolved_features
);
258 Ok((ws_resolve
, diffs
))
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.
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();
270 "Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo.",
274 "This may cause some dependencies to be built with fewer features enabled than previously."
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"
283 "When building the following dependencies, \
284 the given features will no longer be used:\n"
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)");
292 drop_eprint
!(config
, " removed features: ");
293 let joined
: Vec
<_
> = removed
.iter().map(|s
| s
.as_str()).collect();
294 drop_eprintln
!(config
, "{}", joined
.join(", "));
296 drop_eprint
!(config
, "\n");
298 if !without_dev_diffs
.is_empty() {
299 show_diffs(without_dev_diffs
);
301 if !with_dev_diffs
.is_empty() {
304 "The following differences only apply when building with dev-dependencies:\n"
306 show_diffs(with_dev_diffs
);
308 report_maybe_diesel(config
, &ws_resolve
.targeted_resolve
)?
;
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)
317 fn is_broken_diesel_migration(pid
: PackageId
) -> bool
{
318 pid
.name() == "diesel_migrations" && pid
.version().major
<= 1
321 if resolve
.iter().any(is_broken_diesel
) && resolve
.iter().any(is_broken_diesel_migration
) {
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.
334 /// Entry point for `cargo` running as a proxy for `rustc`.
336 /// This is called every time `cargo` is run to check if it is in proxy mode.
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
) {
345 Err(_
) => return Ok(false),
348 let args
= FixArgs
::get()?
;
349 trace
!("cargo-fix as rustc got file {:?}", args
.file
);
351 let workspace_rustc
= std
::env
::var("RUSTC_WORKSPACE_WRAPPER")
354 let mut rustc
= ProcessBuilder
::new(&args
.rustc
).wrapped(workspace_rustc
.as_ref());
355 rustc
.env_remove(FIX_ENV
);
356 args
.apply(&mut rustc
);
358 trace
!("start rustfixing {:?}", args
.file
);
359 let json_error_rustc
= {
360 let mut cmd
= rustc
.clone();
361 cmd
.arg("--error-format=json");
364 let fixes
= rustfix_crate(&lock_addr
, &json_error_rustc
, &args
.file
, &args
, config
)?
;
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.
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()?
;
378 if output
.status
.success() {
379 for (path
, file
) in fixes
.files
.iter() {
382 fixes
: file
.fixes_applied
,
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() {
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
)?
;
405 log_failed_fix(&output
.stderr
, output
.status
)?
;
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.
418 debug
!("calling rustc to display remaining diagnostics: {rustc}");
419 exit_with(rustc
.status()?
);
424 files
: HashMap
<String
, FixedFile
>,
428 errors_applying_fixes
: Vec
<String
>,
430 original_code
: String
,
433 /// Attempts to apply fixes to a single crate.
435 /// This runs `rustc` (possibly multiple times) to gather suggestions from the
436 /// compiler and applies them to the files on disk.
439 rustc
: &ProcessBuilder
,
443 ) -> Result
<FixedCrate
, Error
> {
444 if !args
.can_run_rustfix(config
)?
{
445 // This fix should not be run. Skipping...
446 return Ok(FixedCrate
::default());
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.
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
458 let _lock
= LockServerClient
::lock(&lock_addr
.parse()?
, "global")?
;
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
465 // This is currently done to handle code like:
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
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
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
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")
497 .and_then(|n
| n
.parse().ok())
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();
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() {
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;
519 if !progress_yet_to_be_made
{
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
{
539 /// Executes `rustc` to apply one round of suggestions to the crate in question.
541 /// This will fill in the `fixes` map with original code, suggestions applied,
542 /// and any errors encountered while fixing files.
544 fixes
: &mut FixedCrate
,
545 rustc
: &ProcessBuilder
,
548 ) -> Result
<(), Error
> {
549 // If not empty, filter by these lints.
550 // TODO: implement a way to specify this.
551 let only
= HashSet
::new();
553 debug
!("calling rustc to collect suggestions and validate previous fixes: {rustc}");
554 let output
= rustc
.output()?
;
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() {
563 "rustfixing `{:?}` failed, rustc exited with {:?}",
570 let fix_mode
= env
::var_os("__CARGO_FIX_YOLO")
571 .map(|_
| rustfix
::Filter
::Everything
)
572 .unwrap_or(rustfix
::Filter
::MachineApplicableOnly
);
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")?
;
578 let suggestions
= stderr
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
));
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
600 .flat_map(|s
| s
.replacements
.iter())
601 .map(|r
| &r
.snippet
.file_name
);
603 let file_name
= if let Some(file_name
) = file_names
.clone().next() {
606 trace
!("rejecting as it has no solutions {:?}", suggestion
);
610 // Do not write into registry cache. See rust-lang/cargo#9857.
611 if Path
::new(&file_name
).starts_with(home_path
) {
615 if !file_names
.clone().all(|f
| f
== &file_name
) {
616 trace
!("rejecting as it changes multiple files: {:?}", suggestion
);
620 trace
!("adding suggestion for {:?}: {:?}", file_name
, suggestion
);
623 .or_insert_with(Vec
::new
)
629 "collected {} suggestions for `{}`",
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
638 let code
= match paths
::read(file
.as_ref()) {
641 warn
!("failed to read `{}`: {}", file
, e
);
645 let num_suggestions
= suggestions
.len();
646 debug
!("applying {} fixes to {}", num_suggestions
, file
);
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
655 .or_insert_with(|| FixedFile
{
656 errors_applying_fixes
: Vec
::new(),
658 original_code
: code
.clone(),
660 let mut fixed
= CodeFix
::new(&code
);
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()),
671 let new_code
= fixed
.finish()?
;
672 paths
::write(&file
, new_code
)?
;
678 fn exit_with(status
: ExitStatus
) -> ! {
682 use std
::os
::unix
::prelude
::*;
683 if let Some(signal
) = status
.signal() {
685 std
::io
::stderr().lock(),
686 "child failed with signal `{}`",
692 process
::exit(status
.code().unwrap_or(3));
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")?
;
698 let diagnostics
= stderr
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
);
710 // Include any abnormal messages (like an ICE or whatever).
714 .filter(|x
| !x
.starts_with('
{'
))
715 .map(|x
| x
.to_string()),
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());
724 if arg
== "--crate-name" {
725 prev_dash_dash_krate_name
= true;
727 prev_dash_dash_krate_name
= false;
731 let files
= files
.into_iter().collect();
732 let abnormal_exit
= if status
.code().map_or(false, is_simple_exit_code
) {
735 Some(exit_status_to_string(status
))
748 /// Various command-line options and settings used when `cargo` is running as
749 /// a proxy for `rustc` during the fix operation.
751 /// This is the `.rs` file that is being fixed.
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.
758 /// The current edition.
760 /// `None` if on 2015.
761 enabled_edition
: Option
<Edition
>,
762 /// Other command-line arguments not reflected by other fields in
764 other
: Vec
<OsString
>,
765 /// Path to the `rustc` executable.
767 /// Console output flags (`--error-format`, `--json`, etc.).
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
>,
776 fn get() -> Result
<FixArgs
, Error
> {
777 let rustc
= env
::args_os()
780 .ok_or_else(|| anyhow
::anyhow
!("expected rustc as first argument"))?
;
782 let mut enabled_edition
= None
;
783 let mut other
= Vec
::new();
784 let mut format_args
= Vec
::new();
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() {
792 if let Some(s
) = path
.to_str() {
793 if let Some(edition
) = s
.strip_prefix("--edition=") {
794 enabled_edition
= Some(edition
.parse()?
);
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());
804 other
.push(path
.into());
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();
809 let prepare_for_edition
= env
::var(EDITION_ENV
).ok().map(|_
| {
811 .unwrap_or(Edition
::Edition2015
)
826 fn apply(&self, cmd
: &mut ProcessBuilder
) {
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
834 cmd
.arg("--cap-lints=allow");
836 // This allows `cargo fix` to work even if the crate has #[deny(warnings)].
837 cmd
.arg("--cap-lints=warn");
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
));
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
));
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
{
860 return Message
::Fixing
{
861 file
: self.file
.display().to_string(),
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
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
886 return Message
::EditionAlreadyEnabled
{
888 edition
: to_edition
.previous().unwrap(),
891 .and(Ok(false)); // Do not run rustfix for this the edition.
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",
901 Message
::EditionAlreadyEnabled
{
908 file
: self.file
.display().to_string(),