Skip to content

Add -Zfix-edition #15596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions src/bin/cargo/commands/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,24 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {

let allow_dirty = args.flag("allow-dirty");

ops::fix(
gctx,
&ws,
&root_manifest,
&mut ops::FixOptions {
edition: args.flag("edition"),
idioms: args.flag("edition-idioms"),
compile_opts: opts,
allow_dirty,
allow_staged: allow_dirty || args.flag("allow-staged"),
allow_no_vcs: args.flag("allow-no-vcs"),
broken_code: args.flag("broken-code"),
requested_lockfile_path: lockfile_path,
},
)?;
let mut opts = ops::FixOptions {
edition: args
.flag("edition")
.then_some(ops::EditionFixMode::NextRelative),
idioms: args.flag("edition-idioms"),
compile_opts: opts,
allow_dirty,
allow_staged: allow_dirty || args.flag("allow-staged"),
allow_no_vcs: args.flag("allow-no-vcs"),
broken_code: args.flag("broken-code"),
requested_lockfile_path: lockfile_path,
};

if let Some(fe) = &gctx.cli_unstable().fix_edition {
ops::fix_edition(gctx, &ws, &mut opts, fe)?;
} else {
ops::fix(gctx, &ws, &mut opts)?;
}

Ok(())
}
97 changes: 81 additions & 16 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,17 @@ pub type AllowFeatures = BTreeSet<String>;
/// - 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 invocation below for the new edition.
/// - Gate on that new feature in [`toml`].
/// - Update the shell completion files.
/// - Update any failing tests (hopefully there are very few).
/// - Update unstable.md to add a new section for this new edition (see [this example]).
///
/// ## Stabilization instructions
///
/// - Set [`LATEST_UNSTABLE`] to None.
/// - Set [`LATEST_STABLE`] to the new version.
/// - Update [`is_stable`] to `true`.
/// - Set [`first_version`] to the version it will be released.
/// - Set the editionNNNN feature to stable in the [`features!`] macro invocation below.
/// - Update any tests that are affected.
/// - Update the man page for the `--edition` flag.
/// - Update unstable.md to move the edition section to the bottom.
/// - Update the documentation:
/// - Update any features impacted by the edition.
/// - Update manifest.md#the-edition-field.
Expand All @@ -178,7 +173,6 @@ pub type AllowFeatures = BTreeSet<String>;
/// [`CLI_VALUES`]: Edition::CLI_VALUES
/// [`LATEST_UNSTABLE`]: Edition::LATEST_UNSTABLE
/// [`LATEST_STABLE`]: Edition::LATEST_STABLE
/// [this example]: https://github.com/rust-lang/cargo/blob/3ebb5f15a940810f250b68821149387af583a79e/src/doc/src/reference/unstable.md?plain=1#L1238-L1264
/// [`first_version`]: Edition::first_version
/// [`is_stable`]: Edition::is_stable
/// [`toml`]: crate::util::toml
Expand All @@ -196,12 +190,17 @@ pub enum Edition {
Edition2021,
/// The 2024 edition
Edition2024,
/// The future edition (permanently unstable)
EditionFuture,
}

impl Edition {
/// The latest edition that is unstable.
///
/// This is `None` if there is no next unstable edition.
///
/// Note that this does *not* include "future" since this is primarily
/// used for tests that need to step between stable and unstable.
pub const LATEST_UNSTABLE: Option<Edition> = None;
/// The latest stable edition.
pub const LATEST_STABLE: Edition = Edition::Edition2024;
Expand All @@ -210,11 +209,15 @@ impl Edition {
Self::Edition2018,
Self::Edition2021,
Self::Edition2024,
Self::EditionFuture,
];
/// 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.
///
/// This does not include `future` since we don't need to create new
/// projects with it.
pub const CLI_VALUES: [&'static str; 4] = ["2015", "2018", "2021", "2024"];

/// Returns the first version that a particular edition was released on
Expand All @@ -226,6 +229,7 @@ impl Edition {
Edition2018 => Some(semver::Version::new(1, 31, 0)),
Edition2021 => Some(semver::Version::new(1, 56, 0)),
Edition2024 => Some(semver::Version::new(1, 85, 0)),
EditionFuture => None,
}
}

Expand All @@ -237,6 +241,7 @@ impl Edition {
Edition2018 => true,
Edition2021 => true,
Edition2024 => true,
EditionFuture => false,
}
}

Expand All @@ -250,18 +255,21 @@ impl Edition {
Edition2018 => Some(Edition2015),
Edition2021 => Some(Edition2018),
Edition2024 => Some(Edition2021),
EditionFuture => panic!("future does not have a previous edition"),
}
}

/// 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::*;
// Nothing should treat "future" as being next.
match self {
Edition2015 => Edition2018,
Edition2018 => Edition2021,
Edition2021 => Edition2024,
Edition2024 => Edition2024,
EditionFuture => EditionFuture,
}
}

Expand All @@ -274,18 +282,23 @@ impl Edition {
}
}

