1use std::collections::{BTreeSet, HashMap, HashSet};
42use std::env;
43use std::ffi::OsString;
44use std::fs;
45use std::path::{Path, PathBuf};
46use std::process::{self, Command, ExitStatus};
47use std::str;
48
49use anyhow::{Context, Error};
50use log::{debug, trace, warn};
51use rustfix::diagnostics::Diagnostic;
52use rustfix::{self, CodeFix};
53
54use crate::core::Workspace;
55use crate::ops::{self, CompileOptions};
56use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
57use crate::util::errors::CargoResult;
58use crate::util::{self, Config, ProcessBuilder};
59use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
60
61const FIX_ENV: &str = "__CARGO_FIX_PLZ";
62const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
63const PREPARE_FOR_ENV: &str = "__CARGO_FIX_PREPARE_FOR";
64const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
65const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS";
66
67pub struct FixOptions<'a> {
68 pub edition: bool,
69 pub prepare_for: Option<&'a str>,
70 pub idioms: bool,
71 pub compile_opts: CompileOptions,
72 pub allow_dirty: bool,
73 pub allow_no_vcs: bool,
74 pub allow_staged: bool,
75 pub broken_code: bool,
76}
77
78pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions<'_>) -> CargoResult<()> {
79 check_version_control(ws.config(), opts)?;
80
81 let lock_server = LockServer::new()?;
83 let mut wrapper = util::process(env::current_exe()?);
84 wrapper.env(FIX_ENV, lock_server.addr().to_string());
85 let _started = lock_server.start()?;
86
87 opts.compile_opts.build_config.force_rebuild = true;
88
89 if opts.broken_code {
90 wrapper.env(BROKEN_CODE_ENV, "1");
91 }
92
93 if opts.edition {
94 wrapper.env(EDITION_ENV, "1");
95 } else if let Some(edition) = opts.prepare_for {
96 wrapper.env(PREPARE_FOR_ENV, edition);
97 }
98 if opts.idioms {
99 wrapper.env(IDIOMS_ENV, "1");
100 }
101
102 *opts
103 .compile_opts
104 .build_config
105 .rustfix_diagnostic_server
106 .borrow_mut() = Some(RustfixDiagnosticServer::new()?);
107
108 if let Some(server) = opts
109 .compile_opts
110 .build_config
111 .rustfix_diagnostic_server
112 .borrow()
113 .as_ref()
114 {
115 server.configure(&mut wrapper);
116 }
117
118 let rustc = ws.config().load_global_rustc(Some(ws))?;
119 wrapper.arg(&rustc.path);
120
121 opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper);
124
125 ops::compile(ws, &opts.compile_opts)?;
126 Ok(())
127}
128
129fn check_version_control(config: &Config, opts: &FixOptions<'_>) -> CargoResult<()> {
130 if opts.allow_no_vcs {
131 return Ok(());
132 }
133 if !existing_vcs_repo(config.cwd(), config.cwd()) {
134 anyhow::bail!(
135 "no VCS found for this package and `cargo fix` can potentially \
136 perform destructive changes; if you'd like to suppress this \
137 error pass `--allow-no-vcs`"
138 )
139 }
140
141 if opts.allow_dirty && opts.allow_staged {
142 return Ok(());
143 }
144
145 let mut dirty_files = Vec::new();
146 let mut staged_files = Vec::new();
147 if let Ok(repo) = git2::Repository::discover(config.cwd()) {
148 let mut repo_opts = git2::StatusOptions::new();
149 repo_opts.include_ignored(false);
150 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
151 if let Some(path) = status.path() {
152 match status.status() {
153 git2::Status::CURRENT => (),
154 git2::Status::INDEX_NEW
155 | git2::Status::INDEX_MODIFIED
156 | git2::Status::INDEX_DELETED
157 | git2::Status::INDEX_RENAMED
158 | git2::Status::INDEX_TYPECHANGE => {
159 if !opts.allow_staged {
160 staged_files.push(path.to_string())
161 }
162 }
163 _ => {
164 if !opts.allow_dirty {
165 dirty_files.push(path.to_string())
166 }
167 }
168 };
169 }
170 }
171 }
172
173 if dirty_files.is_empty() && staged_files.is_empty() {
174 return Ok(());
175 }
176
177 let mut files_list = String::new();
178 for file in dirty_files {
179 files_list.push_str(" * ");
180 files_list.push_str(&file);
181 files_list.push_str(" (dirty)\n");
182 }
183 for file in staged_files {
184 files_list.push_str(" * ");
185 files_list.push_str(&file);
186 files_list.push_str(" (staged)\n");
187 }
188
189 anyhow::bail!(
190 "the working directory of this package has uncommitted changes, and \
191 `cargo fix` can potentially perform destructive changes; if you'd \
192 like to suppress this error pass `--allow-dirty`, `--allow-staged`, \
193 or commit the changes to these files:\n\
194 \n\
195 {}\n\
196 ",
197 files_list
198 );
199}
200
201pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
202 let lock_addr = match env::var(FIX_ENV) {
203 Ok(s) => s,
204 Err(_) => return Ok(false),
205 };
206
207 let args = FixArgs::get();
208 trace!("cargo-fix as rustc got file {:?}", args.file);
209
210 let rustc = args.rustc.as_ref().expect("fix wrapper rustc was not set");
211 let workspace_rustc = std::env::var("RUSTC_WORKSPACE_WRAPPER")
212 .map(PathBuf::from)
213 .ok();
214 let rustc = util::process(rustc).wrapped(workspace_rustc.as_ref());
215
216 let mut fixes = FixedCrate::default();
217 if let Some(path) = &args.file {
218 trace!("start rustfixing {:?}", path);
219 fixes = rustfix_crate(&lock_addr, &rustc, path, &args)?;
220 }
221
222 if !fixes.files.is_empty() {
231 let mut cmd = rustc.build_command();
232 args.apply(&mut cmd);
233 cmd.arg("--error-format=json");
234 let output = cmd.output().context("failed to spawn rustc")?;
235
236 if output.status.success() {
237 for (path, file) in fixes.files.iter() {
238 Message::Fixing {
239 file: path.clone(),
240 fixes: file.fixes_applied,
241 }
242 .post()?;
243 }
244 }
245
246 if output.status.success() && output.stderr.is_empty() {
250 return Ok(true);
251 }
252
253 if !output.status.success() {
257 if env::var_os(BROKEN_CODE_ENV).is_none() {
258 for (path, file) in fixes.files.iter() {
259 fs::write(path, &file.original_code)
260 .with_context(|| format!("failed to write file `{}`", path))?;
261 }
262 }
263 log_failed_fix(&output.stderr)?;
264 }
265 }
266
267 let mut cmd = rustc.build_command();
272 args.apply(&mut cmd);
273 for arg in args.format_args {
274 cmd.arg(arg);
277 }
278 exit_with(cmd.status().context("failed to spawn rustc")?);
279}
280
281#[derive(Default)]
282struct FixedCrate {
283 files: HashMap<String, FixedFile>,
284}
285
286struct FixedFile {
287 errors_applying_fixes: Vec<String>,
288 fixes_applied: u32,
289 original_code: String,
290}
291
292fn rustfix_crate(
293 lock_addr: &str,
294 rustc: &ProcessBuilder,
295 filename: &Path,
296 args: &FixArgs,
297) -> Result<FixedCrate, Error> {
298 args.verify_not_preparing_for_enabled_edition()?;
299
300 let dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is missing?");
307 let _lock = LockServerClient::lock(&lock_addr.parse()?, dir)?;
308
309 let mut fixes = FixedCrate::default();
343 let mut last_fix_counts = HashMap::new();
344 let iterations = env::var("CARGO_FIX_MAX_RETRIES")
345 .ok()
346 .and_then(|n| n.parse().ok())
347 .unwrap_or(4);
348 for _ in 0..iterations {
349 last_fix_counts.clear();
350 for (path, file) in fixes.files.iter_mut() {
351 last_fix_counts.insert(path.clone(), file.fixes_applied);
352 file.errors_applying_fixes.clear();
354 }
355 rustfix_and_fix(&mut fixes, rustc, filename, args)?;
356 let mut progress_yet_to_be_made = false;
357 for (path, file) in fixes.files.iter_mut() {
358 if file.errors_applying_fixes.is_empty() {
359 continue;
360 }
361 if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
365 progress_yet_to_be_made = true;
366 }
367 }
368 if !progress_yet_to_be_made {
369 break;
370 }
371 }
372
373 for (path, file) in fixes.files.iter_mut() {
376 for error in file.errors_applying_fixes.drain(..) {
377 Message::ReplaceFailed {
378 file: path.clone(),
379 message: error,
380 }
381 .post()?;
382 }
383 }
384
385 Ok(fixes)
386}
387
388fn rustfix_and_fix(
393 fixes: &mut FixedCrate,
394 rustc: &ProcessBuilder,
395 filename: &Path,
396 args: &FixArgs,
397) -> Result<(), Error> {
398 let only = HashSet::new();
401
402 let mut cmd = rustc.build_command();
403 cmd.arg("--error-format=json");
404 args.apply(&mut cmd);
405 let output = cmd.output().with_context(|| {
406 format!(
407 "failed to execute `{}`",
408 rustc.get_program().to_string_lossy()
409 )
410 })?;
411
412 if !output.status.success() && env::var_os(BROKEN_CODE_ENV).is_none() {
418 debug!(
419 "rustfixing `{:?}` failed, rustc exited with {:?}",
420 filename,
421 output.status.code()
422 );
423 return Ok(());
424 }
425
426 let fix_mode = env::var_os("__CARGO_FIX_YOLO")
427 .map(|_| rustfix::Filter::Everything)
428 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
429
430 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?;
433
434 let suggestions = stderr
435 .lines()
436 .filter(|x| !x.is_empty())
437 .inspect(|y| trace!("line: {}", y))
438 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
440 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
442
443 let mut file_map = HashMap::new();
445 let mut num_suggestion = 0;
446 for suggestion in suggestions {
447 trace!("suggestion");
448 let file_names = suggestion
452 .solutions
453 .iter()
454 .flat_map(|s| s.replacements.iter())
455 .map(|r| &r.snippet.file_name);
456
457 let file_name = if let Some(file_name) = file_names.clone().next() {
458 file_name.clone()
459 } else {
460 trace!("rejecting as it has no solutions {:?}", suggestion);
461 continue;
462 };
463
464 if !file_names.clone().all(|f| f == &file_name) {
465 trace!("rejecting as it changes multiple files: {:?}", suggestion);
466 continue;
467 }
468
469 file_map
470 .entry(file_name)
471 .or_insert_with(Vec::new)
472 .push(suggestion);
473 num_suggestion += 1;
474 }
475
476 debug!(
477 "collected {} suggestions for `{}`",
478 num_suggestion,
479 filename.display(),
480 );
481
482 for (file, suggestions) in file_map {
483 let code = match util::paths::read(file.as_ref()) {
487 Ok(s) => s,
488 Err(e) => {
489 warn!("failed to read `{}`: {}", file, e);
490 continue;
491 }
492 };
493 let num_suggestions = suggestions.len();
494 debug!("applying {} fixes to {}", num_suggestions, file);
495
496 let fixed_file = fixes
501 .files
502 .entry(file.clone())
503 .or_insert_with(|| FixedFile {
504 errors_applying_fixes: Vec::new(),
505 fixes_applied: 0,
506 original_code: code.clone(),
507 });
508 let mut fixed = CodeFix::new(&code);
509
510 for suggestion in suggestions.iter().rev() {
514 match fixed.apply(suggestion) {
515 Ok(()) => fixed_file.fixes_applied += 1,
516 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
517 }
518 }
519 let new_code = fixed.finish()?;
520 fs::write(&file, new_code).with_context(|| format!("failed to write file `{}`", file))?;
521 }
522
523 Ok(())
524}
525
526fn exit_with(status: ExitStatus) -> ! {
527 #[cfg(unix)]
528 {
529 use std::os::unix::prelude::*;
530 if let Some(signal) = status.signal() {
531 eprintln!("child failed with signal `{}`", signal);
532 process::exit(2);
533 }
534 }
535 process::exit(status.code().unwrap_or(3));
536}
537
538fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {
539 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
540
541 let diagnostics = stderr
542 .lines()
543 .filter(|x| !x.is_empty())
544 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
545 let mut files = BTreeSet::new();
546 let mut errors = Vec::new();
547 for diagnostic in diagnostics {
548 errors.push(diagnostic.rendered.unwrap_or(diagnostic.message));
549 for span in diagnostic.spans.into_iter() {
550 files.insert(span.file_name);
551 }
552 }
553 let mut krate = None;
554 let mut prev_dash_dash_krate_name = false;
555 for arg in env::args() {
556 if prev_dash_dash_krate_name {
557 krate = Some(arg.clone());
558 }
559
560 if arg == "--crate-name" {
561 prev_dash_dash_krate_name = true;
562 } else {
563 prev_dash_dash_krate_name = false;
564 }
565 }
566
567 let files = files.into_iter().collect();
568 Message::FixFailed {
569 files,
570 krate,
571 errors,
572 }
573 .post()?;
574
575 Ok(())
576}
577
578#[derive(Default)]
579struct FixArgs {
580 file: Option<PathBuf>,
581 prepare_for_edition: PrepareFor,
582 idioms: bool,
583 enabled_edition: Option<String>,
584 other: Vec<OsString>,
585 rustc: Option<PathBuf>,
586 format_args: Vec<String>,
587}
588
589enum PrepareFor {
590 Next,
591 Edition(String),
592 None,
593}
594
595impl Default for PrepareFor {
596 fn default() -> PrepareFor {
597 PrepareFor::None
598 }
599}
600
601impl FixArgs {
602 fn get() -> FixArgs {
603 let mut ret = FixArgs::default();
604
605 ret.rustc = env::args_os().nth(1).map(PathBuf::from);
606
607 for arg in env::args_os().skip(2) {
608 let path = PathBuf::from(arg);
609 if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() {
610 ret.file = Some(path);
611 continue;
612 }
613 if let Some(s) = path.to_str() {
614 let prefix = "--edition=";
615 if s.starts_with(prefix) {
616 ret.enabled_edition = Some(s[prefix.len()..].to_string());
617 continue;
618 }
619 if s.starts_with("--error-format=") || s.starts_with("--json=") {
620 ret.format_args.push(s.to_string());
623 continue;
624 }
625 }
626 ret.other.push(path.into());
627 }
628 if let Ok(s) = env::var(PREPARE_FOR_ENV) {
629 ret.prepare_for_edition = PrepareFor::Edition(s);
630 } else if env::var(EDITION_ENV).is_ok() {
631 ret.prepare_for_edition = PrepareFor::Next;
632 }
633
634 ret.idioms = env::var(IDIOMS_ENV).is_ok();
635 ret
636 }
637
638 fn apply(&self, cmd: &mut Command) {
639 if let Some(path) = &self.file {
640 cmd.arg(path);
641 }
642
643 cmd.args(&self.other).arg("--cap-lints=warn");
644 if let Some(edition) = &self.enabled_edition {
645 cmd.arg("--edition").arg(edition);
646 if self.idioms && edition == "2018" {
647 cmd.arg("-Wrust-2018-idioms");
648 }
649 }
650
651 if let Some(edition) = self.prepare_for_edition_resolve() {
652 cmd.arg("-W").arg(format!("rust-{}-compatibility", edition));
653 }
654 }
655
656 fn verify_not_preparing_for_enabled_edition(&self) -> CargoResult<()> {
664 let edition = match self.prepare_for_edition_resolve() {
665 Some(s) => s,
666 None => return Ok(()),
667 };
668 let enabled = match &self.enabled_edition {
669 Some(s) => s,
670 None => return Ok(()),
671 };
672 if edition != enabled {
673 return Ok(());
674 }
675 let path = match &self.file {
676 Some(s) => s,
677 None => return Ok(()),
678 };
679
680 Message::EditionAlreadyEnabled {
681 file: path.display().to_string(),
682 edition: edition.to_string(),
683 }
684 .post()?;
685
686 process::exit(1);
687 }
688
689 fn prepare_for_edition_resolve(&self) -> Option<&str> {
690 match &self.prepare_for_edition {
691 PrepareFor::Edition(s) => Some(s),
692 PrepareFor::Next => Some(self.next_edition()),
693 PrepareFor::None => None,
694 }
695 }
696
697 fn next_edition(&self) -> &str {
698 match self.enabled_edition.as_deref() {
699 None | Some("2015") => "2018",
701
702 _ => "2018",
706 }
707 }
708}