diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index fbaa9dd0d9b..7b130f443af 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -1524,6 +1524,7 @@ fn substitute_macros(input: &str) -> String { ("[REPLACING]", " Replacing"), ("[UNPACKING]", " Unpacking"), ("[SUMMARY]", " Summary"), + ("[FIXED]", " Fixed"), ("[FIXING]", " Fixing"), ("[EXE]", env::consts::EXE_SUFFIX), ("[IGNORED]", " Ignored"), @@ -1534,6 +1535,7 @@ fn substitute_macros(input: &str) -> String { ("[LOGOUT]", " Logout"), ("[YANK]", " Yank"), ("[OWNER]", " Owner"), + ("[MIGRATING]", " Migrating"), ]; let mut result = input.to_owned(); for &(pat, subst) in ¯os { diff --git a/src/bin/cargo/commands/fix.rs b/src/bin/cargo/commands/fix.rs index bbcf6f7d4e5..49fe40cd8ea 100644 --- a/src/bin/cargo/commands/fix.rs +++ b/src/bin/cargo/commands/fix.rs @@ -41,17 +41,6 @@ pub fn cli() -> App { .long("edition") .help("Fix in preparation for the next edition"), ) - .arg( - // This is a deprecated argument, we'll want to phase it out - // eventually. - Arg::with_name("prepare-for") - .long("prepare-for") - .help("Fix warnings in preparation of an edition upgrade") - .takes_value(true) - .possible_values(&["2018"]) - .conflicts_with("edition") - .hidden(true), - ) .arg( Arg::with_name("idioms") .long("edition-idioms") @@ -111,7 +100,6 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult { &ws, &mut ops::FixOptions { edition: args.is_present("edition"), - prepare_for: args.value_of("prepare-for"), idioms: args.is_present("idioms"), compile_opts: opts, allow_dirty: args.is_present("allow-dirty"), diff --git a/src/cargo/core/compiler/compilation.rs b/src/cargo/core/compiler/compilation.rs index 40389e7ae7b..626f4f56388 100644 --- a/src/cargo/core/compiler/compilation.rs +++ b/src/cargo/core/compiler/compilation.rs @@ -8,7 +8,7 @@ use semver::Version; use super::BuildContext; use crate::core::compiler::{CompileKind, Metadata, Unit}; -use crate::core::{Edition, Package}; +use crate::core::Package; use crate::util::{self, config, join_paths, process, CargoResult, Config, ProcessBuilder}; /// Structure with enough information to run `rustdoc --test`. @@ -187,9 +187,7 @@ impl<'cfg> Compilation<'cfg> { let rustdoc = process(&*self.config.rustdoc()?); let cmd = fill_rustc_tool_env(rustdoc, unit); let mut p = self.fill_env(cmd, &unit.pkg, script_meta, unit.kind, true)?; - if unit.target.edition() != Edition::Edition2015 { - p.arg(format!("--edition={}", unit.target.edition())); - } + unit.target.edition().cmd_edition_arg(&mut p); for crate_type in unit.target.rustc_crate_types() { p.arg("--crate-type").arg(crate_type.as_str()); diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 3c47cef8adf..fa6c6b06432 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -52,7 +52,7 @@ pub use crate::core::compiler::unit::{Unit, UnitInterner}; use crate::core::features::nightly_features_allowed; use crate::core::manifest::TargetSourcePath; use crate::core::profiles::{PanicStrategy, Profile, Strip}; -use crate::core::{Edition, Feature, PackageId, Target}; +use crate::core::{Feature, PackageId, Target}; use crate::util::errors::{self, CargoResult, CargoResultExt, ProcessError, VerboseError}; use crate::util::interning::InternedString; use crate::util::machine_message::Message; @@ -747,9 +747,7 @@ fn build_base_args( cmd.arg("--crate-name").arg(&unit.target.crate_name()); let edition = unit.target.edition(); - if edition != Edition::Edition2015 { - cmd.arg(format!("--edition={}", edition)); - } + edition.cmd_edition_arg(cmd); add_path_args(bcx.ws, unit, cmd); add_error_format_and_color(cx, cmd, cx.rmeta_required(unit)); diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 9342c12dcc1..377cf25fb58 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -101,7 +101,7 @@ use anyhow::{bail, Error}; use serde::{Deserialize, Serialize}; use crate::util::errors::CargoResult; -use crate::util::indented_lines; +use crate::util::{indented_lines, ProcessBuilder}; pub const SEE_CHANNELS: &str = "See https://doc.rust-lang.org/book/appendix-07-nightly-rust.html for more information \ @@ -118,15 +118,117 @@ pub enum Edition { Edition2021, } +// Adding a new edition: +// - Add the next edition to the enum. +// - Update every match expression that now fails to compile. +// - Update the `FromStr` impl. +// - Update CLI_VALUES to include the new edition. +// - Set LATEST_UNSTABLE to Some with the new edition. +// - Add an unstable feature to the features! macro below for the new edition. +// - Gate on that new feature in TomlManifest::to_real_manifest. +// - Update the shell completion files. +// - Update any failing tests (hopefully there are very few). +// +// Stabilization instructions: +// - Set LATEST_UNSTABLE to None. +// - Set LATEST_STABLE to the new version. +// - Update `is_stable` to `true`. +// - Set the editionNNNN feature to stable in the features macro below. +// - Update the man page for the --edition flag. impl Edition { + /// The latest edition that is unstable. + /// + /// This is `None` if there is no next unstable edition. + pub const LATEST_UNSTABLE: Option = Some(Edition::Edition2021); + /// The latest stable edition. + pub const LATEST_STABLE: Edition = Edition::Edition2018; + /// Possible values allowed for the `--edition` CLI flag. + /// + /// This requires a static value due to the way clap works, otherwise I + /// would have built this dynamically. + pub const CLI_VALUES: &'static [&'static str] = &["2015", "2018", "2021"]; + + /// Returns the first version that a particular edition was released on + /// stable. pub(crate) fn first_version(&self) -> Option { use Edition::*; match self { Edition2015 => None, Edition2018 => Some(semver::Version::new(1, 31, 0)), + // FIXME: This will likely be 1.56, update when that seems more likely. Edition2021 => Some(semver::Version::new(1, 62, 0)), } } + + /// Returns `true` if this edition is stable in this release. + pub fn is_stable(&self) -> bool { + use Edition::*; + match self { + Edition2015 => true, + Edition2018 => true, + Edition2021 => false, + } + } + + /// Returns the previous edition from this edition. + /// + /// Returns `None` for 2015. + pub fn previous(&self) -> Option { + use Edition::*; + match self { + Edition2015 => None, + Edition2018 => Some(Edition2015), + Edition2021 => Some(Edition2018), + } + } + + /// Returns the next edition from this edition, returning the last edition + /// if this is already the last one. + pub fn saturating_next(&self) -> Edition { + use Edition::*; + match self { + Edition2015 => Edition2018, + Edition2018 => Edition2021, + Edition2021 => Edition2021, + } + } + + /// Updates the given [`ProcessBuilder`] to include the appropriate flags + /// for setting the edition. + pub(crate) fn cmd_edition_arg(&self, cmd: &mut ProcessBuilder) { + if *self != Edition::Edition2015 { + cmd.arg(format!("--edition={}", self)); + } + if !self.is_stable() { + cmd.arg("-Z").arg("unstable-options"); + } + } + + /// Whether or not this edition supports the `rust_*_compatibility` lint. + /// + /// Ideally this would not be necessary, but currently 2021 does not have + /// any lints, and thus `rustc` doesn't recognize it. Perhaps `rustc` + /// could create an empty group instead? + pub(crate) fn supports_compat_lint(&self) -> bool { + use Edition::*; + match self { + Edition2015 => false, + Edition2018 => true, + Edition2021 => false, + } + } + + /// Whether or not this edition supports the `rust_*_idioms` lint. + /// + /// Ideally this would not be necessary... + pub(crate) fn supports_idiom_lint(&self) -> bool { + use Edition::*; + match self { + Edition2015 => false, + Edition2018 => true, + Edition2021 => false, + } + } } impl fmt::Display for Edition { @@ -282,6 +384,9 @@ features! { // Specifying a minimal 'rust-version' attribute for crates (unstable, rust_version, "", "reference/unstable.html#rust-version"), + + // Support for 2021 edition. + (unstable, edition2021, "", "reference/unstable.html#edition-2021"), } const PUBLISH_LOCKFILE_REMOVED: &str = "The publish-lockfile key in Cargo.toml \ diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 864cef730ed..f72f0915c9f 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -13,7 +13,7 @@ use crate::core::features::Features; use crate::core::registry::PackageRegistry; use crate::core::resolver::features::RequestedFeatures; use crate::core::resolver::ResolveBehavior; -use crate::core::{Dependency, PackageId, PackageIdSpec}; +use crate::core::{Dependency, Edition, PackageId, PackageIdSpec}; use crate::core::{EitherManifest, Package, SourceId, VirtualManifest}; use crate::ops; use crate::sources::PathSource; @@ -88,7 +88,7 @@ pub struct Workspace<'cfg> { ignore_lock: bool, /// The resolver behavior specified with the `resolver` field. - resolve_behavior: Option, + resolve_behavior: ResolveBehavior, /// Workspace-level custom metadata custom_metadata: Option, @@ -164,10 +164,7 @@ impl<'cfg> Workspace<'cfg> { .load_workspace_config()? .and_then(|cfg| cfg.custom_metadata); ws.find_members()?; - ws.resolve_behavior = match ws.root_maybe() { - MaybePackage::Package(p) => p.manifest().resolve_behavior(), - MaybePackage::Virtual(vm) => vm.resolve_behavior(), - }; + ws.set_resolve_behavior(); ws.validate()?; Ok(ws) } @@ -189,7 +186,7 @@ impl<'cfg> Workspace<'cfg> { require_optional_deps: true, loaded_packages: RefCell::new(HashMap::new()), ignore_lock: false, - resolve_behavior: None, + resolve_behavior: ResolveBehavior::V1, custom_metadata: None, } } @@ -203,11 +200,11 @@ impl<'cfg> Workspace<'cfg> { let mut ws = Workspace::new_default(current_manifest, config); ws.root_manifest = Some(root_path.join("Cargo.toml")); ws.target_dir = config.target_dir()?; - ws.resolve_behavior = manifest.resolve_behavior(); ws.packages .packages .insert(root_path, MaybePackage::Virtual(manifest)); ws.find_members()?; + ws.set_resolve_behavior(); // TODO: validation does not work because it walks up the directory // tree looking for the root which is a fake file that doesn't exist. Ok(ws) @@ -231,7 +228,6 @@ impl<'cfg> Workspace<'cfg> { let mut ws = Workspace::new_default(package.manifest_path().to_path_buf(), config); ws.is_ephemeral = true; ws.require_optional_deps = require_optional_deps; - ws.resolve_behavior = package.manifest().resolve_behavior(); let key = ws.current_manifest.parent().unwrap(); let id = package.package_id(); let package = MaybePackage::Package(package); @@ -244,9 +240,28 @@ impl<'cfg> Workspace<'cfg> { ws.members.push(ws.current_manifest.clone()); ws.member_ids.insert(id); ws.default_members.push(ws.current_manifest.clone()); + ws.set_resolve_behavior(); Ok(ws) } + fn set_resolve_behavior(&mut self) { + // - If resolver is specified in the workspace definition, use that. + // - If the root package specifies the resolver, use that. + // - If the root package specifies edition 2021, use v2. + // - Otherwise, use the default v1. + self.resolve_behavior = match self.root_maybe() { + MaybePackage::Package(p) => p.manifest().resolve_behavior().or_else(|| { + if p.manifest().edition() >= Edition::Edition2021 { + Some(ResolveBehavior::V2) + } else { + None + } + }), + MaybePackage::Virtual(vm) => vm.resolve_behavior(), + } + .unwrap_or(ResolveBehavior::V1); + } + /// Returns the current package of this workspace. /// /// Note that this can return an error if it the current manifest is @@ -634,7 +649,7 @@ impl<'cfg> Workspace<'cfg> { } pub fn resolve_behavior(&self) -> ResolveBehavior { - self.resolve_behavior.unwrap_or(ResolveBehavior::V1) + self.resolve_behavior } /// Returns `true` if this workspace uses the new CLI features behavior. @@ -843,11 +858,11 @@ impl<'cfg> Workspace<'cfg> { if !manifest.patch().is_empty() { emit_warning("patch")?; } - if manifest.resolve_behavior().is_some() - && manifest.resolve_behavior() != self.resolve_behavior - { - // Only warn if they don't match. - emit_warning("resolver")?; + if let Some(behavior) = manifest.resolve_behavior() { + if behavior != self.resolve_behavior { + // Only warn if they don't match. + emit_warning("resolver")?; + } } } } diff --git a/src/cargo/ops/cargo_new.rs b/src/cargo/ops/cargo_new.rs index 4ade5d3dd90..06b88084c5e 100644 --- a/src/cargo/ops/cargo_new.rs +++ b/src/cargo/ops/cargo_new.rs @@ -1,4 +1,4 @@ -use crate::core::{Shell, Workspace}; +use crate::core::{Edition, Shell, Workspace}; use crate::util::errors::{CargoResult, CargoResultExt}; use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo}; use crate::util::{paths, restricted_names, Config}; @@ -743,7 +743,7 @@ edition = {} }, match opts.edition { Some(edition) => toml::Value::String(edition.to_string()), - None => toml::Value::String("2018".to_string()), + None => toml::Value::String(Edition::LATEST_STABLE.to_string()), }, match opts.registry { Some(registry) => format!( diff --git a/src/cargo/ops/fix.rs b/src/cargo/ops/fix.rs index 1ebcef67bbc..c1d1f20d2ad 100644 --- a/src/cargo/ops/fix.rs +++ b/src/cargo/ops/fix.rs @@ -45,12 +45,12 @@ use std::path::{Path, PathBuf}; use std::process::{self, Command, ExitStatus}; use std::str; -use anyhow::{Context, Error}; +use anyhow::{bail, Context, Error}; use log::{debug, trace, warn}; use rustfix::diagnostics::Diagnostic; use rustfix::{self, CodeFix}; -use crate::core::{Edition, Workspace}; +use crate::core::{nightly_features_allowed, Edition, Workspace}; use crate::ops::{self, CompileOptions}; use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer}; use crate::util::errors::CargoResult; @@ -59,13 +59,11 @@ use crate::util::{existing_vcs_repo, LockServer, LockServerClient}; const FIX_ENV: &str = "__CARGO_FIX_PLZ"; const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE"; -const PREPARE_FOR_ENV: &str = "__CARGO_FIX_PREPARE_FOR"; const EDITION_ENV: &str = "__CARGO_FIX_EDITION"; const IDIOMS_ENV: &str = "__CARGO_FIX_IDIOMS"; -pub struct FixOptions<'a> { +pub struct FixOptions { pub edition: bool, - pub prepare_for: Option<&'a str>, pub idioms: bool, pub compile_opts: CompileOptions, pub allow_dirty: bool, @@ -74,7 +72,7 @@ pub struct FixOptions<'a> { pub broken_code: bool, } -pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions<'_>) -> CargoResult<()> { +pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> { check_version_control(ws.config(), opts)?; // Spin up our lock server, which our subprocesses will use to synchronize fixes. @@ -91,8 +89,6 @@ pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions<'_>) -> CargoResult<()> { if opts.edition { wrapper.env(EDITION_ENV, "1"); - } else if let Some(edition) = opts.prepare_for { - wrapper.env(PREPARE_FOR_ENV, edition); } if opts.idioms { wrapper.env(IDIOMS_ENV, "1"); @@ -125,12 +121,12 @@ pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions<'_>) -> CargoResult<()> { Ok(()) } -fn check_version_control(config: &Config, opts: &FixOptions<'_>) -> CargoResult<()> { +fn check_version_control(config: &Config, opts: &FixOptions) -> CargoResult<()> { if opts.allow_no_vcs { return Ok(()); } if !existing_vcs_repo(config.cwd(), config.cwd()) { - anyhow::bail!( + bail!( "no VCS found for this package and `cargo fix` can potentially \ perform destructive changes; if you'd like to suppress this \ error pass `--allow-no-vcs`" @@ -185,7 +181,7 @@ fn check_version_control(config: &Config, opts: &FixOptions<'_>) -> CargoResult< files_list.push_str(" (staged)\n"); } - anyhow::bail!( + bail!( "the working directory of this package has uncommitted changes, and \ `cargo fix` can potentially perform destructive changes; if you'd \ like to suppress this error pass `--allow-dirty`, `--allow-staged`, \ @@ -197,6 +193,14 @@ fn check_version_control(config: &Config, opts: &FixOptions<'_>) -> CargoResult< ); } +/// Entry point for `cargo` running as a proxy for `rustc`. +/// +/// This is called every time `cargo` is run to check if it is in proxy mode. +/// +/// Returns `false` if `fix` is not being run (not in proxy mode). Returns +/// `true` if in `fix` proxy mode, and the fix was complete without any +/// warnings or errors. If there are warnings or errors, this does not return, +/// and the process exits with the corresponding `rustc` exit code. pub fn fix_maybe_exec_rustc() -> CargoResult { let lock_addr = match env::var(FIX_ENV) { Ok(s) => s, @@ -206,17 +210,13 @@ pub fn fix_maybe_exec_rustc() -> CargoResult { let args = FixArgs::get()?; trace!("cargo-fix as rustc got file {:?}", args.file); - let rustc = args.rustc.as_ref().expect("fix wrapper rustc was not set"); let workspace_rustc = std::env::var("RUSTC_WORKSPACE_WRAPPER") .map(PathBuf::from) .ok(); - let rustc = util::process(rustc).wrapped(workspace_rustc.as_ref()); + let rustc = util::process(&args.rustc).wrapped(workspace_rustc.as_ref()); - let mut fixes = FixedCrate::default(); - if let Some(path) = &args.file { - trace!("start rustfixing {:?}", path); - fixes = rustfix_crate(&lock_addr, &rustc, path, &args)?; - } + trace!("start rustfixing {:?}", args.file); + let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args)?; // Ok now we have our final goal of testing out the changes that we applied. // If these changes went awry and actually started to cause the crate to @@ -234,7 +234,7 @@ pub fn fix_maybe_exec_rustc() -> CargoResult { if output.status.success() { for (path, file) in fixes.files.iter() { - Message::Fixing { + Message::Fixed { file: path.clone(), fixes: file.fixes_applied, } @@ -287,13 +287,17 @@ struct FixedFile { original_code: String, } +/// Attempts to apply fixes to a single crate. +/// +/// This runs `rustc` (possibly multiple times) to gather suggestions from the +/// compiler and applies them to the files on disk. fn rustfix_crate( lock_addr: &str, rustc: &ProcessBuilder, filename: &Path, args: &FixArgs, ) -> Result { - args.verify_not_preparing_for_enabled_edition()?; + args.check_edition_and_send_status()?; // First up, we want to make sure that each crate is only checked by one // process at a time. If two invocations concurrently check a crate then @@ -578,129 +582,146 @@ fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> { Ok(()) } -#[derive(Default)] +/// Various command-line options and settings used when `cargo` is running as +/// a proxy for `rustc` during the fix operation. struct FixArgs { - file: Option, - prepare_for_edition: PrepareFor, + /// This is the `.rs` file that is being fixed. + file: PathBuf, + /// If `--edition` is used to migrate to the next edition, this is the + /// edition we are migrating towards. + prepare_for_edition: Option, + /// `true` if `--edition-idioms` is enabled. idioms: bool, + /// The current edition. + /// + /// `None` if on 2015. enabled_edition: Option, + /// Other command-line arguments not reflected by other fields in + /// `FixArgs`. other: Vec, - rustc: Option, + /// Path to the `rustc` executable. + rustc: PathBuf, + /// Console output flags (`--error-format`, `--json`, etc.). + /// + /// The normal fix procedure always uses `--json`, so it overrides what + /// Cargo normally passes when applying fixes. When displaying warnings or + /// errors, it will use these flags. format_args: Vec, } -enum PrepareFor { - Next, - Edition(Edition), - None, -} - -impl Default for PrepareFor { - fn default() -> PrepareFor { - PrepareFor::None - } -} - impl FixArgs { fn get() -> Result { - let mut ret = FixArgs::default(); - - ret.rustc = env::args_os().nth(1).map(PathBuf::from); + let rustc = env::args_os() + .nth(1) + .map(PathBuf::from) + .ok_or_else(|| anyhow::anyhow!("expected rustc as first argument"))?; + let mut file = None; + let mut enabled_edition = None; + let mut other = Vec::new(); + let mut format_args = Vec::new(); for arg in env::args_os().skip(2) { let path = PathBuf::from(arg); if path.extension().and_then(|s| s.to_str()) == Some("rs") && path.exists() { - ret.file = Some(path); + file = Some(path); continue; } if let Some(s) = path.to_str() { if let Some(edition) = s.strip_prefix("--edition=") { - ret.enabled_edition = Some(edition.parse()?); + enabled_edition = Some(edition.parse()?); continue; } if s.starts_with("--error-format=") || s.starts_with("--json=") { // Cargo may add error-format in some cases, but `cargo // fix` wants to add its own. - ret.format_args.push(s.to_string()); + format_args.push(s.to_string()); continue; } } - ret.other.push(path.into()); - } - if let Ok(s) = env::var(PREPARE_FOR_ENV) { - ret.prepare_for_edition = PrepareFor::Edition(s.parse()?); - } else if env::var(EDITION_ENV).is_ok() { - ret.prepare_for_edition = PrepareFor::Next; + other.push(path.into()); } - - ret.idioms = env::var(IDIOMS_ENV).is_ok(); - Ok(ret) + let file = file.ok_or_else(|| anyhow::anyhow!("could not find .rs file in rustc args"))?; + let idioms = env::var(IDIOMS_ENV).is_ok(); + + let prepare_for_edition = env::var(EDITION_ENV).ok().map(|_| { + enabled_edition + .unwrap_or(Edition::Edition2015) + .saturating_next() + }); + + Ok(FixArgs { + file, + prepare_for_edition, + idioms, + enabled_edition, + other, + rustc, + format_args, + }) } fn apply(&self, cmd: &mut Command) { - if let Some(path) = &self.file { - cmd.arg(path); - } - + cmd.arg(&self.file); cmd.args(&self.other).arg("--cap-lints=warn"); if let Some(edition) = self.enabled_edition { cmd.arg("--edition").arg(edition.to_string()); - if self.idioms && edition >= Edition::Edition2018 { - cmd.arg("-Wrust-2018-idioms"); + if self.idioms && edition.supports_idiom_lint() { + cmd.arg(format!("-Wrust-{}-idioms", edition)); } } - if let Some(edition) = self.prepare_for_edition_resolve() { - cmd.arg("-W").arg(format!("rust-{}-compatibility", edition)); + if let Some(edition) = self.prepare_for_edition { + if edition.supports_compat_lint() { + cmd.arg("-W").arg(format!("rust-{}-compatibility", edition)); + } } } - /// Verifies that we're not both preparing for an enabled edition and enabling - /// the edition. - /// - /// This indicates that `cargo fix --prepare-for` is being executed out of - /// order with enabling the edition itself, meaning that we wouldn't - /// actually be able to fix anything! If it looks like this is happening - /// then yield an error to the user, indicating that this is happening. - fn verify_not_preparing_for_enabled_edition(&self) -> CargoResult<()> { - let edition = match self.prepare_for_edition_resolve() { - Some(s) => s, - None => return Ok(()), - }; - let enabled = match self.enabled_edition { + /// Validates the edition, and sends a message indicating what is being + /// done. + fn check_edition_and_send_status(&self) -> CargoResult<()> { + let to_edition = match self.prepare_for_edition { Some(s) => s, - None => return Ok(()), - }; - if edition != enabled { - return Ok(()); - } - let path = match &self.file { - Some(s) => s, - None => return Ok(()), + None => { + return Message::Fixing { + file: self.file.display().to_string(), + } + .post(); + } }; - - Message::EditionAlreadyEnabled { - file: path.display().to_string(), - edition: edition.to_string(), + // Unfortunately determining which cargo targets are being built + // isn't easy, and each target can be a different edition. The + // cargo-as-rustc fix wrapper doesn't know anything about the + // workspace, so it can't check for the `cargo-features` unstable + // opt-in. As a compromise, this just restricts to the nightly + // toolchain. + // + // Unfortunately this results in a pretty poor error message when + // multiple jobs run in parallel (the error appears multiple + // times). Hopefully this doesn't happen often in practice. + if !to_edition.is_stable() && !nightly_features_allowed() { + bail!( + "cannot migrate {} to edition {to_edition}\n\ + Edition {to_edition} is unstable and not allowed in this release, \ + consider trying the nightly release channel.", + self.file.display(), + to_edition = to_edition + ); } - .post()?; - - process::exit(1); - } - - fn prepare_for_edition_resolve(&self) -> Option { - match self.prepare_for_edition { - PrepareFor::Edition(s) => Some(s), - PrepareFor::Next => Some(self.next_edition()), - PrepareFor::None => None, - } - } - - fn next_edition(&self) -> Edition { - match self.enabled_edition { - None | Some(Edition::Edition2015) => Edition::Edition2018, - Some(Edition::Edition2018) => Edition::Edition2018, // TODO: Change to 2021 when rustc is ready for it. - Some(Edition::Edition2021) => Edition::Edition2021, + let from_edition = self.enabled_edition.unwrap_or(Edition::Edition2015); + if from_edition == to_edition { + Message::EditionAlreadyEnabled { + file: self.file.display().to_string(), + edition: to_edition, + } + .post() + } else { + Message::Migrating { + file: self.file.display().to_string(), + from_edition, + to_edition, + } + .post() } } } diff --git a/src/cargo/util/command_prelude.rs b/src/cargo/util/command_prelude.rs index e60d7890225..3d7268caac5 100644 --- a/src/cargo/util/command_prelude.rs +++ b/src/cargo/util/command_prelude.rs @@ -1,5 +1,5 @@ use crate::core::compiler::{BuildConfig, MessageFormat}; -use crate::core::Workspace; +use crate::core::{Edition, Workspace}; use crate::ops::{CompileFilter, CompileOptions, NewOptions, Packages, VersionControl}; use crate::sources::CRATES_IO_REGISTRY; use crate::util::important_paths::find_root_manifest_for_wd; @@ -188,7 +188,7 @@ pub trait AppExt: Sized { ._arg(opt("lib", "Use a library template")) ._arg( opt("edition", "Edition to set for the crate generated") - .possible_values(&["2015", "2018", "2021"]) + .possible_values(Edition::CLI_VALUES) .value_name("YEAR"), ) ._arg( diff --git a/src/cargo/util/diagnostic_server.rs b/src/cargo/util/diagnostic_server.rs index 705c1c989ae..1fedf621cba 100644 --- a/src/cargo/util/diagnostic_server.rs +++ b/src/cargo/util/diagnostic_server.rs @@ -13,6 +13,7 @@ use anyhow::{Context, Error}; use log::warn; use serde::{Deserialize, Serialize}; +use crate::core::Edition; use crate::util::errors::CargoResult; use crate::util::{Config, ProcessBuilder}; @@ -28,10 +29,18 @@ const PLEASE_REPORT_THIS_BUG: &str = fixing code with the `--broken-code` flag\n\n\ "; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] pub enum Message { + Migrating { + file: String, + from_edition: Edition, + to_edition: Edition, + }, Fixing { file: String, + }, + Fixed { + file: String, fixes: u32, }, FixFailed { @@ -45,12 +54,7 @@ pub enum Message { }, EditionAlreadyEnabled { file: String, - edition: String, - }, - IdiomEditionMismatch { - file: String, - idioms: String, - edition: Option, + edition: Edition, }, } @@ -80,25 +84,40 @@ impl Message { pub struct DiagnosticPrinter<'a> { config: &'a Config, - edition_already_enabled: HashSet, - idiom_mismatch: HashSet, + dedupe: HashSet, } impl<'a> DiagnosticPrinter<'a> { pub fn new(config: &'a Config) -> DiagnosticPrinter<'a> { DiagnosticPrinter { config, - edition_already_enabled: HashSet::new(), - idiom_mismatch: HashSet::new(), + dedupe: HashSet::new(), } } pub fn print(&mut self, msg: &Message) -> CargoResult<()> { match msg { - Message::Fixing { file, fixes } => { + Message::Migrating { + file, + from_edition, + to_edition, + } => { + if !self.dedupe.insert(msg.clone()) { + return Ok(()); + } + self.config.shell().status( + "Migrating", + &format!("{} from {} edition to {}", file, from_edition, to_edition), + ) + } + Message::Fixing { file } => self + .config + .shell() + .verbose(|shell| shell.status("Fixing", file)), + Message::Fixed { file, fixes } => { let msg = if *fixes == 1 { "fix" } else { "fixes" }; let msg = format!("{} ({} {})", file, fixes, msg); - self.config.shell().status("Fixing", msg) + self.config.shell().status("Fixed", msg) } Message::ReplaceFailed { file, message } => { let msg = format!("error applying suggestions to `{}`\n", file); @@ -158,57 +177,13 @@ impl<'a> DiagnosticPrinter<'a> { Ok(()) } Message::EditionAlreadyEnabled { file, edition } => { - // Like above, only warn once per file - if !self.edition_already_enabled.insert(file.clone()) { + if !self.dedupe.insert(msg.clone()) { return Ok(()); } - - let msg = format!( - "\ -cannot prepare for the {} edition when it is enabled, so cargo cannot -automatically fix errors in `{}` - -To prepare for the {0} edition you should first remove `edition = '{0}'` from -your `Cargo.toml` and then rerun this command. Once all warnings have been fixed -then you can re-enable the `edition` key in `Cargo.toml`. For some more -information about transitioning to the {0} edition see: - - https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html -", - edition, - file, - ); - self.config.shell().error(&msg)?; - Ok(()) - } - Message::IdiomEditionMismatch { - file, - idioms, - edition, - } => { - // Same as above - if !self.idiom_mismatch.insert(file.clone()) { - return Ok(()); - } - self.config.shell().error(&format!( - "\ -cannot migrate to the idioms of the {} edition for `{}` -because it is compiled {}, which doesn't match {0} - -consider migrating to the {0} edition by adding `edition = '{0}'` to -`Cargo.toml` and then rerunning this command; a more detailed transition -guide can be found at - - https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html -", - idioms, - file, - match edition { - Some(s) => format!("with the {} edition", s), - None => "without an edition".to_string(), - }, - ))?; - Ok(()) + self.config.shell().warn(&format!( + "`{}` is already on the latest edition ({}), unable to migrate further", + file, edition + )) } } } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 6d6b9f1e76a..683f5e7c825 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -1061,6 +1061,15 @@ impl TomlManifest { } else { Edition::Edition2015 }; + if edition == Edition::Edition2021 { + features.require(Feature::edition2021())?; + } else if !edition.is_stable() { + // Guard in case someone forgets to add .require() + return Err(util::errors::internal(format!( + "edition {} should be gated", + edition + ))); + } let rust_version = if let Some(rust_version) = &project.rust_version { if features.require(Feature::rust_version()).is_err() { diff --git a/src/doc/man/cargo-fix.md b/src/doc/man/cargo-fix.md index 606fe1acd62..ab1e24e4759 100644 --- a/src/doc/man/cargo-fix.md +++ b/src/doc/man/cargo-fix.md @@ -14,17 +14,13 @@ cargo-fix - Automatically fix lint warnings reported by rustc This Cargo subcommand will automatically take rustc's suggestions from diagnostics like warnings and apply them to your source code. This is intended to help automate tasks that rustc itself already knows how to tell you to fix! -The `cargo fix` subcommand is also being developed for the Rust 2018 edition -to provide code the ability to easily opt-in to the new edition without having -to worry about any breakage. Executing `cargo fix` will under the hood execute {{man "cargo-check" 1}}. Any warnings applicable to your crate will be automatically fixed (if possible) and all remaining warnings will be displayed when the check process is finished. For -example if you'd like to prepare for the 2018 edition, you can do so by -executing: +example if you'd like to apply all fixes to the current package, you can run: - cargo fix --edition + cargo fix which behaves the same as `cargo check --all-targets`. @@ -32,16 +28,40 @@ which behaves the same as `cargo check --all-targets`. `cargo check`. If code is conditionally enabled with optional features, you will need to enable those features for that code to be analyzed: - cargo fix --edition --features foo + cargo fix --features foo Similarly, other `cfg` expressions like platform-specific code will need to pass `--target` to fix code for the given target. - cargo fix --edition --target x86_64-pc-windows-gnu + cargo fix --target x86_64-pc-windows-gnu If you encounter any problems with `cargo fix` or otherwise have any questions or feature requests please don't hesitate to file an issue at - +. + +### Edition migration + +The `cargo fix` subcommand can also be used to migrate a package from one +[edition] to the next. The general procedure is: + +1. Run `cargo fix --edition`. Consider also using the `--all-features` flag if + your project has multiple features. You may also want to run `cargo fix + --edition` multiple times with different `--target` flags if your project + has platform-specific code gated by `cfg` attributes. +2. Modify `Cargo.toml` to set the [edition field] to the new edition. +3. Run your project tests to verify that everything still works. If new + warnings are issued, you may want to consider running `cargo fix` again + (without the `--edition` flag) to apply any suggestions given by the + compiler. + +And hopefully that's it! Just keep in mind of the caveats mentioned above that +`cargo fix` cannot update code for inactive features or `cfg` expressions. +Also, in some rare cases the compiler is unable to automatically migrate all +code to the new edition, and this may require manual changes after building +with the new edition. + +[edition]: https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html +[edition field]: ../reference/manifest.html#the-edition-field ## OPTIONS @@ -56,9 +76,9 @@ code in the working directory for you to inspect and manually fix. {{/option}} {{#option "`--edition`" }} -Apply changes that will update the code to the latest edition. This will not +Apply changes that will update the code to the next edition. This will not update the edition in the `Cargo.toml` manifest, which must be updated -manually. +manually after `cargo fix --edition` has finished. {{/option}} {{#option "`--edition-idioms`" }} @@ -146,7 +166,7 @@ When no target selection options are given, `cargo fix` will fix all targets cargo fix -2. Convert a 2015 edition to 2018: +2. Update a package to prepare it for the next edition: cargo fix --edition diff --git a/src/doc/man/generated_txt/cargo-fix.txt b/src/doc/man/generated_txt/cargo-fix.txt index f0785699ea7..c2b3aa5300d 100644 --- a/src/doc/man/generated_txt/cargo-fix.txt +++ b/src/doc/man/generated_txt/cargo-fix.txt @@ -10,17 +10,15 @@ DESCRIPTION This Cargo subcommand will automatically take rustc's suggestions from diagnostics like warnings and apply them to your source code. This is intended to help automate tasks that rustc itself already knows how to - tell you to fix! The cargo fix subcommand is also being developed for - the Rust 2018 edition to provide code the ability to easily opt-in to - the new edition without having to worry about any breakage. + tell you to fix! Executing cargo fix will under the hood execute cargo-check(1). Any warnings applicable to your crate will be automatically fixed (if possible) and all remaining warnings will be displayed when the check - process is finished. For example if you'd like to prepare for the 2018 - edition, you can do so by executing: + process is finished. For example if you'd like to apply all fixes to the + current package, you can run: - cargo fix --edition + cargo fix which behaves the same as cargo check --all-targets. @@ -28,16 +26,42 @@ DESCRIPTION cargo check. If code is conditionally enabled with optional features, you will need to enable those features for that code to be analyzed: - cargo fix --edition --features foo + cargo fix --features foo Similarly, other cfg expressions like platform-specific code will need to pass --target to fix code for the given target. - cargo fix --edition --target x86_64-pc-windows-gnu + cargo fix --target x86_64-pc-windows-gnu If you encounter any problems with cargo fix or otherwise have any questions or feature requests please don't hesitate to file an issue at - + . + + Edition migration + The cargo fix subcommand can also be used to migrate a package from one + edition + + to the next. The general procedure is: + + 1. Run cargo fix --edition. Consider also using the --all-features flag + if your project has multiple features. You may also want to run cargo + fix --edition multiple times with different --target flags if your + project has platform-specific code gated by cfg attributes. + + 2. Modify Cargo.toml to set the edition field + + to the new edition. + + 3. Run your project tests to verify that everything still works. If new + warnings are issued, you may want to consider running cargo fix again + (without the --edition flag) to apply any suggestions given by the + compiler. + + And hopefully that's it! Just keep in mind of the caveats mentioned + above that cargo fix cannot update code for inactive features or cfg + expressions. Also, in some rare cases the compiler is unable to + automatically migrate all code to the new edition, and this may require + manual changes after building with the new edition. OPTIONS Fix options @@ -48,9 +72,9 @@ OPTIONS and manually fix. --edition - Apply changes that will update the code to the latest edition. This + Apply changes that will update the code to the next edition. This will not update the edition in the Cargo.toml manifest, which must - be updated manually. + be updated manually after cargo fix --edition has finished. --edition-idioms Apply suggestions that will update code to the preferred style for @@ -356,7 +380,7 @@ EXAMPLES cargo fix - 2. Convert a 2015 edition to 2018: + 2. Update a package to prepare it for the next edition: cargo fix --edition diff --git a/src/doc/src/commands/cargo-fix.md b/src/doc/src/commands/cargo-fix.md index 19b5d3e34c6..0d8c9143369 100644 --- a/src/doc/src/commands/cargo-fix.md +++ b/src/doc/src/commands/cargo-fix.md @@ -14,17 +14,13 @@ cargo-fix - Automatically fix lint warnings reported by rustc This Cargo subcommand will automatically take rustc's suggestions from diagnostics like warnings and apply them to your source code. This is intended to help automate tasks that rustc itself already knows how to tell you to fix! -The `cargo fix` subcommand is also being developed for the Rust 2018 edition -to provide code the ability to easily opt-in to the new edition without having -to worry about any breakage. Executing `cargo fix` will under the hood execute [cargo-check(1)](cargo-check.html). Any warnings applicable to your crate will be automatically fixed (if possible) and all remaining warnings will be displayed when the check process is finished. For -example if you'd like to prepare for the 2018 edition, you can do so by -executing: +example if you'd like to apply all fixes to the current package, you can run: - cargo fix --edition + cargo fix which behaves the same as `cargo check --all-targets`. @@ -32,16 +28,40 @@ which behaves the same as `cargo check --all-targets`. `cargo check`. If code is conditionally enabled with optional features, you will need to enable those features for that code to be analyzed: - cargo fix --edition --features foo + cargo fix --features foo Similarly, other `cfg` expressions like platform-specific code will need to pass `--target` to fix code for the given target. - cargo fix --edition --target x86_64-pc-windows-gnu + cargo fix --target x86_64-pc-windows-gnu If you encounter any problems with `cargo fix` or otherwise have any questions or feature requests please don't hesitate to file an issue at - +. + +### Edition migration + +The `cargo fix` subcommand can also be used to migrate a package from one +[edition] to the next. The general procedure is: + +1. Run `cargo fix --edition`. Consider also using the `--all-features` flag if + your project has multiple features. You may also want to run `cargo fix + --edition` multiple times with different `--target` flags if your project + has platform-specific code gated by `cfg` attributes. +2. Modify `Cargo.toml` to set the [edition field] to the new edition. +3. Run your project tests to verify that everything still works. If new + warnings are issued, you may want to consider running `cargo fix` again + (without the `--edition` flag) to apply any suggestions given by the + compiler. + +And hopefully that's it! Just keep in mind of the caveats mentioned above that +`cargo fix` cannot update code for inactive features or `cfg` expressions. +Also, in some rare cases the compiler is unable to automatically migrate all +code to the new edition, and this may require manual changes after building +with the new edition. + +[edition]: https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html +[edition field]: ../reference/manifest.html#the-edition-field ## OPTIONS @@ -56,9 +76,9 @@ code in the working directory for you to inspect and manually fix.
--edition
-
Apply changes that will update the code to the latest edition. This will not +
Apply changes that will update the code to the next edition. This will not update the edition in the Cargo.toml manifest, which must be updated -manually.
+manually after cargo fix --edition has finished.
--edition-idioms
@@ -437,7 +457,7 @@ details on environment variables that Cargo reads. cargo fix -2. Convert a 2015 edition to 2018: +2. Update a package to prepare it for the next edition: cargo fix --edition diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 49ec4187350..fb248427a68 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1064,6 +1064,33 @@ version = "0.0.1" rust-version = "1.42" ``` +### edition 2021 + +Support for the 2021 [edition] can be enabled by adding the `edition2021` +unstable feature to the top of `Cargo.toml`: + +```toml +cargo-features = ["edition2021"] + +[package] +name = "my-package" +version = "0.1.0" +edition = "2021" +``` + +If you want to transition an existing project from a previous edition, then +`cargo fix --edition` can be used on the nightly channel. After running `cargo +fix`, you can switch the edition to 2021 as illustrated above. + +This feature is very unstable, and is only intended for early testing and +experimentation. Future nightly releases may introduce changes for the 2021 +edition that may break your build. + +The 2021 edition will set the default [resolver version] to "2". + +[edition]: ../../edition-guide/index.html +[resolver version]: resolver.md#resolver-versions +