/// Whether or not this edition supports the `rust_*_compatibility` lint.
///
/// Ideally this would not be necessary, but editions may 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 {
/// Adds the appropriate argument to generate warnings for this edition.
pub(crate) fn force_warn_arg(&self, cmd: &mut ProcessBuilder) {
use Edition::*;
match self {
Edition2015 => false,
Edition2018 => true,
Edition2021 => true,
Edition2024 => true,
Edition2015 => {}
EditionFuture => {
cmd.arg("--force-warn=edition_future_compatibility");
}
e => {
// Note that cargo always passes this even if the
// compatibility lint group does not exist. When a new edition
// is introduced, but there are no migration lints, rustc does
// not create the lint group. That's OK because rustc will
// just generate a warning about an unknown lint which will be
// suppressed due to cap-lints.
cmd.arg(format!("--force-warn=rust-{e}-compatibility"));
}
}
}

Expand All @@ -299,6 +312,7 @@ impl Edition {
Edition2018 => true,
Edition2021 => false,
Edition2024 => false,
EditionFuture => false,
}
}

Expand All @@ -320,6 +334,7 @@ impl fmt::Display for Edition {
Edition::Edition2018 => f.write_str("2018"),
Edition::Edition2021 => f.write_str("2021"),
Edition::Edition2024 => f.write_str("2024"),
Edition::EditionFuture => f.write_str("future"),
}
}
}
Expand All @@ -332,6 +347,7 @@ impl FromStr for Edition {
"2018" => Ok(Edition::Edition2018),
"2021" => Ok(Edition::Edition2021),
"2024" => Ok(Edition::Edition2024),
"future" => Ok(Edition::EditionFuture),
s if s.parse().map_or(false, |y: u16| y > 2024 && y < 2050) => bail!(
"this version of Cargo is older than the `{}` edition, \
and only supports `2015`, `2018`, `2021`, and `2024` editions.",
Expand All @@ -346,6 +362,45 @@ impl FromStr for Edition {
}
}

/// The value for `-Zfix-edition`.
#[derive(Debug, Deserialize)]
pub enum FixEdition {
/// `-Zfix-edition=start=$INITIAL`
///
/// This mode for `cargo fix` will just run `cargo check` if the current
/// edition is equal to this edition. If it is a different edition, then
/// it just exits with success. This is used for crater integration which
/// needs to set a baseline for the "before" toolchain.
Start(Edition),
/// `-Zfix-edition=end=$INITIAL,$NEXT`
///
/// This mode for `cargo fix` will migrate to the `next` edition if the
/// current edition is `initial`. After migration, it will update
/// `Cargo.toml` and verify that that it works on the new edition. If the
/// current edition is not `initial`, then it immediately exits with
/// success since we just want to ignore those packages.
End { initial: Edition, next: Edition },
}

impl FromStr for FixEdition {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
if let Some(start) = s.strip_prefix("start=") {
Ok(FixEdition::Start(start.parse()?))
} else if let Some(end) = s.strip_prefix("end=") {
let (initial, next) = end
.split_once(',')
.ok_or_else(|| anyhow::format_err!("expected `initial,next`"))?;
Ok(FixEdition::End {
initial: initial.parse()?,
next: next.parse()?,
})
} else {
bail!("invalid `-Zfix-edition, expected start= or end=, got `{s}`");
}
}
}

#[derive(Debug, PartialEq)]
enum Status {
Stable,
Expand Down Expand Up @@ -519,6 +574,9 @@ features! {

/// Allow paths that resolve relatively to a base specified in the config.
(unstable, path_bases, "", "reference/unstable.html#path-bases"),

/// Allows use of editions that are not yet stable.
(unstable, unstable_edition, "", "reference/unstable.html#unstable-edition"),
}

/// Status and metadata for a single unstable feature.
Expand Down Expand Up @@ -772,6 +830,7 @@ unstable_cli_options!(
dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
feature_unification: bool = ("Enable new feature unification modes in workspaces"),
features: Option<Vec<String>>,
fix_edition: Option<FixEdition> = ("Permanently unstable edition migration helper"),
gc: bool = ("Track cache usage and \"garbage collect\" unused files"),
#[serde(deserialize_with = "deserialize_git_features")]
git: Option<GitFeatures> = ("Enable support for shallow git fetch operations"),
Expand Down Expand Up @@ -1279,6 +1338,12 @@ impl CliUnstable {
"doctest-xcompile" => stabilized_warn(k, "1.89", STABILIZED_DOCTEST_XCOMPILE),
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
"feature-unification" => self.feature_unification = parse_empty(k, v)?,
"fix-edition" => {
let fe = v
.ok_or_else(|| anyhow::anyhow!("-Zfix-edition expected a value"))?
.parse()?;
self.fix_edition = Some(fe);
}
"gc" => self.gc = parse_empty(k, v)?,
"git" => {
self.git =
Expand Down
12 changes: 12 additions & 0 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,18 @@ impl<'gctx> Workspace<'gctx> {
Ok(ws)
}

/// Reloads the workspace.
///
/// This is useful if the workspace has been updated, such as with `cargo
/// fix` modifying the `Cargo.toml` file.
pub fn reload(&self, gctx: &'gctx GlobalContext) -> CargoResult<Workspace<'gctx>> {
let mut ws = Workspace::new(self.root_manifest(), gctx)?;
ws.set_resolve_honors_rust_version(Some(self.resolve_honors_rust_version));
ws.set_resolve_feature_unification(self.resolve_feature_unification);
ws.set_requested_lockfile_path(self.requested_lockfile_path.clone());
Ok(ws)
}

fn set_resolve_behavior(&mut self) -> CargoResult<()> {
// - If resolver is specified in the workspace definition, use that.
// - If the root package specifies the resolver, use that.
Expand Down
Loading