Skip to main content

cargo/ops/
fix.rs

1//! High-level overview of how `fix` works:
2//!
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.
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
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
21//! fix-proxy-mode.
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
41use 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    // Spin up our lock server, which our subprocesses will use to synchronize fixes.
82    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    // primary crates are compiled using a cargo subprocess to do extra work of applying fixes and
122    // repeating build until there are no more changes to be applied
123    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    // Ok now we have our final goal of testing out the changes that we applied.
223    // If these changes went awry and actually started to cause the crate to
224    // *stop* compiling then we want to back them out and continue to print
225    // warnings to the user.
226    //
227    // If we didn't actually make any changes then we can immediately execute the
228    // new rustc, and otherwise we capture the output to hide it in the scenario
229    // that we have to back it all out.
230    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 we succeeded then we'll want to commit to the changes we made, if
247        // any. If stderr is empty then there's no need for the final exec at
248        // the end, we just bail out here.
249        if output.status.success() && output.stderr.is_empty() {
250            return Ok(true);
251        }
252
253        // Otherwise, if our rustc just failed, then that means that we broke the
254        // user's code with our changes. Back out everything and fall through
255        // below to recompile again.
256        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    // This final fall-through handles multiple cases;
268    // - If the fix failed, show the original warnings and suggestions.
269    // - If `--broken-code`, show the error messages.
270    // - If the fix succeeded, show any remaining warnings.
271    let mut cmd = rustc.build_command();
272    args.apply(&mut cmd);
273    for arg in args.format_args {
274        // Add any json/error format arguments that Cargo wants. This allows
275        // things like colored output to work correctly.
276        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    // First up, we want to make sure that each crate is only checked by one
301    // process at a time. If two invocations concurrently check a crate then
302    // it's likely to corrupt it.
303    //
304    // We currently do this by assigning the name on our lock to the manifest
305    // directory.
306    let dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is missing?");
307    let _lock = LockServerClient::lock(&lock_addr.parse()?, dir)?;
308
309    // Next up, this is a bit suspicious, but we *iteratively* execute rustc and
310    // collect suggestions to feed to rustfix. Once we hit our limit of times to
311    // execute rustc or we appear to be reaching a fixed point we stop running
312    // rustc.
313    //
314    // This is currently done to handle code like:
315    //
316    //      ::foo::<::Bar>();
317    //
318    // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
319    // The spans for these two suggestions are overlapping and its difficult in
320    // the compiler to **not** have overlapping spans here. As a result, a naive
321    // implementation would feed the two compiler suggestions for the above fix
322    // into `rustfix`, but one would be rejected because it overlaps with the
323    // other.
324    //
325    // In this case though, both suggestions are valid and can be automatically
326    // applied! To handle this case we execute rustc multiple times, collecting
327    // fixes each time we do so. Along the way we discard any suggestions that
328    // failed to apply, assuming that they can be fixed the next time we run
329    // rustc.
330    //
331    // Naturally, we want a few protections in place here though to avoid looping
332    // forever or otherwise losing data. To that end we have a few termination
333    // conditions:
334    //
335    // * Do this whole process a fixed number of times. In theory we probably
336    //   need an infinite number of times to apply fixes, but we're not gonna
337    //   sit around waiting for that.
338    // * If it looks like a fix genuinely can't be applied we need to bail out.
339    //   Detect this when a fix fails to get applied *and* no suggestions
340    //   successfully applied to the same file. In that case looks like we
341    //   definitely can't make progress, so bail out.
342    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            // We'll generate new errors below.
353            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 anything was successfully fixed *and* there's at least one
362            // error, then assume the error was spurious and we'll try again on
363            // the next iteration.
364            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    // Any errors still remaining at this point need to be reported as probably
374    // bugs in Cargo and/or rustfix.
375    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
388/// Executes `rustc` to apply one round of suggestions to the crate in question.
389///
390/// This will fill in the `fixes` map with original code, suggestions applied,
391/// and any errors encountered while fixing files.
392fn rustfix_and_fix(
393    fixes: &mut FixedCrate,
394    rustc: &ProcessBuilder,
395    filename: &Path,
396    args: &FixArgs,
397) -> Result<(), Error> {
398    // If not empty, filter by these lints.
399    // TODO: implement a way to specify this.
400    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 rustc didn't succeed for whatever reasons then we're very likely to be
413    // looking at otherwise broken code. Let's not make things accidentally
414    // worse by applying fixes where a bug could cause *more* broken code.
415    // Instead, punt upwards which will reexec rustc over the original code,
416    // displaying pretty versions of the diagnostics we just read out.
417    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    // Sift through the output of the compiler to look for JSON messages.
431    // indicating fixes that we can apply.
432    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        // Parse each line of stderr, ignoring errors, as they may not all be JSON.
439        .filter_map(|line| serde_json::from_str::<Diagnostic>(line).ok())
440        // From each diagnostic, try to extract suggestions from rustc.
441        .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode));
442
443    // Collect suggestions by file so we can apply them one at a time later.
444    let mut file_map = HashMap::new();
445    let mut num_suggestion = 0;
446    for suggestion in suggestions {
447        trace!("suggestion");
448        // Make sure we've got a file associated with this suggestion and all
449        // snippets point to the same file. Right now it's not clear what
450        // we would do with multiple files.
451        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        // Attempt to read the source code for this file. If this fails then
484        // that'd be pretty surprising, so log a message and otherwise keep
485        // going.
486        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        // If this file doesn't already exist then we just read the original
497        // code, so save it. If the file already exists then the original code
498        // doesn't need to be updated as we've just read an interim state with
499        // some fixes but perhaps not all.
500        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        // As mentioned above in `rustfix_crate`, we don't immediately warn
511        // about suggestions that fail to apply here, and instead we save them
512        // off for later processing.
513        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                    // Cargo may add error-format in some cases, but `cargo
621                    // fix` wants to add its own.
622                    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    /// Verifies that we're not both preparing for an enabled edition and enabling
657    /// the edition.
658    ///
659    /// This indicates that `cargo fix --prepare-for` is being executed out of
660    /// order with enabling the edition itself, meaning that we wouldn't
661    /// actually be able to fix anything! If it looks like this is happening
662    /// then yield an error to the user, indicating that this is happening.
663    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            // 2015 -> 2018,
700            None | Some("2015") => "2018",
701
702            // This'll probably be wrong in 2020, but that's future Cargo's
703            // problem. Eventually though we'll just add more editions here as
704            // necessary.
705            _ => "2018",
706        }
707    }
708}