| name | rust-syntax-edition-2024 |
| description | Use when the user migrates a crate from edition 2021 to 2024, writes new edition-2024 code, hits the never-type fallback semantic change, encounters `unsafe extern` requirement, or wonders why RPIT lifetime capture default changed. Prevents writing pre-2024 idioms in 2024 code, missing the `cargo fix --edition` migration step, ignoring the `unsafe_op_in_unsafe_fn` lint becoming warn-by-default, or running into the `tail_expr_drop_order` change silently. Covers: all 13 edition-2024 changes shipped in Rust 1.85 (RPIT lifetime capture, never-type fallback, `unsafe extern`, `unsafe(no_mangle)` and `unsafe(link_section)`, `expr` fragment widening, prelude additions, `unsafe_op_in_unsafe_fn` warn-by-default, `if let` chains scope, `tail_expr_drop_order`, gen blocks status, `rust_2024_compatibility` lint group, reserved syntax), migration workflow. Keywords: edition 2024, "cargo fix --edition", "rust 2024", "1.85", "edition migration", "never type fallback", "! to ()", "unsafe extern", "unsafe(no_mangle)", "RPIT capture", "use<>", "unsafe_op_in_unsafe_fn", "if let chain", "tail_expr_drop_order", "expr fragment", "gen block", prelude 2024, "rust_2024_compatibility", "edition guide", "what changed in 2024", "should I migrate".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-syntax-edition-2024
Edition 2024 stabilized in Rust 1.85.0 (2025-02-20). It is the largest edition the Rust project has shipped: 13 language changes, library prelude additions, Cargo default shifts (cargo new defaults to edition = "2024" and resolver "3"), and Rustfmt rule updates. This skill is the authoritative reference every other syntax/impl skill in this package depends on. ALWAYS treat edition 2024 as the baseline; NEVER ship code in this package using pre-2024 idioms unless explicitly demonstrating a migration delta.
Quick Reference
Core rules (deterministic)
- ALWAYS migrate via
cargo fix --edition on a clean working tree. NEVER hand-edit edition idioms before running the automated migration; you will miss lints that only the rustc driver emits.
- NEVER write
extern "C" { ... } in edition 2024 code. ALWAYS write unsafe extern "C" { ... }; items inside are unsafe by default and may be opted into safe per item.
- NEVER use bare
#[no_mangle], #[link_section = "..."], or #[export_name = "..."] in edition 2024. ALWAYS wrap them as #[unsafe(no_mangle)], #[unsafe(link_section = "...")], #[unsafe(export_name = "...")].
- ALWAYS wrap every unsafe operation inside an
unsafe fn body in its own unsafe { ... } block. The 2024 default is #[warn(unsafe_op_in_unsafe_fn)]; pre-2024 code that calls unsafe ops bare inside an unsafe fn body breaks the lint.
- NEVER assume RPIT (
-> impl Trait) does NOT capture an input lifetime in edition 2024. The 2024 default captures all in-scope generics and lifetimes. Opt out with + use<> (or list specific params with + use<'a, T>).
- NEVER rely on
! falling back to () in 2024. The fallback is ! itself; type-inference outcomes for ?-and-unwrap-only paths change. Annotate the turbofish (f::<()>()) at the call site when migration breaks.
- NEVER take a shared or mutable reference to a
static mut. The static_mut_refs lint is deny-by-default in 2024. ALWAYS migrate to AtomicX, Mutex<T>, OnceLock<T>, LazyLock<T>, or &raw const/&raw mut.
- NEVER use
gen as an identifier in edition 2024 code. ALWAYS rename, or escape as r#gen if you cannot. gen blocks themselves remain unstable.
- ALWAYS bind the result of a
RefCell::borrow() / Mutex::lock() to a let before the tail expression dot-chains it. The tail_expr_drop_order change drops temporaries before locals in 2024.
- ALWAYS rewrite
if let Some(x) = read_lock() { ... } else { write_lock() ... } patterns. In 2024 the scrutinee temporary drops BEFORE entering else, which fixes deadlocks but changes drop order observably.
Decision tree: "Should I migrate this crate to 2024?"
Does the crate compile cleanly on Rust 1.85 or newer with edition 2021?
NO -> Fix 1.85 compile errors first, THEN migrate edition.
YES -> Run `cargo +stable fix --edition` on clean working tree.
Did `cargo fix --edition` apply changes without error?
YES -> Bump `edition = "2024"` in Cargo.toml. Build + test.
Tests pass?
YES -> Commit. Run `cargo clippy` for new lints. Done.
NO -> Inspect `never_type_fallback`, `tail_expr_drop_order`,
`if_let_rescope` lints in build log. Apply targeted fixes.
NO -> Read failure output. Most common cause is references to
`static mut` (lint `static_mut_refs`) which has NO autofix.
Refactor to atomics / Mutex / OnceLock, then re-run.
Migration checklist (mandatory order)
git status must be clean. cargo build must be green on edition 2021 with rustc 1.85+.
cargo fix --edition (NOT cargo fix --edition --allow-dirty unless you committed your cargo fix output deliberately).
- Review diff. The driver runs every lint in the
rust_2024_compatibility lint group.
- Edit
Cargo.toml: edition = "2024". Optionally set resolver = "3" if you have a workspace root.
cargo build; cargo test; cargo clippy --all-targets.
- Manually inspect any
tail_expr_drop_order warnings (no autofix); migrate or accept.
- Manually inspect any
static_mut_refs errors (no autofix); refactor away from static mut.
- Commit migration as a single dedicated commit.
The 13 language changes (cheat-sheet table)
| # | Change | Autofix? | Lint name |
|---|
| 1 | RPIT captures all in-scope lifetimes | YES | impl_trait_overcaptures |
| 2 | Never-type fallback () -> ! | partial | never_type_fallback_flowing_into_unsafe |
| 3 | unsafe extern { ... } mandatory | YES | missing_unsafe_on_extern |
| 4 | #[unsafe(no_mangle)] etc. required | YES | unsafe_attr_outside_unsafe |
| 5 | unsafe_op_in_unsafe_fn warn-by-default | YES | unsafe_op_in_unsafe_fn |
| 6 | if let scrutinee dropped before else | YES (rewrite to match) | if_let_rescope |
| 7 | Tail-expression temporaries drop before locals | NO | tail_expr_drop_order |
| 8 | static mut refs deny-by-default | NO | static_mut_refs |
| 9 | expr fragment matches const {} and _ | YES (rewrite to expr_2021) | edition_2024_expr_fragment_specifier |
| 10 | gen reserved keyword | YES (r#gen) | keyword_idents_2024 |
| 11 | Reserved guarded string literal syntax | YES | rust_2024_guarded_string_incompatible_syntax |
| 12 | Match-ergonomics restrictions for & patterns | YES | mismatched_lifetime_syntaxes (closely related) |
| 13 | Prelude adds Future, IntoFuture | YES (FQN rewrite) | rust_2024_prelude_collisions |
ALL of the above except items 2 (partial), 7, and 8 are fully covered by cargo fix --edition. Items 7 and 8 ALWAYS require human review.
Patterns
Pattern: RPIT lifetime capture (item 1)
In edition 2024, every in-scope lifetime and type parameter is implicitly captured by -> impl Trait. Pre-2024 only types/lifetimes that syntactically appeared in trait bounds were captured.
Side-by-side:
fn f<'a>(x: &'a [u8]) -> impl Sized { 42_u32 }
fn f<'a>(x: &'a [u8]) -> impl Sized { 42_u32 }
fn f<'a>(x: &'a [u8]) -> impl Sized + use<> { 42_u32 }
fn g<'a, 'b, T>(x: &'a [u8], y: &'b T) -> impl Sized + use<'a, T> { 42_u32 }
When the caller requires 'static from the returned opaque type, 2024 overcapture is a compile error; add + use<> to opt out. The impl_trait_overcaptures lint flags every such case; cargo fix --edition inserts + use<> automatically. See [[rust-syntax-lifetimes]] for the broader use<> story.
Pattern: never-type fallback (item 2)
fn outer<T>(x: T) -> Result<T, ()> {
fn f<T: Default>() -> Result<T, ()> { Ok(T::default()) }
f()?;
Ok(x)
}
fn outer<T>(x: T) -> Result<T, ()> {
fn f<T: Default>() -> Result<T, ()> { Ok(T::default()) }
f::<()>()?;
Ok(x)
}
The never_type_fallback_flowing_into_unsafe lint is deny-by-default in 2024 because the change interacts with unsafe code paths in dangerous ways. See references/methods.md for the safety rationale.
Pattern: unsafe extern (item 3)
extern "C" {
pub fn sqrt(x: f64) -> f64;
pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;
}
unsafe extern "C" {
pub safe fn sqrt(x: f64) -> f64;
pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;
pub fn free(p: *mut core::ffi::c_void);
}
The safe modifier per item is a 2024 feature: it lets you declare bindings whose ABI you have audited and whose preconditions hold unconditionally. See [[rust-impl-ffi-bindgen]].
Pattern: unsafe attributes (item 4)
#[no_mangle]
pub extern "C" fn example() {}
#[link_section = ".init_array"]
static INIT: extern "C" fn() = example;
#[unsafe(no_mangle)]
pub extern "C" fn example() {}
#[unsafe(link_section = ".init_array")]
static INIT: extern "C" fn() = example;
ALWAYS write a // SAFETY: comment above each #[unsafe(...)] attribute. The compiler does not enforce it, but reviewers will reject changes without it.
Pattern: unsafe_op_in_unsafe_fn (item 5)
unsafe fn read_unchecked<T>(slice: &[T], i: usize) -> &T {
slice.get_unchecked(i)
}
unsafe fn read_unchecked<T>(slice: &[T], i: usize) -> &T {
unsafe { slice.get_unchecked(i) }
}
See [[rust-syntax-unsafe]] for the broader two-purposes-of-unsafe-fn discussion.
Pattern: if let rescope (item 6) and tail expr drop order (item 7)
use std::sync::RwLock;
fn f_2021(value: &RwLock<Option<bool>>) {
if let Some(x) = *value.read().unwrap() {
println!("{x}");
} else {
let mut w = value.write().unwrap();
if w.is_none() { *w = Some(true); }
}
}
Tail-expression drop order pairs with this. A RefCell::borrow() chained off a local goes wrong in 2024 because the borrow temporary now drops BEFORE the local:
use std::cell::RefCell;
fn len() -> usize {
let c = RefCell::new("..");
c.borrow().len()
}
fn len_2024() -> usize {
let c = RefCell::new("..");
let borrowed = c.borrow();
borrowed.len()
}
Items 6 and 7 are the two most common 2024-migration footguns. The if_let_rescope lint suggests rewriting as match (preserves 2021 semantics); the tail_expr_drop_order lint flags potential issues but provides NO autofix.
Pattern: static mut references (item 8)
static mut COUNTER: i32 = 0;
unsafe { let _r = &COUNTER; }
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
COUNTER.fetch_add(1, Ordering::Relaxed);
static mut COUNTER: i32 = 0;
let p = &raw const COUNTER;
There is NO automatic migration for static_mut_refs. ALWAYS refactor toward atomics, Mutex, OnceLock, or &raw const/&raw mut. See [[rust-syntax-unsafe]] for strict-provenance APIs.
Pattern: expr fragment widening (item 9), gen keyword (item 10), prelude additions (item 13)
macro_rules! takes_either {
($e:expr) => { };
(const $e:expr_2021) => { };
}
fn r#gen() -> i32 { 0 }
See [[rust-syntax-pattern-matching]] for the related match-ergonomics tightening (item 12).
Edge Cases
- The
unsafe(...) attribute syntax (item 4) is independently stable since Rust 1.82, so you can adopt it BEFORE migrating the edition. Doing so reduces churn in the migration commit.
- The precise-capturing
+ use<...> syntax (item 1) is stable since Rust 1.82 as well; the EDITION change is the implicit-capture default, not the syntax availability.
cargo new defaults to edition = "2024" AND resolver = "3" since 1.85. If you scaffold subprojects inside a workspace whose root uses resolver = "2", Cargo warns and uses the workspace setting; declare resolver explicitly at the workspace root to avoid surprises. See [[rust-impl-cargo-project]].
- The
rust_2024_compatibility lint group is a UMBRELLA group: enabling it during 2021 development surfaces every 2024-incompatibility before you migrate. ALWAYS add #![warn(rust_2024_compatibility)] to crate roots that target a future migration.
- Edition is a per-CRATE property, NOT a per-workspace property. A workspace MAY mix 2021 and 2024 crates. Public APIs across the boundary are stable; only the source-level idioms differ.
Cross-References
- [[rust-syntax-lifetimes]] : RPIT lifetime capture deep dive,
use<> semantics, variance.
- [[rust-syntax-unsafe]] :
unsafe_op_in_unsafe_fn rationale, 8 unsafe superpowers, strict-provenance APIs replacing static mut refs.
- [[rust-syntax-pattern-matching]] : match-ergonomics changes for
& patterns (item 12).
- [[rust-core-type-system]] : never-type
! and its fallback rules.
- [[rust-impl-ffi-bindgen]] :
unsafe extern blocks and the safe per-item modifier.
- [[rust-core-language-versions]] : Rust 1.80 through 1.87 release-by-release feature index; edition 2024 is the 1.85 entry.
Reference Links
Official Sources