]> git.proxmox.com Git - cargo.git/blame - src/cargo/ops/fix.rs
add `clippy::`
[cargo.git] / src / cargo / ops / fix.rs
CommitLineData
b02ba377
AC
1use std::collections::{HashMap, HashSet, BTreeSet};
2use std::env;
fa7a3877
AC
3use std::ffi::OsString;
4use std::fs;
5use std::path::{Path, PathBuf};
b02ba377
AC
6use std::process::{self, Command, ExitStatus};
7use std::str;
8
9use failure::{Error, ResultExt};
10use git2;
11use rustfix::diagnostics::Diagnostic;
876a5036 12use rustfix::{self, CodeFix};
b02ba377
AC
13use serde_json;
14
15use core::Workspace;
16use ops::{self, CompileOptions};
b02ba377 17use util::errors::CargoResult;
eea58f20 18use util::{LockServer, LockServerClient, existing_vcs_repo};
b02ba377
AC
19use util::diagnostic_server::{Message, RustfixDiagnosticServer};
20use util::paths;
21
22const FIX_ENV: &str = "__CARGO_FIX_PLZ";
23const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
b2b120e9 24const PREPARE_FOR_ENV: &str = "__CARGO_FIX_PREPARE_FOR";
fa7a3877 25const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
b02ba377 26
80f9d318
AC
27const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS";
28
b02ba377 29pub struct FixOptions<'a> {
b2b120e9
AC
30 pub edition: bool,
31 pub prepare_for: Option<&'a str>,
80f9d318 32 pub idioms: bool,
b02ba377
AC
33 pub compile_opts: CompileOptions<'a>,
34 pub allow_dirty: bool,
35 pub allow_no_vcs: bool,
6a616cb7 36 pub allow_staged: bool,
b02ba377
AC
37 pub broken_code: bool,
38}
39
40pub fn fix(ws: &Workspace, opts: &mut FixOptions) -> CargoResult<()> {
41 check_version_control(opts)?;
42
43 // Spin up our lock server which our subprocesses will use to synchronize
44 // fixes.
45 let lock_server = LockServer::new()?;
46 opts.compile_opts.build_config.extra_rustc_env.push((
47 FIX_ENV.to_string(),
48 lock_server.addr().to_string(),
49 ));
50 let _started = lock_server.start()?;
51
616e0ad3
PH
52 opts.compile_opts.build_config.force_rebuild = true;
53
b02ba377
AC
54 if opts.broken_code {
55 let key = BROKEN_CODE_ENV.to_string();
56 opts.compile_opts.build_config.extra_rustc_env.push((key, "1".to_string()));
57 }
58
b2b120e9
AC
59 if opts.edition {
60 let key = EDITION_ENV.to_string();
61 opts.compile_opts.build_config.extra_rustc_env.push((key, "1".to_string()));
62 } else if let Some(edition) = opts.prepare_for {
fa7a3877 63 opts.compile_opts.build_config.extra_rustc_env.push((
b2b120e9 64 PREPARE_FOR_ENV.to_string(),
fa7a3877
AC
65 edition.to_string(),
66 ));
b02ba377 67 }
80f9d318
AC
68 if opts.idioms {
69 opts.compile_opts.build_config.extra_rustc_env.push((
70 IDIOMS_ENV.to_string(),
71 "1".to_string(),
72 ));
73 }
b02ba377
AC
74 opts.compile_opts.build_config.cargo_as_rustc_wrapper = true;
75 *opts.compile_opts.build_config.rustfix_diagnostic_server.borrow_mut() =
76 Some(RustfixDiagnosticServer::new()?);
77
78 ops::compile(ws, &opts.compile_opts)?;
79 Ok(())
80}
81
82fn check_version_control(opts: &FixOptions) -> CargoResult<()> {
83 if opts.allow_no_vcs {
84 return Ok(())
85 }
86 let config = opts.compile_opts.config;
87 if !existing_vcs_repo(config.cwd(), config.cwd()) {
3492a390 88 bail!("no VCS found for this package and `cargo fix` can potentially \
b02ba377
AC
89 perform destructive changes; if you'd like to suppress this \
90 error pass `--allow-no-vcs`")
91 }
92
6a616cb7 93 if opts.allow_dirty && opts.allow_staged {
b02ba377
AC
94 return Ok(())
95 }
96
97 let mut dirty_files = Vec::new();
6a616cb7 98 let mut staged_files = Vec::new();
b02ba377 99 if let Ok(repo) = git2::Repository::discover(config.cwd()) {
6a616cb7
JJ
100 let mut repo_opts = git2::StatusOptions::new();
101 repo_opts.include_ignored(false);
102 for status in repo.statuses(Some(&mut repo_opts))?.iter() {
103 if let Some(path) = status.path() {
104 match status.status() {
105 git2::Status::CURRENT => (),
106 git2::Status::INDEX_NEW |
107 git2::Status::INDEX_MODIFIED |
108 git2::Status::INDEX_DELETED |
109 git2::Status::INDEX_RENAMED |
110 git2::Status::INDEX_TYPECHANGE =>
111 if !opts.allow_staged {
112 staged_files.push(path.to_string())
113 },
114 _ =>
115 if !opts.allow_dirty {
116 dirty_files.push(path.to_string())
117 },
118 };
b02ba377
AC
119 }
120
121 }
122 }
123
6a616cb7 124 if dirty_files.is_empty() && staged_files.is_empty() {
b02ba377
AC
125 return Ok(())
126 }
127
128 let mut files_list = String::new();
129 for file in dirty_files {
130 files_list.push_str(" * ");
131 files_list.push_str(&file);
4539ff21 132 files_list.push_str(" (dirty)\n");
b02ba377 133 }
6a616cb7
JJ
134 for file in staged_files {
135 files_list.push_str(" * ");
136 files_list.push_str(&file);
137 files_list.push_str(" (staged)\n");
138 }
b02ba377 139
3492a390 140 bail!("the working directory of this package has uncommitted changes, and \
b02ba377 141 `cargo fix` can potentially perform destructive changes; if you'd \
6a616cb7
JJ
142 like to suppress this error pass `--allow-dirty`, `--allow-staged`, \
143 or commit the changes to these files:\n\
b02ba377
AC
144 \n\
145 {}\n\
146 ", files_list);
147}
148
149pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
150 let lock_addr = match env::var(FIX_ENV) {
151 Ok(s) => s,
152 Err(_) => return Ok(false),
153 };
154
fa7a3877
AC
155 let args = FixArgs::get();
156 trace!("cargo-fix as rustc got file {:?}", args.file);
b02ba377
AC
157 let rustc = env::var_os("RUSTC").expect("failed to find RUSTC env var");
158
159 // Our goal is to fix only the crates that the end user is interested in.
160 // That's very likely to only mean the crates in the workspace the user is
161 // working on, not random crates.io crates.
162 //
163 // To that end we only actually try to fix things if it looks like we're
164 // compiling a Rust file and it *doesn't* have an absolute filename. That's
165 // not the best heuristic but matches what Cargo does today at least.
166 let mut fixes = FixedCrate::default();
fa7a3877 167 if let Some(path) = &args.file {
4f784a10 168 if args.primary_package {
b02ba377 169 trace!("start rustfixing {:?}", path);
fa7a3877 170 fixes = rustfix_crate(&lock_addr, rustc.as_ref(), path, &args)?;
b02ba377
AC
171 }
172 }
173
174 // Ok now we have our final goal of testing out the changes that we applied.
175 // If these changes went awry and actually started to cause the crate to
176 // *stop* compiling then we want to back them out and continue to print
177 // warnings to the user.
178 //
179 // If we didn't actually make any changes then we can immediately exec the
180 // new rustc, and otherwise we capture the output to hide it in the scenario
181 // that we have to back it all out.
876a5036 182 if !fixes.files.is_empty() {
fa7a3877
AC
183 let mut cmd = Command::new(&rustc);
184 args.apply(&mut cmd);
185 cmd.arg("--error-format=json");
b02ba377
AC
186 let output = cmd.output().context("failed to spawn rustc")?;
187
188 if output.status.success() {
876a5036
AC
189 for (path, file) in fixes.files.iter() {
190 Message::Fixing {
191 file: path.clone(),
192 fixes: file.fixes_applied,
193 }.post()?;
b02ba377
AC
194 }
195 }
196
197 // If we succeeded then we'll want to commit to the changes we made, if
198 // any. If stderr is empty then there's no need for the final exec at
199 // the end, we just bail out here.
385b54b3 200 if output.status.success() && output.stderr.is_empty() {
b02ba377
AC
201 return Ok(true);
202 }
203
204 // Otherwise if our rustc just failed then that means that we broke the
205 // user's code with our changes. Back out everything and fall through
206 // below to recompile again.
207 if !output.status.success() {
08dc6da0
AC
208 if env::var_os(BROKEN_CODE_ENV).is_none() {
209 for (path, file) in fixes.files.iter() {
210 fs::write(path, &file.original_code)
211 .with_context(|_| format!("failed to write file `{}`", path))?;
212 }
b02ba377
AC
213 }
214 log_failed_fix(&output.stderr)?;
215 }
216 }
217
218 let mut cmd = Command::new(&rustc);
fa7a3877 219 args.apply(&mut cmd);
b02ba377
AC
220 exit_with(cmd.status().context("failed to spawn rustc")?);
221}
222
223#[derive(Default)]
224struct FixedCrate {
876a5036
AC
225 files: HashMap<String, FixedFile>,
226}
227
228struct FixedFile {
229 errors_applying_fixes: Vec<String>,
230 fixes_applied: u32,
231 original_code: String,
b02ba377
AC
232}
233
fa7a3877 234fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
b02ba377
AC
235 -> Result<FixedCrate, Error>
236{
fa7a3877 237 args.verify_not_preparing_for_enabled_edition()?;
fa7a3877 238
b02ba377
AC
239 // First up we want to make sure that each crate is only checked by one
240 // process at a time. If two invocations concurrently check a crate then
241 // it's likely to corrupt it.
242 //
243 // Currently we do this by assigning the name on our lock to the first
244 // argument that looks like a Rust file.
245 let _lock = LockServerClient::lock(&lock_addr.parse()?, filename)?;
246
876a5036
AC
247 // Next up this is a bit suspicious, but we *iteratively* execute rustc and
248 // collect suggestions to feed to rustfix. Once we hit our limit of times to
249 // execute rustc or we appear to be reaching a fixed point we stop running
250 // rustc.
251 //
252 // This is currently done to handle code like:
253 //
254 // ::foo::<::Bar>();
255 //
256 // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
257 // The spans for these two suggestions are overlapping and its difficult in
258 // the compiler to *not* have overlapping spans here. As a result, a naive
259 // implementation would feed the two compiler suggestions for the above fix
260 // into `rustfix`, but one would be rejected because it overlaps with the
261 // other.
262 //
263 // In this case though, both suggestions are valid and can be automatically
264 // applied! To handle this case we execute rustc multiple times, collecting
265 // fixes each time we do so. Along the way we discard any suggestions that
266 // failed to apply, assuming that they can be fixed the next time we run
267 // rustc.
268 //
269 // Naturally we want a few protections in place here though to avoid looping
270 // forever or otherwise losing data. To that end we have a few termination
271 // conditions:
272 //
273 // * Do this whole process a fixed number of times. In theory we probably
274 // need an infinite number of times to apply fixes, but we're not gonna
275 // sit around waiting for that.
276 // * If it looks like a fix genuinely can't be applied we need to bail out.
277 // Detect this when a fix fails to get applied *and* no suggestions
278 // successfully applied to the same file. In that case looks like we
279 // definitely can't make progress, so bail out.
280 let mut fixes = FixedCrate::default();
281 let mut last_fix_counts = HashMap::new();
282 let iterations = env::var("CARGO_FIX_MAX_RETRIES")
283 .ok()
284 .and_then(|n| n.parse().ok())
285 .unwrap_or(4);
286 for _ in 0..iterations {
287 last_fix_counts.clear();
288 for (path, file) in fixes.files.iter_mut() {
289 last_fix_counts.insert(path.clone(), file.fixes_applied);
290 file.errors_applying_fixes.clear(); // we'll generate new errors below
291 }
292 rustfix_and_fix(&mut fixes, rustc, filename, args)?;
293 let mut progress_yet_to_be_made = false;
294 for (path, file) in fixes.files.iter_mut() {
8798bf0d 295 if file.errors_applying_fixes.is_empty() {
876a5036
AC
296 continue
297 }
298 // If anything was successfully fixed *and* there's at least one
299 // error, then assume the error was spurious and we'll try again on
300 // the next iteration.
301 if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
302 progress_yet_to_be_made = true;
303 }
304 }
305 if !progress_yet_to_be_made {
306 break
307 }
308 }
309
310 // Any errors still remaining at this point need to be reported as probably
311 // bugs in Cargo and/or rustfix.
312 for (path, file) in fixes.files.iter_mut() {
313 for error in file.errors_applying_fixes.drain(..) {
314 Message::ReplaceFailed {
315 file: path.clone(),
316 message: error,
317 }.post()?;
318 }
319 }
320
321 Ok(fixes)
322}
323
324/// Execute `rustc` to apply one round of suggestions to the crate in question.
325///
326/// This will fill in the `fixes` map with original code, suggestions applied,
327/// and any errors encountered while fixing files.
328fn rustfix_and_fix(fixes: &mut FixedCrate, rustc: &Path, filename: &Path, args: &FixArgs)
329 -> Result<(), Error>
330{
331 // If not empty, filter by these lints
332 //
333 // TODO: Implement a way to specify this
334 let only = HashSet::new();
335
336 let mut cmd = Command::new(rustc);
fa7a3877
AC
337 cmd.arg("--error-format=json");
338 args.apply(&mut cmd);
b02ba377
AC
339 let output = cmd.output()
340 .with_context(|_| format!("failed to execute `{}`", rustc.display()))?;
341
342 // If rustc didn't succeed for whatever reasons then we're very likely to be
343 // looking at otherwise broken code. Let's not make things accidentally
344 // worse by applying fixes where a bug could cause *more* broken code.
345 // Instead, punt upwards which will reexec rustc over the original code,
346 // displaying pretty versions of the diagnostics we just read out.
347 if !output.status.success() && env::var_os(BROKEN_CODE_ENV).is_none() {
348 debug!(
349 "rustfixing `{:?}` failed, rustc exited with {:?}",
350 filename,
351 output.status.code()
352 );
6dd73398 353 return Ok(());
b02ba377
AC
354 }
355
356 let fix_mode = env::var_os("__CARGO_FIX_YOLO")
357 .map(|_| rustfix::Filter::Everything)
358 .unwrap_or(rustfix::Filter::MachineApplicableOnly);
359
360 // Sift through the output of the compiler to look for JSON messages
361 // indicating fixes that we can apply.
362 let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as utf-8")?;
363
364 let suggestions = stderr.lines()
365 .filter(|x| !x.is_empty())
366 .inspect(|y| trace!("line: {}", y))
367
368 // Parse each line of stderr ignoring errors as they may not all be json
369 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
370
371 // From each diagnostic try to extract suggestions from rustc
372 .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
373
374 // Collect suggestions by file so we can apply them one at a time later.
375 let mut file_map = HashMap::new();
376 let mut num_suggestion = 0;
377 for suggestion in suggestions {
378 trace!("suggestion");
379 // Make sure we've got a file associated with this suggestion and all
380 // snippets point to the same location. Right now it's not clear what
381 // we would do with multiple locations.
382 let (file_name, range) = match suggestion.snippets.get(0) {
383 Some(s) => (s.file_name.clone(), s.line_range),
384 None => {
385 trace!("rejecting as it has no snippets {:?}", suggestion);
386 continue;
387 }
388 };
389 if !suggestion
390 .snippets
391 .iter()
392 .all(|s| s.file_name == file_name && s.line_range == range)
393 {
394 trace!("rejecting as it spans multiple files {:?}", suggestion);
395 continue;
396 }
397
398 file_map
399 .entry(file_name)
400 .or_insert_with(Vec::new)
401 .push(suggestion);
402 num_suggestion += 1;
403 }
404
405 debug!(
406 "collected {} suggestions for `{}`",
fa7a3877
AC
407 num_suggestion,
408 filename.display(),
b02ba377
AC
409 );
410
b02ba377
AC
411 for (file, suggestions) in file_map {
412 // Attempt to read the source code for this file. If this fails then
413 // that'd be pretty surprising, so log a message and otherwise keep
414 // going.
415 let code = match paths::read(file.as_ref()) {
416 Ok(s) => s,
417 Err(e) => {
418 warn!("failed to read `{}`: {}", file, e);
419 continue;
420 }
421 };
422 let num_suggestions = suggestions.len();
423 debug!("applying {} fixes to {}", num_suggestions, file);
424
876a5036
AC
425 // If this file doesn't already exist then we just read the original
426 // code, so save it. If the file already exists then the original code
427 // doesn't need to be updated as we've just read an interim state with
428 // some fixes but perhaps not all.
429 let fixed_file = fixes.files.entry(file.clone())
430 .or_insert_with(|| {
431 FixedFile {
432 errors_applying_fixes: Vec::new(),
433 fixes_applied: 0,
434 original_code: code.clone(),
435 }
436 });
437 let mut fixed = CodeFix::new(&code);
438
439 // As mentioned above in `rustfix_crate`, we don't immediately warn
440 // about suggestions that fail to apply here, and instead we save them
441 // off for later processing.
442 for suggestion in suggestions.iter().rev() {
443 match fixed.apply(suggestion) {
444 Ok(()) => fixed_file.fixes_applied += 1,
445 Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
b02ba377
AC
446 }
447 }
876a5036
AC
448 let new_code = fixed.finish()?;
449 fs::write(&file, new_code)
450 .with_context(|_| format!("failed to write file `{}`", file))?;
b02ba377
AC
451 }
452
876a5036 453 Ok(())
b02ba377
AC
454}
455
456fn exit_with(status: ExitStatus) -> ! {
457 #[cfg(unix)]
458 {
459 use std::os::unix::prelude::*;
460 if let Some(signal) = status.signal() {
461 eprintln!("child failed with signal `{}`", signal);
462 process::exit(2);
463 }
464 }
465 process::exit(status.code().unwrap_or(3));
466}
467
468fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {
469 let stderr = str::from_utf8(stderr).context("failed to parse rustc stderr as utf-8")?;
470
471 let diagnostics = stderr
472 .lines()
473 .filter(|x| !x.is_empty())
474 .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok());
475 let mut files = BTreeSet::new();
476 for diagnostic in diagnostics {
477 for span in diagnostic.spans.into_iter() {
478 files.insert(span.file_name);
479 }
480 }
481 let mut krate = None;
482 let mut prev_dash_dash_krate_name = false;
483 for arg in env::args() {
484 if prev_dash_dash_krate_name {
485 krate = Some(arg.clone());
486 }
487
488 if arg == "--crate-name" {
489 prev_dash_dash_krate_name = true;
490 } else {
491 prev_dash_dash_krate_name = false;
492 }
493 }
494
495 let files = files.into_iter().collect();
496 Message::FixFailed { files, krate }.post()?;
497
498 Ok(())
499}
fa7a3877
AC
500
501#[derive(Default)]
502struct FixArgs {
503 file: Option<PathBuf>,
b2b120e9 504 prepare_for_edition: PrepareFor,
80f9d318 505 idioms: bool,
fa7a3877
AC
506 enabled_edition: Option<String>,
507 other: Vec<OsString>,
4f784a10 508 primary_package: bool,
fa7a3877
AC
509}
510
b2b120e9
AC
511enum PrepareFor {
512 Next,
513 Edition(String),
514 None,
515}
516
517impl Default for PrepareFor {
518 fn default() -> PrepareFor {
519 PrepareFor::None
520 }
521}
522
fa7a3877
AC
523impl FixArgs {
524 fn get() -> FixArgs {
525 let mut ret = FixArgs::default();
526 for arg in env::args_os().skip(1) {
527 let path = PathBuf::from(arg);
528 if path.extension().and_then(|s| s.to_str()) == Some("rs") {
529 if path.exists() {
530 ret.file = Some(path);
531 continue
532 }
533 }
534 if let Some(s) = path.to_str() {
535 let prefix = "--edition=";
536 if s.starts_with(prefix) {
537 ret.enabled_edition = Some(s[prefix.len()..].to_string());
538 continue
539 }
540 }
541 ret.other.push(path.into());
542 }
b2b120e9
AC
543 if let Ok(s) = env::var(PREPARE_FOR_ENV) {
544 ret.prepare_for_edition = PrepareFor::Edition(s);
545 } else if env::var(EDITION_ENV).is_ok() {
546 ret.prepare_for_edition = PrepareFor::Next;
fa7a3877 547 }
80f9d318 548 ret.idioms = env::var(IDIOMS_ENV).is_ok();
4f784a10 549 ret.primary_package = env::var("CARGO_PRIMARY_PACKAGE").is_ok();
8798bf0d 550 ret
fa7a3877
AC
551 }
552
553 fn apply(&self, cmd: &mut Command) {
554 if let Some(path) = &self.file {
555 cmd.arg(path);
556 }
557 cmd.args(&self.other)
558 .arg("--cap-lints=warn");
559 if let Some(edition) = &self.enabled_edition {
560 cmd.arg("--edition").arg(edition);
4f784a10 561 if self.idioms && self.primary_package {
8798bf0d 562 if edition == "2018" { cmd.arg("-Wrust-2018-idioms"); }
80f9d318 563 }
fa7a3877 564 }
4f784a10
AC
565 if self.primary_package {
566 if let Some(edition) = self.prepare_for_edition_resolve() {
567 cmd.arg("-W").arg(format!("rust-{}-compatibility", edition));
568 }
fa7a3877
AC
569 }
570 }
571
572 /// Verify that we're not both preparing for an enabled edition and enabling
573 /// the edition.
574 ///
575 /// This indicates that `cargo fix --prepare-for` is being executed out of
576 /// order with enabling the edition itself, meaning that we wouldn't
577 /// actually be able to fix anything! If it looks like this is happening
578 /// then yield an error to the user, indicating that this is happening.
579 fn verify_not_preparing_for_enabled_edition(&self) -> CargoResult<()> {
35f745ac
DW
580 let edition = match self.prepare_for_edition_resolve() {
581 Some(s) => s,
582 None => return Ok(()),
fa7a3877
AC
583 };
584 let enabled = match &self.enabled_edition {
585 Some(s) => s,
586 None => return Ok(()),
587 };
588 if edition != enabled {
589 return Ok(())
590 }
591 let path = match &self.file {
592 Some(s) => s,
593 None => return Ok(()),
594 };
595
596 Message::EditionAlreadyEnabled {
597 file: path.display().to_string(),
598 edition: edition.to_string(),
599 }.post()?;
600
601 process::exit(1);
602 }
603
35f745ac
DW
604 fn prepare_for_edition_resolve(&self) -> Option<&str> {
605 match &self.prepare_for_edition {
606 PrepareFor::Edition(s) => Some(s),
607 PrepareFor::Next => Some(self.next_edition()),
608 PrepareFor::None => None,
609 }
610 }
611
b2b120e9
AC
612 fn next_edition(&self) -> &str {
613 match self.enabled_edition.as_ref().map(|s| &**s) {
614 // 2015 -> 2018,
615 None | Some("2015") => "2018",
616
617 // This'll probably be wrong in 2020, but that's future Cargo's
618 // problem. Eventually though we'll just add more editions here as
619 // necessary.
620 _ => "2018",
621 }
622 }
fa7a3877 623}