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