From 128c8a9b5942a0105e38e7e4eb274fb9c0b22474 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Mon, 17 Aug 2026 03:05:35 +0300 Subject: [PATCH 01/30] Fix HIR lowering of params of trait assoc fns It turns out they should be treated as bare idents, not patterns, but only if they have no body. Also allow macro patterns for both them and extern fns, because rustc allows that. --- .../crates/hir-def/src/expr_store/lower.rs | 81 ++++++++++++------- .../crates/hir-ty/src/tests/regression.rs | 44 +++++++++- 2 files changed, 92 insertions(+), 33 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs index 91faafaf843e0..08cecb52570eb 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs @@ -135,7 +135,9 @@ pub(super) fn lower_body( } collector.with_expr_root(|collector| { - if let Some(param_list) = parameters { + if let DefWithBodyId::FunctionId(func) = owner + && let Some(param_list) = parameters + { if let Some(self_param_syn) = param_list.self_param().filter(|it| collector.check_cfg(it)) { @@ -155,23 +157,28 @@ pub(super) fn lower_body( Some(collector.expander.in_file(AstPtr::new(&self_param_syn))); } - let is_extern = matches!( - owner, - DefWithBodyId::FunctionId(id) - if matches!(id.loc(db).container, ItemContainerId::ExternBlockId(_)), - ); + let params_are_bare_idents = match func.loc(db).container { + ItemContainerId::ExternBlockId(_) => true, + ItemContainerId::TraitId(_) => body.is_none(), + ItemContainerId::ModuleId(_) | ItemContainerId::ImplId(_) => false, + }; for param in param_list.params() { if collector.check_cfg(¶m) { - let param_pat = if is_extern { - collector.collect_extern_fn_param(param.pat()) - } else { - collector.collect_pat_top(param.pat()) + let param_pat = match param.pat() { + Some(pat) => { + if params_are_bare_idents { + collector.collect_param_as_ident(pat) + } else { + collector.collect_pat_top(Some(pat)) + } + } + None => collector.missing_pat(), }; params.push(Param::new(param_pat)); } } - }; + } collector.collect( &mut self_param, @@ -2792,11 +2799,11 @@ impl<'db> ExprCollector<'db> { } } - fn collect_extern_fn_param(&mut self, pat: Option) -> PatId { - // parameters of functions in `extern` blocks can only be simple identifiers and wildcards. + fn collect_param_as_ident(&mut self, pat: ast::Pat) -> PatId { + // parameters of functions in `extern` blocks and associated trait functions without a body + // can only be simple identifiers and wildcards. // Furthermore, the identifiers in their parameters are always interpreted as bindings, even // if in a normal function they won't be, because they would refer to a path pattern. - let Some(pat) = pat else { return self.missing_pat() }; match &pat { ast::Pat::IdentPat(bp) if bp.is_simple_ident() => { @@ -2812,6 +2819,8 @@ impl<'db> ExprCollector<'db> { pat } ast::Pat::WildcardPat(_) => self.alloc_pat(Pat::Wild, AstPtr::new(&pat)), + ast::Pat::MacroPat(mac) => self + .collect_macro_pat_with(mac.clone(), |this, pat| this.collect_param_as_ident(pat)), _ => { self.store.diagnostics.push(ExpressionStoreDiagnostics::PatternArgInExternFn { node: self.expander.in_file(AstPtr::new(&pat)), @@ -3017,19 +3026,11 @@ impl<'db> ExprCollector<'db> { Pat::Missing } } - ast::Pat::MacroPat(mac) => match mac.macro_call() { - Some(call) => { - let macro_ptr = AstPtr::new(&call); - let src = self.expander.in_file(AstPtr::new(&pat)); - let pat = - self.collect_macro_call(call, macro_ptr, true, |this, expanded_pat| { - this.collect_pat_opt(expanded_pat, binding_list) - }); - self.store.pat_map.insert(src, pat.into()); - return pat; - } - None => Pat::Missing, - }, + ast::Pat::MacroPat(mac) => { + return self.collect_macro_pat_with(mac.clone(), |this, expanded_pat| { + this.collect_pat(expanded_pat, binding_list) + }); + } ast::Pat::RangePat(p) => { let mut range_part_lower = |p: Option| -> Option { p.and_then(|it| { @@ -3068,6 +3069,28 @@ impl<'db> ExprCollector<'db> { self.alloc_pat(pattern, ptr) } + fn collect_macro_pat_with( + &mut self, + mac: ast::MacroPat, + callback: impl FnOnce(&mut Self, ast::Pat) -> PatId, + ) -> PatId { + match mac.macro_call() { + Some(call) => { + let macro_ptr = AstPtr::new(&call); + let src = self.expander.in_file(AstPtr::new(&mac.into())); + let pat = self.collect_macro_call(call, macro_ptr, true, |this, expanded_pat| { + match expanded_pat { + Some(pat) => callback(this, pat), + None => this.missing_pat(), + } + }); + self.store.pat_map.insert(src, pat.into()); + pat + } + None => self.missing_pat(), + } + } + fn collect_pat_opt(&mut self, pat: Option, binding_list: &mut BindingList) -> PatId { match pat { Some(pat) => self.collect_pat(pat, binding_list), @@ -3172,9 +3195,7 @@ impl<'db> ExprCollector<'db> { ) } ast::Pat::MacroPat(pat) => { - let Some(call) = pat.macro_call() else { return self.missing_pat() }; - let ptr = AstPtr::new(&call); - self.collect_macro_call(call, ptr, true, |this, pat| this.collect_ty_pat_opt(pat)) + self.collect_macro_pat_with(pat, |this, pat| this.collect_ty_pat(pat)) } _ => { // FIXME: Emit an error. diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs index cdc0a8ab9e36e..ba418558e913f 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs @@ -2796,10 +2796,48 @@ where fn extern_fns_cannot_have_param_patterns() { check_no_mismatches( r#" -pub(crate) struct Builder<'a>(&'a ()); +macro_rules! m { + () => { Builder }; +} + +pub(crate) struct Builder; + +unsafe extern "C" { + pub(crate) fn foo(Builder: (), m!(): ()); +} + "#, + ); +} + +#[test] +fn trait_assoc_fns_cannot_have_param_patterns() { + check_no_mismatches( + r#" +macro_rules! m { + () => { Builder }; +} -unsafe extern "C" { - pub(crate) fn foo<'a>(Builder: &Builder<'a>); +pub(crate) struct Builder; + +trait Trait { + fn foo(Builder: (), m!(): ()); +} + "#, + ); + // But assoc fns with bodies do have patterns: + check( + r#" +macro_rules! m { + () => { Builder }; +} + +pub(crate) struct Builder; + +trait Trait { + fn foo(Builder: (), + // ^^^^^^^ expected (), got Builder + m!(): ()) {} + // ^^ expected (), got Builder } "#, ); From 7c99e9480e54f1eda2259bb17dbf1322e87d684a Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Mon, 17 Aug 2026 22:55:02 +0300 Subject: [PATCH 02/30] Push a generic params scope for consts --- .../crates/hir-def/src/resolver.rs | 4 ++- .../ide-completion/src/tests/expression.rs | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs b/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs index 5b11f5ff8bbb6..9d5d80a73ef6a 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs @@ -1392,7 +1392,9 @@ impl HasResolver for FunctionId { impl HasResolver for ConstId { fn resolver(self, db: &dyn SourceDatabase) -> Resolver<'_> { - lookup_resolver(db, self) + // Consts can have generic params on nightly. Furthermore they're a `GenericDefId`, + // so not pushing a generic params scope here complicates things (e.g. `TypeOwnerId` tracking). + lookup_resolver(db, self).push_generic_params_scope(db, self.into()) } } diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index 0e558cf6a2b57..0a4057df974e2 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -4208,3 +4208,38 @@ fn foo(t: T) { "#]], ); } + +#[test] +fn const_is_type_owner() { + check( + r#" +pub struct Boo; +pub struct A(Boo); +impl A { + const X: A = A(B$0); +} + "#, + expect![[r#" + sp Self A + st A A + st Boo Boo + st Boo Boo + bt u32 u32 + kw const + kw crate:: + kw false + kw for + kw if + kw if let + kw loop + kw match + kw self:: + kw true + kw unsafe + kw while + kw while let + ex A::X.0 + ex Boo + "#]], + ); +} From a52423a484146b594a903063b494d6578c878807 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Tue, 18 Aug 2026 15:26:17 +0300 Subject: [PATCH 03/30] Fix unsafeck of `&raw *` We accidentally skipped one level more than needed. --- .../crates/hir-ty/src/diagnostics/unsafe_check.rs | 4 +--- .../ide-diagnostics/src/handlers/missing_unsafe.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs b/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs index 3021de68f3fdf..58598980707b3 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs @@ -315,9 +315,7 @@ impl<'db> UnsafeVisitor<'db> { // https://github.com/rust-lang/rust/pull/129248 // Taking a raw ref to a deref place expr is always safe. Expr::UnaryOp { expr, op: UnaryOp::Deref } => { - self.body - .walk_child_exprs_without_pats(expr, |child| self.walk_expr(child)); - + self.walk_expr(expr); return; } _ => (), diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs index 18859c0db1e60..4b55995a058df 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs @@ -1061,6 +1061,17 @@ fn foo() {} #[target_feature(enable = "avx2", enable = "fma")] fn bar() { foo(); +} + "#, + ); + } + + #[test] + fn raw_ref_deref_raw_ref_deref() { + check_diagnostics( + r#" +fn foo() { + &raw const *&raw const *&raw const *&2; } "#, ); From b055fad7fffedae646d0f09ea9a6ba9cb6f0f7cd Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Fri, 21 Aug 2026 02:34:21 +0300 Subject: [PATCH 04/30] Switch from `temp-dir` to a homemade `NamedTempFile` implementation That also deletes the file when the process exits without being dropped; this is important especially for the proc macro server since it is killed and does not exit normally. Linux and the BSDs unlink the file then access it via `/proc/self/fd` or `/dev/fd`; Windows has a dedicated API for that; and macOS unfortunately does not support that (the file is not removed after being unlinked, but you cannot access it anymore via `/dev/fd`, or at least that's what the AI said - I don't have a macOS machine to check). --- src/tools/rust-analyzer/Cargo.lock | 9 +- src/tools/rust-analyzer/Cargo.toml | 1 - .../crates/proc-macro-srv/Cargo.toml | 4 +- .../crates/proc-macro-srv/src/dylib.rs | 48 ++--- .../crates/proc-macro-srv/src/lib.rs | 10 +- .../crates/proc-macro-srv/src/tests/utils.rs | 4 +- .../crates/project-model/Cargo.toml | 1 - .../project-model/src/cargo_config_file.rs | 23 +-- .../rust-analyzer/crates/stdx/src/lib.rs | 1 + .../rust-analyzer/crates/stdx/src/tempfile.rs | 164 ++++++++++++++++++ 10 files changed, 193 insertions(+), 72 deletions(-) create mode 100644 src/tools/rust-analyzer/crates/stdx/src/tempfile.rs diff --git a/src/tools/rust-analyzer/Cargo.lock b/src/tools/rust-analyzer/Cargo.lock index d1432a481a35b..fea42a383d29a 100644 --- a/src/tools/rust-analyzer/Cargo.lock +++ b/src/tools/rust-analyzer/Cargo.lock @@ -1890,7 +1890,7 @@ dependencies = [ "paths", "proc-macro-test", "span", - "temp-dir", + "stdx", ] [[package]] @@ -1966,7 +1966,6 @@ dependencies = [ "serde_json", "span", "stdx", - "temp-dir", "toml", "toolchain", "tracing", @@ -2752,12 +2751,6 @@ dependencies = [ "tt", ] -[[package]] -name = "temp-dir" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016ef9739649996fcc983b9c588fe3d557cf216d4d98503ce1b057ab5a66d689" - [[package]] name = "tempfile" version = "3.27.0" diff --git a/src/tools/rust-analyzer/Cargo.toml b/src/tools/rust-analyzer/Cargo.toml index d043a3aee4f7f..c75324968c1cf 100644 --- a/src/tools/rust-analyzer/Cargo.toml +++ b/src/tools/rust-analyzer/Cargo.toml @@ -143,7 +143,6 @@ smallvec = { version = "1.15.1", features = [ "const_generics", ] } smol_str = "0.3.2" -temp-dir = "0.2.0" text-size = "1.1.1" toml = "1.1.2" tracing = { version = "0.1.41", default-features = false, features = ["std"] } diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml index 1b86eac0129aa..05e0012586d5f 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml @@ -13,13 +13,11 @@ rust-version.workspace = true doctest = false [dependencies] -temp-dir.workspace = true - paths.workspace = true # span = {workspace = true, default-features = false} does not work span = { path = "../span", version = "0.0.0", default-features = false} intern.workspace = true - +stdx.workspace = true [dev-dependencies] expect-test.workspace = true diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs index 3b9c345fc27f5..2a5e79d9e5cc5 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs @@ -2,13 +2,12 @@ mod proc_macros; +use paths::{Utf8Path, Utf8PathBuf}; use rustc_codegen_ssa::back::metadata::DefaultMetadataLoader; use rustc_interface::util::rustc_version_str; use rustc_proc_macro::bridge; -use std::{fs, io, time::SystemTime}; -use temp_dir::TempDir; - -use paths::{Utf8Path, Utf8PathBuf}; +use std::{fs, io, path::Path, time::SystemTime}; +use stdx::tempfile::NamedTempFile; use crate::{ PanicMessage, ProcMacroClientHandle, ProcMacroKind, ProcMacroSrvSpan, TrackedEnv, @@ -18,19 +17,20 @@ use crate::{ pub(crate) struct Expander { inner: ProcMacroLibrary, modified_time: SystemTime, + _file: NamedTempFile, } impl Expander { - pub(crate) fn new(temp_dir: &TempDir, lib: &Utf8Path) -> io::Result { + pub(crate) fn new(lib: &Utf8Path) -> io::Result { // Some libraries for dynamic loading require canonicalized path even when it is // already absolute let lib = lib.canonicalize_utf8()?; let modified_time = fs::metadata(&lib).and_then(|it| it.modified())?; - let path = ensure_file_with_lock_free_access(temp_dir, &lib)?; - let library = ProcMacroLibrary::open(path.as_ref())?; + let file = ensure_file_with_lock_free_access(lib)?; + let library = ProcMacroLibrary::open(file.path())?; - Ok(Expander { inner: library, modified_time }) + Ok(Expander { inner: library, modified_time, _file: file }) } pub(crate) fn expand<'a, S: ProcMacroSrvSpan + 'a>( @@ -73,10 +73,10 @@ struct ProcMacroLibrary { } impl ProcMacroLibrary { - fn open(path: &Utf8Path) -> io::Result { + fn open(path: &Path) -> io::Result { let proc_macros = rustc_span::create_default_session_globals_then(|| { rustc_metadata::locator::get_proc_macros( - path.as_ref(), + path, &DefaultMetadataLoader, rustc_version_str().unwrap_or("unknown"), ) @@ -88,37 +88,19 @@ impl ProcMacroLibrary { /// Copy the dylib to temp directory to prevent locking in Windows #[cfg(windows)] -fn ensure_file_with_lock_free_access( - temp_dir: &TempDir, - path: &Utf8Path, -) -> io::Result { - use std::collections::hash_map::RandomState; - use std::hash::{BuildHasher, Hasher}; - +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> io::Result { if std::env::var("RA_DONT_COPY_PROC_MACRO_DLL").is_ok() { - return Ok(path.to_path_buf()); + return Ok(NamedTempFile::from_path(path.into_std_path_buf())); } - let mut to = Utf8Path::from_path(temp_dir.path()).unwrap().to_owned(); - let file_name = path.file_stem().ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, format!("File path is invalid: {path}")) })?; - to.push({ - // Generate a unique number by abusing `HashMap`'s hasher. - // Maybe this will also "inspire" a libs team member to finally put `rand` in libstd. - let unique_name = RandomState::new().build_hasher().finish(); - format!("{file_name}-{unique_name}.dll") - }); - fs::copy(path, &to)?; - Ok(to) + NamedTempFile::new_from_existing(&format!("proc-macro-srv-{file_name}.dll"), path.as_std_path()) } #[cfg(unix)] -fn ensure_file_with_lock_free_access( - _temp_dir: &TempDir, - path: &Utf8Path, -) -> io::Result { - Ok(path.to_owned()) +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> io::Result { + Ok(NamedTempFile::from_path(path.into_std_path_buf())) } diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs index 28570e1af4426..7fc04a05155f8 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs @@ -41,7 +41,6 @@ use std::{ use paths::{Utf8Path, Utf8PathBuf}; use span::{FIXUP_ERASED_FILE_AST_ID_MARKER, Span}; -use temp_dir::TempDir; pub use crate::server_impl::token_id::SpanId; @@ -64,16 +63,11 @@ pub const RUSTC_VERSION_STRING: &str = env!("RUSTC_VERSION"); pub struct ProcMacroSrv<'env> { expanders: Mutex>>, env: &'env EnvSnapshot, - temp_dir: TempDir, } impl<'env> ProcMacroSrv<'env> { pub fn new(env: &'env EnvSnapshot) -> Self { - Self { - expanders: Default::default(), - env, - temp_dir: TempDir::with_prefix("proc-macro-srv").unwrap(), - } + Self { expanders: Default::default(), env } } pub fn join_spans(&self, first: Span, second: Span) -> Option { @@ -205,7 +199,7 @@ impl ProcMacroSrv<'_> { fn expander(&self, path: &Utf8Path) -> Result, String> { let expander = || { - let expander = dylib::Expander::new(&self.temp_dir, path) + let expander = dylib::Expander::new(path) .map_err(|err| format!("Cannot create expander for {path}: {err}",)); expander.map(Arc::new) }; diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs index 9780bcf3481b4..7f92c66fb69d5 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs @@ -56,7 +56,7 @@ fn assert_expand_impl( expect_spanned: Expect, ) { let path = proc_macro_test_dylib_path(); - let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap(); + let expander = dylib::Expander::new(&path).unwrap(); let def_site = SpanId(0); let call_site = SpanId(1); @@ -186,7 +186,7 @@ pub fn assert_expand_with_callback( expect_spanned: Expect, ) { let path = proc_macro_test_dylib_path(); - let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap(); + let expander = dylib::Expander::new(&path).unwrap(); let def_site = Span { range: TextRange::new(0.into(), 150.into()), diff --git a/src/tools/rust-analyzer/crates/project-model/Cargo.toml b/src/tools/rust-analyzer/crates/project-model/Cargo.toml index f825a456dea70..86ae3e837d1a8 100644 --- a/src/tools/rust-analyzer/crates/project-model/Cargo.toml +++ b/src/tools/rust-analyzer/crates/project-model/Cargo.toml @@ -20,7 +20,6 @@ semver.workspace = true serde_json.workspace = true serde.workspace = true serde_derive.workspace = true -temp-dir.workspace = true toml.workspace = true tracing = { workspace = true, features = ["attributes"] } triomphe.workspace = true diff --git a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs index defd9f96ab5fb..976f5ddf8abba 100644 --- a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs +++ b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs @@ -1,6 +1,7 @@ //! Read `.cargo/config.toml` as a TOML table use paths::{AbsPath, Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; +use stdx::tempfile::NamedTempFile; use toml::{ Spanned, de::{DeTable, DeValue}, @@ -139,7 +140,7 @@ impl<'a> CargoConfigFileReader<'a> { pub(crate) struct LockfileCopy { pub(crate) path: Utf8PathBuf, pub(crate) usage: LockfileUsage, - _temp_dir: temp_dir::TempDir, + _temp_file: NamedTempFile, } pub(crate) enum LockfileUsage { @@ -193,22 +194,12 @@ pub(crate) fn make_lockfile_copy( return None; }; - let temp_dir = temp_dir::TempDir::with_prefix("rust-analyzer").ok()?; - let path: Utf8PathBuf = temp_dir.path().join("Cargo.lock").try_into().ok()?; - let path = match std::fs::copy(lockfile_path, &path) { - Ok(_) => { - tracing::debug!("Copied lock file from `{}` to `{}`", lockfile_path, path); - path - } - // lockfile does not yet exist, so we can just create a new one in the temp dir - Err(e) if e.kind() == std::io::ErrorKind::NotFound => path, - Err(e) => { - tracing::warn!("Failed to copy lock file from `{lockfile_path}` to `{path}`: {e}",); - return None; - } - }; + let temp_file = + NamedTempFile::new_from_existing("rust-analyzer-Cargo.lock", lockfile_path.as_std_path()) + .ok()?; + let path = Utf8Path::from_path(temp_file.path())?.to_path_buf(); - Some(LockfileCopy { path, usage, _temp_dir: temp_dir }) + Some(LockfileCopy { path, usage, _temp_file: temp_file }) } #[test] diff --git a/src/tools/rust-analyzer/crates/stdx/src/lib.rs b/src/tools/rust-analyzer/crates/stdx/src/lib.rs index 275e0e5ac8db1..dcba06415b5f3 100644 --- a/src/tools/rust-analyzer/crates/stdx/src/lib.rs +++ b/src/tools/rust-analyzer/crates/stdx/src/lib.rs @@ -13,6 +13,7 @@ pub mod non_empty_vec; pub mod panic_context; pub mod process; pub mod rand; +pub mod tempfile; pub mod thread; pub mod variance; diff --git a/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs new file mode 100644 index 0000000000000..b67d4c46c39a3 --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs @@ -0,0 +1,164 @@ +//! A temporary named file that will be deleted on drop, and on operating systems that support that, +//! also when the process exits (including being killed). + +use std::{ + fs::File, + io, + path::{Path, PathBuf}, +}; + +pub struct NamedTempFile { + _file: Option, + path: PathBuf, + delete_on_drop: bool, +} + +impl NamedTempFile { + pub fn new(prefix: &str) -> io::Result { + imp::create(prefix) + } + + /// Creates a new `NamedTempFile` that is a copy of an existing file. + pub fn new_from_existing(prefix: &str, existing: &Path) -> io::Result { + let result = NamedTempFile::new(prefix)?; + std::fs::copy(existing, &result.path)?; + Ok(result) + } + + /// Creates a `NamedTempFile` from a path, without deleting it on drop. + #[inline] + pub fn from_path(path: PathBuf) -> NamedTempFile { + NamedTempFile { _file: None, path, delete_on_drop: false } + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for NamedTempFile { + fn drop(&mut self) { + if self.delete_on_drop && std::fs::remove_file(&self.path).is_err() { + tracing::info!("cannot remove temporary file {}", self.path.display()); + } + } +} + +mod general_imp { + use std::{ + fs::{File, OpenOptions}, + io::{self, ErrorKind}, + path::PathBuf, + sync::atomic::{AtomicU32, Ordering}, + }; + + static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0); + + pub(super) fn create( + prefix: &str, + mut options_callback: impl FnMut(&mut OpenOptions), + ) -> io::Result<(File, PathBuf)> { + let temp_dir = std::env::temp_dir().canonicalize()?; + let pid = std::process::id(); + loop { + let path = temp_dir.join(format!( + "{prefix}{pid:x}-{:x}", + INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), + )); + let mut open_options = OpenOptions::new(); + open_options.create_new(true); + options_callback(&mut open_options); + match open_options.open(&path) { + Err(e) if e.kind() == ErrorKind::AlreadyExists => {} + Err(e) => { + return Err(io::Error::new( + e.kind(), + format!("error creating directory {path:?}: {e}"), + )); + } + Ok(file) => { + return Ok((file, path)); + } + } + } + } +} + +#[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", +))] +mod imp { + use std::{ + ffi::CString, + io, + os::{ + fd::{AsRawFd, RawFd}, + unix::ffi::OsStrExt, + }, + }; + + use super::*; + + #[cfg(target_os = "linux")] + fn path_after_unlink(fd: RawFd) -> PathBuf { + PathBuf::from(format!("/proc/self/fd/{fd}")) + } + + #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] + fn path_after_unlink(fd: RawFd) -> PathBuf { + PathBuf::from(format!("/dev/fd/{fd}")) + } + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, mut path) = general_imp::create(prefix, |_| {})?; + let mut delete_on_drop = true; + if let Ok(original_path) = CString::new(path.as_os_str().as_bytes()) { + // Unlinking the file will *not* remove it per the POSIX specification since it is open. + // We cannot use `std::fs::remove_file()`, since, while currently using `unlink()`, it does + // not guarantee it will use it. + if unsafe { libc::unlink(original_path.as_ptr()) } == 0 { + path = path_after_unlink(file.as_raw_fd()); + delete_on_drop = false; + } + } + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop }) + } +} + +#[cfg(windows)] +mod imp { + use std::os::windows::fs::OpenOptionsExt; + + use super::*; + + const FILE_ATTRIBUTE_TEMPORARY: u32 = 0x100; + const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x04000000; + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, path) = general_imp::create(prefix, |options| { + options.attributes(FILE_ATTRIBUTE_TEMPORARY); + options.custom_flags(FILE_FLAG_DELETE_ON_CLOSE); + })?; + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false }) + } +} + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + windows, +)))] +mod imp { + use super::*; + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, path) = general_imp::create(prefix, |_| {})?; + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true }) + } +} From 1c2b8a1f4b2efe219c438c160d0249c9653314ea Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sat, 22 Aug 2026 22:04:43 +0800 Subject: [PATCH 05/30] minor: offer 'add_return_type' after l_curly Example --- ```rust fn foo() {$0 45 } ``` **Before this PR** Assist not applicable **After this PR** ```rust fn foo() -> i32 { 45 } ``` --- .../src/handlers/add_return_type.rs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs index e7203a96bb218..453a1b26e08fd 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs @@ -161,7 +161,7 @@ fn extract_tail(ctx: &AssistContext<'_, '_>) -> Option<(FnType, ast::Expr, Inser let stmt_list = body.stmt_list()?; let tail_expr = stmt_list.tail_expr()?; - let ret_range_end = stmt_list.l_curly_token()?.text_range().start(); + let ret_range_end = stmt_list.l_curly_token()?.text_range().end(); let ret_range = TextRange::new(rparen_pos, ret_range_end); (FnType::Function, tail_expr, ret_range, action) } @@ -215,7 +215,7 @@ mod tests { #[test] fn infer_return_type_cursor_at_return_type_pos() { - cov_mark::check!(cursor_in_ret_position); + cov_mark::check_count!(cursor_in_ret_position, 3); check_assist( add_return_type, r#"fn foo() $0{ @@ -223,6 +223,24 @@ mod tests { }"#, r#"fn foo() -> i32 { 45 +}"#, + ); + check_assist( + add_return_type, + r#"fn foo()$0 { + 45 +}"#, + r#"fn foo() -> i32 { + 45 +}"#, + ); + check_assist( + add_return_type, + r#"fn foo() {$0 + 45 +}"#, + r#"fn foo() -> i32 { + 45 }"#, ); } From dee480fbca17b500110766048fa3c94e4f2e657a Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sat, 22 Aug 2026 21:16:59 +0800 Subject: [PATCH 06/30] fix: adds-arrow unmap ranges when fn inside macro Example --- ```rust macro_rules! identity { ($($t:tt)*) => {$($t)*}; } identity! { fn foo() u$0 } ``` **Before this PR** ```rust macro_r-> ules! identity { ($($t:tt)*) => {$($t)*}; } identity! { fn foo() u32 } ``` **After this PR** ```rust macro_rules! identity { ($($t:tt)*) => {$($t)*}; } identity! { fn foo() -> u32 } ``` --- .../crates/ide-completion/src/completions.rs | 4 ++- .../crates/ide-completion/src/context.rs | 9 +++-- .../crates/ide-completion/src/render.rs | 3 +- .../ide-completion/src/tests/type_pos.rs | 34 +++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs index f1a34f15d0a5b..0c5b658b07d74 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs @@ -752,7 +752,9 @@ pub(super) fn complete_name_ref<'db>( TypeLocation::TypeAscription(ascription) => { if let TypeAscriptionTarget::RetType { item: Some(item), .. } = ascription - && path_ctx.required_thin_arrow().is_some() + && path_ctx + .required_thin_arrow(|it| Some(it.text_range())) + .is_some() && matches!(path_ctx.qualified, Qualified::No) { keyword::complete_for_and_where(acc, ctx, &item.clone().into()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs index 705305f557e9c..aa6b9906ffc15 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs @@ -103,7 +103,10 @@ impl PathCompletionCtx<'_> { ) } - pub(crate) fn required_thin_arrow(&self) -> Option<(&'static str, TextSize)> { + pub(crate) fn required_thin_arrow( + &self, + unmap: impl Fn(&syntax::SyntaxNode) -> Option, + ) -> Option<(&'static str, TextSize)> { let PathKind::Type { location: TypeLocation::TypeAscription(TypeAscriptionTarget::RetType { @@ -119,8 +122,8 @@ impl PathCompletionCtx<'_> { } let ret_type = fn_item.ret_type().and_then(|it| it.ty()); match (ret_type, fn_item.param_list()) { - (Some(ty), _) => Some(("-> ", ty.syntax().text_range().start())), - (None, Some(param)) => Some((" ->", param.syntax().text_range().end())), + (Some(ty), _) => Some(("-> ", unmap(ty.syntax())?.start())), + (None, Some(param)) => Some((" ->", unmap(param.syntax())?.end())), (None, None) => None, } } diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs index 43b2a53a7f7ea..2534729bd4f65 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs @@ -651,7 +651,8 @@ fn adds_ret_type_arrow( item: &mut Builder, insert_text: String, ) { - if let Some((arrow, at)) = path_ctx.required_thin_arrow() { + let unmap = |node: &_| ctx.sema.original_range_opt(node).map(|it| it.range); + if let Some((arrow, at)) = path_ctx.required_thin_arrow(unmap) { let mut edit = TextEdit::builder(); edit.insert(at, arrow.to_owned()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs index ad058901c0473..419b15ed868b3 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs @@ -244,6 +244,40 @@ fn foo() -> foo::Num "#, ); + check_edit( + "u32", + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +identity! { + fn foo() u$0 +} +"#, + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +identity! { + fn foo() -> u32 +} +"#, + ); + + check_edit( + "Num", + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +mod foo { pub type Num = u32; } +identity! { + fn foo() foo::N$0 +} +"#, + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +mod foo { pub type Num = u32; } +identity! { + fn foo() -> foo::Num +} +"#, + ); + // no spaces, test edit order check_edit( "foo", From 04b65368308a6881066d878b4d231977c44d4e17 Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Mon, 24 Aug 2026 05:04:39 +0200 Subject: [PATCH 07/30] fix outdated comments --- src/tools/rust-analyzer/crates/hir-ty/src/infer.rs | 4 ---- src/tools/rust-analyzer/crates/hir-ty/src/lower.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs b/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs index 70539cf83673a..3fbb02aee94bd 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs @@ -1629,10 +1629,6 @@ impl<'db> InferenceContext<'db> { self.defined_anon_consts.borrow_mut().append(&mut defined_anon_consts); } - // FIXME: This function should be private in module. It is currently only used in the consteval, since we need - // `InferenceResult` in the middle of inference. See the fixme comment in `consteval::eval_to_const`. If you - // used this function for another workaround, mention it here. If you really need this function and believe that - // there is no problem in it being `pub(crate)`, remove this comment. fn resolve_all(self) -> InferenceResult<'db> { let InferenceContext { table, diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs b/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs index 5f7e6782fdd28..7733b49d32f78 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs @@ -788,7 +788,7 @@ impl<'db, 'a> TyLoweringContext<'db, 'a> { ) } - /// This is only for `generic_predicates_for_param`, where we can't just + /// This is only for [`resolve_type_param_assoc_type_shorthand`], where we can't just /// lower the self types of the predicates since that could lead to cycles. /// So we just check here if the `type_ref` resolves to a generic param, and which. fn lower_ty_only_param(&self, type_ref: TypeRefId) -> Option { From 4a21a99f3af81c2d2e59ffcb47e40a23b3571d2b Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Tue, 25 Aug 2026 01:13:58 +0800 Subject: [PATCH 08/30] Remove callback --- .../rust-analyzer/crates/ide-completion/src/completions.rs | 4 +--- src/tools/rust-analyzer/crates/ide-completion/src/context.rs | 3 ++- src/tools/rust-analyzer/crates/ide-completion/src/render.rs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs index 0c5b658b07d74..eec35cc4024e5 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs @@ -752,9 +752,7 @@ pub(super) fn complete_name_ref<'db>( TypeLocation::TypeAscription(ascription) => { if let TypeAscriptionTarget::RetType { item: Some(item), .. } = ascription - && path_ctx - .required_thin_arrow(|it| Some(it.text_range())) - .is_some() + && path_ctx.required_thin_arrow(&ctx.sema).is_some() && matches!(path_ctx.qualified, Qualified::No) { keyword::complete_for_and_where(acc, ctx, &item.clone().into()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs index aa6b9906ffc15..5935e16542e4a 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs @@ -105,7 +105,7 @@ impl PathCompletionCtx<'_> { pub(crate) fn required_thin_arrow( &self, - unmap: impl Fn(&syntax::SyntaxNode) -> Option, + sema: &Semantics<'_, RootDatabase>, ) -> Option<(&'static str, TextSize)> { let PathKind::Type { location: @@ -120,6 +120,7 @@ impl PathCompletionCtx<'_> { if fn_item.ret_type().is_some_and(|it| it.thin_arrow_token().is_some()) { return None; } + let unmap = |node: &_| sema.original_range_opt(node).map(|it| it.range); let ret_type = fn_item.ret_type().and_then(|it| it.ty()); match (ret_type, fn_item.param_list()) { (Some(ty), _) => Some(("-> ", unmap(ty.syntax())?.start())), diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs index 2534729bd4f65..de0a9a9174314 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs @@ -651,8 +651,7 @@ fn adds_ret_type_arrow( item: &mut Builder, insert_text: String, ) { - let unmap = |node: &_| ctx.sema.original_range_opt(node).map(|it| it.range); - if let Some((arrow, at)) = path_ctx.required_thin_arrow(unmap) { + if let Some((arrow, at)) = path_ctx.required_thin_arrow(&ctx.sema) { let mut edit = TextEdit::builder(); edit.insert(at, arrow.to_owned()); From c5c0004de5dae131c24e51bfa8f9ae0d840edb7c Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Mon, 24 Aug 2026 20:42:44 +0300 Subject: [PATCH 09/30] Remove non-longer-needed remnants from previous versions of the tt encoding --- src/tools/rust-analyzer/crates/tt/src/storage.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/tools/rust-analyzer/crates/tt/src/storage.rs b/src/tools/rust-analyzer/crates/tt/src/storage.rs index 150777cc39e55..ba7a661b5e8a8 100644 --- a/src/tools/rust-analyzer/crates/tt/src/storage.rs +++ b/src/tools/rust-analyzer/crates/tt/src/storage.rs @@ -12,9 +12,6 @@ use std::{assert_matches, collections::hash_map, fmt::Debug, hint::cold_path, me #[cfg(all(debug_assertions, not(miri)))] use std::cell::Cell; -#[cfg(not(all(debug_assertions, not(miri))))] -use std::mem::MaybeUninit; - use intern::Symbol; use rustc_hash::FxHashMap; use span::{Span, SpanAnchor, SyntaxContext, TextRange, TextSize}; @@ -105,7 +102,7 @@ struct UninitBuffer { #[cfg(all(debug_assertions, not(miri)))] buffer: Box<[u8]>, #[cfg(not(all(debug_assertions, not(miri))))] - buffer: Box<[MaybeUninit]>, + buffer: Box<[std::mem::MaybeUninit]>, } impl UninitBuffer { @@ -807,10 +804,6 @@ unsafe fn decode_extended_span( } } -// FIXME: It'll probably be better to ensure this ourselves via a `#[repr(C, align(4))]` wrapper, even though practically -// this holds for all 32- and 64-bit targets (Rust does not guarantee this). -const _: () = assert!(align_of::<*const *const str>() >= 4); // Needed for the tagging of idents. - unsafe fn decode_symbol<'a>( mut ptr: BufferReader<'a>, first_byte: u8, @@ -830,8 +823,6 @@ unsafe fn decode_symbol<'a>( (ptr, symbols[symbol_idx as usize].clone()) } -/// We need `MaybeUninit` to preserve provenance. -/// /// The returned `u32` is the length of the children *in bytes*, if we read a subtree. Otherwise it's zero. unsafe fn decode<'a>( mut ptr: BufferReader<'a>, From 1df776d1e361167699191559523cd1aedb4b5eb8 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Tue, 25 Aug 2026 11:47:08 +0100 Subject: [PATCH 10/30] fix: Panic on accessing numeric fields in unions When writing `foo.0` where `foo` is a union, we'd end up panicking with: Failed to make ast node `syntax::ast::generated::nodes::Name` from text mod 0; This is because `make::name()` requires a legal ident. In 480db310b60fb4ec20deecdd26a066e026c0b522 we added checks for numeric fields (i.e. `.0` instead of `.foo`) in several code paths but missed the union case. Rather than adding another check, just change the fix to never be offered when the field name isn't a valid identifier. Add a unit test for the union case too. AI disclosure: Code partly written by GPT-5.6, review and commit message by me. --- .../src/handlers/unresolved_field.rs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs index 682a8130a8822..26f5e45ea3420 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs @@ -85,6 +85,7 @@ fn field_fix(ctx: &DiagnosticsContext<'_, '_>, d: &hir::UnresolvedField<'_>) -> if !is_editable_crate(target_module.krate(ctx.sema.db), ctx.sema.db) || SyntaxKind::from_keyword(field_name, ctx.edition).is_some() + || !syntax::utils::is_identifier(field_name, ctx.edition) { return None; } @@ -148,11 +149,7 @@ fn add_field_to_struct_fix( Some(make::visibility_pub_crate()) }; - let field_name = match field_name.chars().next() { - Some(ch) if ch.is_numeric() => return None, - Some(_) => make::name(field_name), - None => return None, - }; + let field_name = make::name(field_name); let (offset, record_field) = record_field_layout( visibility, @@ -180,12 +177,7 @@ fn add_field_to_struct_fix( // Add a field list to the Unit Struct let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db)); - let field_name = match field_name.chars().next() { - // FIXME : See match arm below regarding tuple structs. - Some(ch) if ch.is_numeric() => return None, - Some(_) => make::name(field_name), - None => return None, - }; + let field_name = make::name(field_name); let visibility = if error_range.file_id == struct_range.file_id { None } else { @@ -524,6 +516,18 @@ fn main() {} ) } + #[test] + fn no_fix_when_indexed_on_union() { + check_no_fix( + r#" +union U { a: u32 } +fn main(u: U) { + u.0$0; +} +"#, + ) + } + #[test] fn no_fix_when_without_field() { check_no_fix( From 0ecada956386bc3c5d701d48ab3f53d337475cc2 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Tue, 25 Aug 2026 15:13:10 +0100 Subject: [PATCH 11/30] internal: Improve panic messages on invalid AST nodes Add backticks so it's easier to see exactly what the input was. --- src/tools/rust-analyzer/crates/syntax/src/ast/make.rs | 4 ++-- .../rust-analyzer/crates/syntax/src/syntax_editor/edits.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs index 9017bae474273..36f7b56e3cd2a 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs @@ -1383,7 +1383,7 @@ fn expr_from_text_with_edition + AstNode>(text: &str, edition Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make expr node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); @@ -1403,7 +1403,7 @@ fn ast_from_text_with_edition(text: &str, edition: Edition) -> N { Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make ast node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); diff --git a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs index 9fab8716b412f..35e9b8d2f87d0 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs @@ -528,7 +528,7 @@ mod tests { Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make ast node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); From bceece16f135c3862071c51ebabf7d4e42114e35 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Fri, 21 Aug 2026 17:07:54 +0100 Subject: [PATCH 12/30] fix: Panic on deref of unresolved aliases Previously, code like `let &(x, y) = unknown_var;` would produce a panic of the form: deref projection of non-dereferenceable ty PlaceTy { ... } Fix MIR lowering so we only project out of references if the type is a reference, and treat it as a MIR lowering error otherwise. AI disclosure: Code partly written by GPT-5.6, commit message and review entirely done by a human. --- .../hir-ty/src/mir/lower/pattern_matching.rs | 6 ++++++ .../crates/hir-ty/src/mir/lower/tests.rs | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs index bd1ad70fe6a14..44f410408a40a 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs @@ -491,6 +491,12 @@ impl<'db> MirLowerCtx<'_, 'db> { )? } Pat::Ref { pat, mutability: _ } => { + let ty = cond_place.ty(&self.result, &self.infcx, self.env).ty; + if !ty.is_ref() { + return Err(MirLowerError::TypeError( + "non reference type matched with reference pattern", + )); + } let cond_place = cond_place.project(ProjectionElem::Deref); self.pattern_match_inner(current, current_else, cond_place, *pat, mode)? } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs index 1f6aa5c926b15..e4f3b61be4556 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs @@ -49,3 +49,19 @@ fn foo() { "#, ); } + +#[test] +fn ref_pattern_on_unresolved_alias() { + lower_mir( + r#" +//- minicore: sized +trait Tr { + type A; +} + +fn f(x: T::A) { + let &() = x; +} +"#, + ); +} From 610f6ff258262a85cc1fe6fe5cd8ca300953ff04 Mon Sep 17 00:00:00 2001 From: phpont Date: Tue, 25 Aug 2026 14:18:17 -0300 Subject: [PATCH 13/30] fix: reinfer never type in array repeat expressions --- .../rust-analyzer/crates/hir-ty/src/infer/expr.rs | 6 +++++- .../crates/hir-ty/src/tests/never_type.rs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs b/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs index f247b517c541f..fc53d64a2f984 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs @@ -1329,7 +1329,11 @@ impl<'db> InferenceContext<'db> { } None => { let ty = self.table.next_ty_var(element.into()); - self.infer_expr(element, &Expectation::has_type(ty), ExprIsRead::Yes); + self.infer_expr_suptype_coerce_never( + element, + &Expectation::has_type(ty), + ExprIsRead::Yes, + ); ty } }; diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs index 1c5f8aa110463..e53f7503dac6b 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs @@ -114,6 +114,21 @@ fn test() { ); } +#[test] +fn array_repeat_never_can_be_reinferred() { + check_no_mismatches( + r#" +fn test() { + let y = [return; 2]; + match y { + [(1, _), (_, false)] => {} + [_, _] => {} + } +} +"#, + ); +} + #[test] fn match_no_arm() { check_types( From e732f6d3a834e69a858c0450681858e36a9b3cbe Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 21 Aug 2026 11:25:29 -0700 Subject: [PATCH 14/30] Add tests for `extract_variable` assist not applying to patterns. --- .../ide-assists/src/handlers/extract_variable.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs index c2c50b16de76f..8f6f772844a9a 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs @@ -952,6 +952,16 @@ fn foo() { check_assist_not_applicable(extract_variable, r#"fn main() { 1 + /* $0comment$0 */ 1; }"#); } + #[test] + fn dont_extract_in_pattern_with_selection() { + check_assist_not_applicable(extract_variable, r#"fn foo() { [].map(|$0bar$0| bar + 1) } "#); + } + + #[test] + fn dont_extract_in_pattern_without_selection() { + check_assist_not_applicable(extract_variable, r#"fn foo() { [].map(|b$0ar| bar + 1) } "#); + } + #[test] fn extract_var_expr_stmt() { cov_mark::check!(test_extract_var_expr_stmt); From aef7e8b5e1574efa70df6ffcc63d886870058471 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 21 Aug 2026 11:25:29 -0700 Subject: [PATCH 15/30] =?UTF-8?q?feat:=20Allow=20=E2=80=9CExtract=20variab?= =?UTF-8?q?le=E2=80=9D=20to=20be=20invoked=20on=20field=20names=20in=20rec?= =?UTF-8?q?ord=20expressions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change allows a “mistake” I make very often to succeed: putting the cursor on “foo” in `Struct { foo: bar() }` when I want to extract `let foo = bar();`. It is also groundwork for being able to extract multiple field expressions at once. --- .../src/handlers/extract_variable.rs | 100 +++++++++++++++++- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs index 8f6f772844a9a..a5239e03fcdd2 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs @@ -76,10 +76,16 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - if let Some(t) = ctx.token_at_offset().find(|it| it.kind() == T![;]) { t.parent().and_then(ast::ExprStmt::cast)?.syntax().clone() } else { - let expr = ancestors_at_offset(ctx.source_file().syntax(), ctx.offset()) - .next() - .and_then(ast::Expr::cast)?; - expr.syntax().ancestors().find_map(valid_target_expr(ctx))?.syntax().clone() + // Offer the assist only if the nearest syntax node is an expression, or a record + // field, or a record field’s name. This prevents the assist from appearing when + // it is unlikely to be relevant, such as when the cursor is in a pattern. + // (If we did not want to restrict it this way, we could just apply + // `valid_target_expr()` to all ancestors.) + let expr_or_field = ancestors_at_offset(ctx.source_file().syntax(), ctx.offset()) + .find(|it| !ast::NameRef::can_cast(it.kind())) + .and_then(either::Either::::cast)?; + + expr_or_field.syntax().ancestors().find_map(valid_target_expr(ctx))?.syntax().clone() } } else { match ctx.covering_element() { @@ -367,6 +373,11 @@ fn valid_target_expr(ctx: &AssistContext<'_, '_>) -> impl Fn(SyntaxNode) -> Opti let path_resolution = ctx.sema.resolve_path(&path_expr.path()?)?; like_const_value(ctx, path_resolution).then_some(path_expr.into()) } + SyntaxKind::RECORD_EXPR_FIELD => { + // If we are on `k` in `Struct { k: v }`, then extract `v`. + let record_field = ast::RecordExprField::cast(node)?; + record_field.expr() + } _ => ast::Expr::cast(node), } } @@ -1596,6 +1607,87 @@ struct S { foo: i32 } +fn main() { + let $0foo = 1 + 1; + S { foo } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { $0foo: 1 + 1,$0 } +} +"#, + r#" +struct S { + foo: i32 +} + +fn main() { + let $0foo = 1 + 1; + S { foo, } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field_name() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { f$0oo: 1 + 1 } +} +"#, + r#" +struct S { + foo: i32 +} + +fn main() { + let $0foo = 1 + 1; + S { foo } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field_colon() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { foo $0: 1 + 1 } +} +"#, + r#" +struct S { + foo: i32 +} + fn main() { let $0foo = 1 + 1; S { foo } From 75c1da39e0c540f76a8500208956cd5467f13bef Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Tue, 25 Aug 2026 23:20:59 +0300 Subject: [PATCH 16/30] Require rustc citation for analysis changes when using AI --- src/tools/rust-analyzer/AI_POLICY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tools/rust-analyzer/AI_POLICY.md b/src/tools/rust-analyzer/AI_POLICY.md index afe7f82a0a11c..77fca0e8eba5e 100644 --- a/src/tools/rust-analyzer/AI_POLICY.md +++ b/src/tools/rust-analyzer/AI_POLICY.md @@ -30,6 +30,14 @@ E-easy issues are usually easier for maintainers to just fix directly than write AI *may* be used to understand the codebase for E-easy+E-has-instructions contributions, but not to write any code. +When using AI to author changes to *analysis* - the code responsible for analyzing Rust code and not for implementing IDE features, including +but not limited to: type inference, MIR, name resolution, macro expansion - generally anything in the crates `parser`, `mbe`, `hir-expand`, `hir-def`, `hir-ty`, +although there are exceptions; **including when using AI only to analyze bugs and not to write code**, you are required to include a citation +of the rustc code responsible for the change you did, along with an explanation of how your change follows from it in case this is not immediately clear. + +The reason for that is that it is almost impossible to be fully correct in analysis if we implement things differently from rustc. We should not guess +how to fix bugs in analysis without looking at the rustc code. + This policy was adapted from [uv's AI policy]. [uv's AI policy]: https://github.com/astral-sh/.github/blob/c5187e200db51bfe11d56e13053d29bd3793fdd8/AI_POLICY.md From bbf7921d5b3d89582e6a0bc7e6068605ff2e49ed Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 26 Aug 2026 11:30:13 +0300 Subject: [PATCH 17/30] Change relationship to uv's AI policy from "adapted from" to "inspired by" We've diverged quite a bit by now. --- src/tools/rust-analyzer/AI_POLICY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/AI_POLICY.md b/src/tools/rust-analyzer/AI_POLICY.md index 77fca0e8eba5e..59db4a07b4fb5 100644 --- a/src/tools/rust-analyzer/AI_POLICY.md +++ b/src/tools/rust-analyzer/AI_POLICY.md @@ -38,6 +38,6 @@ of the rustc code responsible for the change you did, along with an explanation The reason for that is that it is almost impossible to be fully correct in analysis if we implement things differently from rustc. We should not guess how to fix bugs in analysis without looking at the rustc code. -This policy was adapted from [uv's AI policy]. +This policy was inspired by [uv's AI policy]. [uv's AI policy]: https://github.com/astral-sh/.github/blob/c5187e200db51bfe11d56e13053d29bd3793fdd8/AI_POLICY.md From 1c026c44490a2cf11caf2c8e6eb4ae738f7370c6 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 26 Aug 2026 16:27:51 +0100 Subject: [PATCH 18/30] fix: Panic when computing extract_variable with macros extract_variable assumed that expanding macros and then mapping them back to original source code would produce a span for a single expression. This isn't always true for macros with multiple arguments. If the macro expands and uses multiple arguments, we can end up trying to parse `x, y` from `foo!(x, y)`. Instead, don't offer the assist if we can't map back to a valid Rust expression. This also removes an .unwrap() call (added in 38b37c1f38f94a02098da6d4694b30e544a4ae69). AI disclosure: Code partly written by GPT-5.6, but commit message, comments and review by me. --- .../src/handlers/extract_variable.rs | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs index c2c50b16de76f..4dd36346ede81 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs @@ -95,7 +95,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - let node = node.ancestors().take_while(|anc| anc.text_range() == node.text_range()).last()?; let range = node.text_range(); - let (to_replace, analysis, use_source_expr) = if node.kind() == SyntaxKind::TOKEN_TREE { + let (to_replace, analysis, source_to_extract) = if node.kind() == SyntaxKind::TOKEN_TREE { let (first, last) = extract_token_range_of(&node, ctx.selection_trimmed())?; let first_descend = ctx.sema.descend_into_macros_single_exact(first.clone()); @@ -114,14 +114,16 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - if !node.text_range().contains_range(original_range.range) { return None; } - (cover_edit_range(&node, original_range.range), expr, true) + let to_replace = cover_edit_range(&node, original_range.range); + let source_to_extract = source_expr(ctx, to_replace.clone())?; + (to_replace, expr, Some(source_to_extract)) } else { let expr = node .descendants() .take_while(|it| range.contains_range(it.text_range())) .find_map(valid_target_expr(ctx))?; let to_extract = expr.syntax().syntax_element(); - (to_extract.clone()..=to_extract, expr, false) + (to_extract.clone()..=to_extract, expr, None) }; let place = match to_replace.start() { NodeOrToken::Node(node) => node.clone(), @@ -220,10 +222,9 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - editor.add_annotation(pat_name.syntax().clone(), tabstop); } - let to_extract_no_ref = if use_source_expr { - source_expr(ctx, to_replace.clone()).unwrap() - } else { - to_extract_no_ref.clone() + let to_extract_no_ref = match &source_to_extract { + Some(expr) => expr.clone(), + None => to_extract_no_ref.clone(), }; let initializer = match ty.as_ref().filter(|_| needs_ref) { Some(receiver_type) if receiver_type.is_mutable_reference() => { @@ -1383,6 +1384,46 @@ fn main() { ); } + #[test] + fn extract_var_in_macro_call_with_multiple_args() { + check_assist_not_applicable( + extract_variable, + r#" +macro_rules! m { + ($a:expr, $b:expr) => { $a + $b }; +} +fn f(x: u32) -> u32 { + m!($0x$0, 1) +} +"#, + ); + } + + #[test] + fn extract_var_in_macro_call_with_single_arg() { + check_assist_by_label( + extract_variable, + r#" +macro_rules! m { + ($e:expr) => { $e + 1 }; +} +fn f(x: u32) -> u32 { + m!($0x$0) +} +"#, + r#" +macro_rules! m { + ($e:expr) => { $e + 1 }; +} +fn f(x: u32) -> u32 { + let $0var_name = x; + m!(var_name) +} +"#, + "Extract into variable", + ); + } + #[test] fn extract_var_path_simple() { check_assist_by_label( From 8787b0cc7a9ba009179774396180c6379992fe43 Mon Sep 17 00:00:00 2001 From: Kivanc Gunalp Date: Wed, 26 Aug 2026 16:38:09 +0000 Subject: [PATCH 19/30] fix: strip leading asterisk decoration from block doc comments Adopts rustc's beautify_doc_string algorithm for hir-def's docs gathering so block doc comments render like rustdoc does. Splits the old push_doc_lines into a per-line push_doc_line plus a new push_doc_lines that mirrors beautify_doc_string: single-line input takes a fast path; multi-line input builds Vec<(&str, TextSize)> using str::lines() (matching rustc, correct \r\n handling), runs get_vertical_trim / get_horizontal_trim on a projected &[&str] view, strips the horizontal prefix (and an additional leading '*' when it's block decoration), then pushes each surviving line via push_doc_line so the source-map offsets stay accurate. get_vertical_trim and get_horizontal_trim are byte-for-byte copies of rustc's helpers, modulo the CommentKind -> CommentShape rename and returning String rather than interning to Symbol. Per-line byte offsets are computed by pointer arithmetic instead of str::substr_range because substr_range is stable since 1.98 and the workspace MSRV is 1.95. This matches what substr_range does internally. Doc attributes and macro-expanded doc strings route through push_doc_lines with CommentShape::Line, matching rustc which passes CommentKind::Line for those desugared cases. Adds hover tests covering block comments decorated with leading asterisks (with and without leading/trailing framing). --- .../crates/hir-def/src/attrs/docs.rs | 254 ++++++++++++++++-- .../crates/ide/src/hover/tests.rs | 27 ++ 2 files changed, 262 insertions(+), 19 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs index 3dc278fb1a703..08de18583f759 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs @@ -201,7 +201,8 @@ impl Docs { fn extend_with_doc_comment(&mut self, comment: ast::Comment, indent: &mut usize) { let Some((doc, offset)) = comment.doc_comment() else { return }; - self.extend_with_doc_str(doc, comment.syntax().text_range().start() + offset, indent); + let offset = comment.syntax().text_range().start() + offset; + self.extend_with_doc_str(doc, offset, indent, comment.kind().shape); } fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut usize) { @@ -209,7 +210,9 @@ impl Docs { let value_offset = value_offset.start(); let Ok(value) = value.value() else { return }; // FIXME: Handle source maps for escaped text. - self.extend_with_doc_str(&value, value_offset, indent); + // + // rustc passes `CommentKind::Line` for desugared `#[doc = "..."]` attributes. + self.extend_with_doc_str(&value, value_offset, indent, ast::CommentShape::Line); } pub(crate) fn extend_with_doc_str( @@ -217,30 +220,94 @@ impl Docs { doc: &str, offset_in_ast: TextSize, indent: &mut usize, + shape: ast::CommentShape, ) { - self.push_doc_lines(doc, Some(offset_in_ast), indent); + self.push_doc_lines(doc, Some(offset_in_ast), indent, shape); } fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut usize) { - self.push_doc_lines(doc, None, indent); + // Macro-expanded doc strings are desugared, so pass `CommentShape::Line` matching + // rustc's `CommentKind::Line`. + self.push_doc_lines(doc, None, indent, ast::CommentShape::Line); } - fn push_doc_lines(&mut self, doc: &str, mut ast_offset: Option, indent: &mut usize) { - for line in doc.split('\n') { - self.docs_source_map - .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset }); - if let Some(ref mut offset) = ast_offset { - *offset += TextSize::of(line) + TextSize::of("\n"); - } + /// Beautifies `doc` and appends the result to `self.docs`, one line at a time via + /// [`Docs::push_doc_line`]. Mirrors rustc's [`beautify_doc_string`], delegating to + /// [`get_vertical_trim`] and [`get_horizontal_trim`] for the multi-line case. + /// + /// Individual `///` line comments always reach us as a single-line `doc`, so the + /// `!doc.contains('\n')` fast path fires and the multi-line logic never runs on them. + /// Desugared `#[doc = "..."]` strings and macro-expanded docs also route through here + /// with `shape = CommentShape::Line`, matching rustc. + /// + /// Unlike rustc's version, which joins the beautified lines into a new interned `Symbol`, + /// this port pushes each line individually via [`Docs::push_doc_line`] and pairs it with + /// its byte offset relative to `doc`'s start so the source-map records accurate per-line + /// offsets. + /// + /// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L37 + fn push_doc_lines( + &mut self, + doc: &str, + ast_offset: Option, + indent: &mut usize, + shape: ast::CommentShape, + ) { + if !doc.contains('\n') { + self.push_doc_line(doc, ast_offset, indent); + return; + } + + let doc_start = doc.as_ptr() as usize; + let mut lines: Vec<(&str, TextSize)> = doc + .lines() + .map(|line| { + let offset = TextSize::new((line.as_ptr() as usize - doc_start) as u32); + (line, offset) + }) + .collect(); + + let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect(); + let lines = match get_vertical_trim(&raw_lines) { + Some((i, j)) => &mut lines[i..j], + None => &mut lines[..], + }; - let line = line.trim_end(); - if let Some(line_indent) = line.chars().position(|ch| !ch.is_whitespace()) { - // Empty lines are handled because `position()` returns `None` for them. - *indent = std::cmp::min(*indent, line_indent); + let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect(); + if let Some(horizontal) = get_horizontal_trim(&raw_lines, shape) { + let horizontal_len = TextSize::of(horizontal.as_str()); + // Strip `"[ \t]*\*"` from each line where present, exactly like rustc. + for (line, line_offset) in lines.iter_mut() { + if let Some(rest) = line.strip_prefix(horizontal.as_str()) { + *line = rest; + *line_offset += horizontal_len; + if shape == ast::CommentShape::Block + && (*line == "*" || line.starts_with("* ") || line.starts_with("**")) + { + *line = &line[1..]; + *line_offset += TextSize::of("*"); + } + } } - self.docs.push_str(line); - self.docs.push('\n'); } + + for (line, line_offset) in lines.iter().copied() { + self.push_doc_line(line, ast_offset.map(|it| it + line_offset), indent); + } + } + + /// Appends a single beautified line to `self.docs` and records its source-map row. + fn push_doc_line(&mut self, line: &str, ast_offset: Option, indent: &mut usize) { + self.docs_source_map + .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset }); + + let line = line.trim_end(); + if let Some(line_indent) = line.chars().position(|ch| !ch.is_whitespace()) { + // Empty lines are handled because `position()` returns `None` for them. + *indent = std::cmp::min(*indent, line_indent); + } + self.docs.push_str(line); + self.docs.push('\n'); } fn remove_indent(&mut self, indent: usize, start_source_map_index: usize) { @@ -347,6 +414,79 @@ impl Docs { } } +/// Copied verbatim from rustc's [`beautify_doc_string`], modulo `CommentKind`/`CommentShape` +/// renaming. +/// +/// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L38 +fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> { + let mut i = 0; + let mut j = lines.len(); + // first line of all-stars should be omitted + if lines.first().is_some_and(|line| line.chars().all(|c| c == '*')) { + i += 1; + } + + // like the first, a last line of all stars should be omitted + if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') { + j -= 1; + } + + if i != 0 || j != lines.len() { Some((i, j)) } else { None } +} + +/// Copied verbatim from rustc's [`beautify_doc_string`], modulo `CommentKind`/`CommentShape` +/// renaming and returning `String` rather than interning to `Symbol`. +/// +/// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L54 +fn get_horizontal_trim(lines: &[&str], kind: ast::CommentShape) -> Option { + let mut i = usize::MAX; + let mut first = true; + + // In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are + // present. However, we first need to strip the empty lines so they don't get in the middle + // when we try to compute the "horizontal trim". + let lines = match kind { + ast::CommentShape::Block => { + // Whatever happens, we skip the first line. + let mut i = lines + .first() + .map(|l| if l.trim_start().starts_with('*') { 0 } else { 1 }) + .unwrap_or(0); + let mut j = lines.len(); + + while i < j && lines[i].trim().is_empty() { + i += 1; + } + while j > i && lines[j - 1].trim().is_empty() { + j -= 1; + } + &lines[i..j] + } + ast::CommentShape::Line => lines, + }; + + for line in lines { + for (j, c) in line.chars().enumerate() { + if j > i || !"* \t".contains(c) { + return None; + } + if c == '*' { + if first { + i = j; + first = false; + } else if i != j { + return None; + } + break; + } + } + if i >= line.len() { + return None; + } + } + Some(lines.first()?[..i].to_string()) +} + struct DocMacroExpander<'db> { db: &'db dyn SourceDatabase, krate: Crate, @@ -587,6 +727,7 @@ pub(crate) fn extract_docs<'a, 'db>( mod tests { use expect_test::expect; use hir_expand::InFile; + use syntax::{AstToken, ast}; use test_fixture::WithFixture; use thin_vec::ThinVec; use tt::{TextRange, TextSize}; @@ -613,7 +754,7 @@ mod tests { let outer = " foo\n\tbar baz"; let mut ast_offset = TextSize::new(123); for line in outer.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent); + docs.extend_with_doc_str(line, ast_offset, &mut indent, ast::CommentShape::Line); ast_offset += TextSize::of(line) + TextSize::of("\n"); } @@ -621,7 +762,7 @@ mod tests { ast_offset += TextSize::new(123); let inner = " bar \n baz"; for line in inner.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent); + docs.extend_with_doc_str(line, ast_offset, &mut indent, ast::CommentShape::Line); ast_offset += TextSize::of(line) + TextSize::of("\n"); } @@ -762,4 +903,79 @@ mod tests { Some((in_file(range(263, 265)), IsInnerDoc::Yes)) ); } + + /// Extracts the docs of the first comment in `source`, running the same normalization as + /// [`super::extract_docs`] does for inline docs. + fn comment_docs(source: &str) -> Docs { + let (_db, file_id) = TestDB::with_single_file(""); + let comment = syntax::SourceFile::parse(source, span::Edition::CURRENT) + .syntax_node() + .descendants_with_tokens() + .filter_map(|it| it.into_token()) + .find_map(ast::Comment::cast) + .expect("no comment in the fixture"); + let mut docs = Docs { + docs: String::new(), + docs_source_map: Vec::new(), + outline_mod: None, + inline_file: file_id.into(), + prefix_len: TextSize::new(0), + inline_inner_docs_start: None, + outline_inner_docs_start: None, + macro_calls: ThinVec::new(), + }; + let mut indent = usize::MAX; + docs.extend_with_doc_comment(comment, &mut indent); + docs.remove_indent(indent, 0); + docs.remove_last_newline(); + docs + } + + #[test] + fn block_doc_comment_stars() { + #[track_caller] + fn check(source: &str, expect: expect_test::Expect) { + expect.assert_eq(&comment_docs(source).docs); + } + + // The decoration is stripped, but markdown bullets and `*foo` are content. + // `*bar` doesn't start with `* ` / `**`, so rustc's beautifier only strips the + // horizontal `[ \t]*` prefix (here a single space) and leaves the leading `*` in + // place. That in turn pins the block's minimum indent at 0, so surrounding lines + // aren't re-indented. + check( + "/**\n * foo\n *\n * * bullet\n *bar\n */", + expect![[r#" + foo + + * bullet + *bar + "#]], + ); + // Single-line block doc comments are left alone, like rustdoc does. + check("/** * item */", expect!["* item"]); + // So are blocks without a consistent star column. + check( + "/**\n * foo\n * bar\n */", + expect![[r#" + * foo + * bar + "#]], + ); + } + + #[test] + fn block_doc_comment_source_map() { + let docs = comment_docs("/**\n * foo\n * bar\n */"); + // `.lines()` (matching rustc) doesn't emit a leading empty entry for the newline + // right after `/**`, so the docs body starts at `foo`, not with a blank line. + assert_eq!(docs.docs, "foo\nbar\n"); + + let range = |start, end| TextRange::new(TextSize::new(start), TextSize::new(end)); + let in_file = |range| InFile::new(docs.inline_file, range); + let mapped = |start, end| docs.find_ast_range(range(start, end)); + // Both `foo` and `bar` map back past the stripped ` * ` decoration. + assert_eq!(mapped(0, 3), Some((in_file(range(7, 10)), IsInnerDoc::No))); + assert_eq!(mapped(4, 7), Some((in_file(range(14, 17)), IsInnerDoc::No))); + } } diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs index fad322aa4f5da..cc139a2ab90a6 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -5085,6 +5085,33 @@ fn foo$0() {} ); } +#[test] +fn hover_doc_block_style_leading_asterisks() { + check( + r#" +/** + * Some docs, *not a bullet*. + */ +fn foo$0() {} +"#, + expect![[r#" + *foo* + + ```rust + ra_test_fixture + ``` + + ```rust + fn foo() + ``` + + --- + + Some docs, *not a bullet*. + "#]], + ); +} + #[test] fn hover_comments_dont_highlight_parent() { cov_mark::check!(no_highlight_on_comment_hover); From f9cb5fd81f6d91b3805dfd0d9a3a03f92357eb14 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 26 Aug 2026 20:40:49 +0300 Subject: [PATCH 20/30] Re-use `substr_range()` in docs --- src/tools/rust-analyzer/.github/workflows/ci.yaml | 6 +----- src/tools/rust-analyzer/Cargo.toml | 2 +- src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/tools/rust-analyzer/.github/workflows/ci.yaml b/src/tools/rust-analyzer/.github/workflows/ci.yaml index ca9e9719add48..cc05c9e0101fe 100644 --- a/src/tools/rust-analyzer/.github/workflows/ci.yaml +++ b/src/tools/rust-analyzer/.github/workflows/ci.yaml @@ -206,11 +206,7 @@ jobs: - name: Install Rust toolchain run: | - # FIXME: Pin nightly due to a regression in miri on nightly-2026-02-12. - # See https://github.com/rust-lang/miri/issues/4855. - # Revert to plain `nightly` once this is fixed upstream. - rustup toolchain install nightly-2026-02-10 - rustup default nightly-2026-02-10 + rustup default nightly rustup component add miri # - name: Cache Dependencies diff --git a/src/tools/rust-analyzer/Cargo.toml b/src/tools/rust-analyzer/Cargo.toml index e53a82eb6f88f..d77e89df45e44 100644 --- a/src/tools/rust-analyzer/Cargo.toml +++ b/src/tools/rust-analyzer/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/proc-macro-srv/proc-macro-test/imp"] resolver = "2" [workspace.package] -rust-version = "1.95" +rust-version = "1.98" edition = "2024" license = "MIT OR Apache-2.0" authors = ["rust-analyzer team"] diff --git a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs index 08de18583f759..d1bc92bc500bf 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs @@ -258,11 +258,10 @@ impl Docs { return; } - let doc_start = doc.as_ptr() as usize; let mut lines: Vec<(&str, TextSize)> = doc .lines() .map(|line| { - let offset = TextSize::new((line.as_ptr() as usize - doc_start) as u32); + let offset = TextSize::new(doc.substr_range(line).unwrap().start as u32); (line, offset) }) .collect(); From 1cbc439859e8f8e4f0780ad05800a8eb94b8cf61 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 26 Aug 2026 06:32:00 +0300 Subject: [PATCH 21/30] Fix some subtle bugs in docs rendering Match the behavior of rustdoc *precisely*: - Fix handling of mixed sugared and desugared docs. - Only trim spaces and tabs for indentation. --- .../crates/hir-def/src/attrs/docs.rs | 188 ++++++++++++++---- .../crates/ide/src/hover/tests.rs | 2 +- .../test_data/highlight_doctest.html | 6 +- 3 files changed, 151 insertions(+), 45 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs index 08de18583f759..5d2d0faed6861 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs @@ -31,7 +31,7 @@ use tt::{TextRange, TextSize}; use crate::{macro_call_as_call_id, nameres::MacroSubNs, resolver::Resolver}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) struct DocsSourceMapLine { +struct DocsSourceMapLine { /// The offset in [`Docs::docs`]. string_offset: TextSize, /// The offset in the AST of the text. `None` for macro-expanded doc strings @@ -62,6 +62,20 @@ pub struct Docs { macro_calls: ThinVec<(AstId, MacroCallId)>, } +#[derive(Clone, Copy)] +enum DocCommentKind { + /// `///` etc.. + Sugared(ast::CommentShape), + /// `#[doc = ""]`. + Desugared, +} + +#[derive(Default)] +struct Indent { + lines: Vec>, + seen_sugared: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IsInnerDoc { No, @@ -199,36 +213,37 @@ impl Docs { )); } - fn extend_with_doc_comment(&mut self, comment: ast::Comment, indent: &mut usize) { + fn extend_with_doc_comment(&mut self, comment: ast::Comment, indent: &mut Indent) { let Some((doc, offset)) = comment.doc_comment() else { return }; let offset = comment.syntax().text_range().start() + offset; - self.extend_with_doc_str(doc, offset, indent, comment.kind().shape); + self.extend_with_doc_str( + doc, + offset, + DocCommentKind::Sugared(comment.kind().shape), + indent, + ); } - fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut usize) { + fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut Indent) { let Some(value_offset) = value.text_range_between_quotes() else { return }; let value_offset = value_offset.start(); let Ok(value) = value.value() else { return }; // FIXME: Handle source maps for escaped text. - // - // rustc passes `CommentKind::Line` for desugared `#[doc = "..."]` attributes. - self.extend_with_doc_str(&value, value_offset, indent, ast::CommentShape::Line); + self.extend_with_doc_str(&value, value_offset, DocCommentKind::Desugared, indent); } - pub(crate) fn extend_with_doc_str( + fn extend_with_doc_str( &mut self, doc: &str, offset_in_ast: TextSize, - indent: &mut usize, - shape: ast::CommentShape, + comment_kind: DocCommentKind, + indent: &mut Indent, ) { - self.push_doc_lines(doc, Some(offset_in_ast), indent, shape); + self.push_doc_lines(doc, Some(offset_in_ast), comment_kind, indent); } - fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut usize) { - // Macro-expanded doc strings are desugared, so pass `CommentShape::Line` matching - // rustc's `CommentKind::Line`. - self.push_doc_lines(doc, None, indent, ast::CommentShape::Line); + fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut Indent) { + self.push_doc_lines(doc, None, DocCommentKind::Desugared, indent); } /// Beautifies `doc` and appends the result to `self.docs`, one line at a time via @@ -250,11 +265,21 @@ impl Docs { &mut self, doc: &str, ast_offset: Option, - indent: &mut usize, - shape: ast::CommentShape, + comment_kind: DocCommentKind, + indent: &mut Indent, ) { + // Note: this is pushed even if there are only empty lines here, because that's what rustdoc does. + let shape = match comment_kind { + DocCommentKind::Sugared(shape) => { + indent.seen_sugared = true; + shape + } + // rustc uses `Line` for desugared comments. + DocCommentKind::Desugared => ast::CommentShape::Line, + }; + if !doc.contains('\n') { - self.push_doc_line(doc, ast_offset, indent); + self.push_doc_line(doc, ast_offset, comment_kind, indent); return; } @@ -292,25 +317,35 @@ impl Docs { } for (line, line_offset) in lines.iter().copied() { - self.push_doc_line(line, ast_offset.map(|it| it + line_offset), indent); + self.push_doc_line(line, ast_offset.map(|it| it + line_offset), comment_kind, indent); } } /// Appends a single beautified line to `self.docs` and records its source-map row. - fn push_doc_line(&mut self, line: &str, ast_offset: Option, indent: &mut usize) { + fn push_doc_line( + &mut self, + line: &str, + ast_offset: Option, + comment_kind: DocCommentKind, + indent: &mut Indent, + ) { self.docs_source_map .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset }); let line = line.trim_end(); - if let Some(line_indent) = line.chars().position(|ch| !ch.is_whitespace()) { - // Empty lines are handled because `position()` returns `None` for them. - *indent = std::cmp::min(*indent, line_indent); - } + let line_indent = if line.chars().any(|ch| !ch.is_whitespace()) { + // Empty lines are handled because `any()` returns `false` for them. + let line_indent = line.bytes().take_while(|c| *c == b' ' || *c == b'\t').count(); + Some((line_indent, comment_kind)) + } else { + None + }; + indent.lines.push(line_indent); self.docs.push_str(line); self.docs.push('\n'); } - fn remove_indent(&mut self, indent: usize, start_source_map_index: usize) { + fn remove_indent(&mut self, indent: &Indent) { /// In case of panics, we want to avoid corrupted UTF-8 in `self.docs`, so we clear it. struct Guard<'a>(&'a mut Docs); impl Drop for Guard<'_> { @@ -338,8 +373,38 @@ impl Docs { return; } + // `add` is used in case the most common sugared doc syntax is used ("/// "). The other + // fragments kind's lines are never starting with a whitespace unless they are using some + // markdown formatting requiring it. Therefore, if the doc block have a mix between the two, + // we need to take into account the fact that the minimum indent minus one (to take this + // whitespace into account). + // + // For example: + // + // /// hello! + // #[doc = "another"] + // + // In this case, you want "hello! another" and not "hello! another". + let add_indent = if indent.seen_sugared { 1 } else { 0 }; + + let Some(min_indent) = indent + .lines + .iter() + .filter_map(|it| *it) + .map(|(line_indent, line_kind)| { + line_indent + + match line_kind { + DocCommentKind::Sugared(_) => 0, + DocCommentKind::Desugared => add_indent, + } + }) + .min() + else { + return; + }; + let guard = Guard(self); - let source_map = &mut guard.0.docs_source_map[start_source_map_index..]; + let source_map = guard.0.docs_source_map.as_mut_slice(); let Some(&DocsSourceMapLine { string_offset: mut copy_into, .. }) = source_map.first() else { return; @@ -356,7 +421,14 @@ impl Docs { let line_docs = &guard.0.docs[TextRange::new(line_source.string_offset, string_end_offset)]; let line_docs_len = TextSize::of(line_docs); - let indent_size = line_docs.char_indices().nth(indent).map_or_else( + let indent_size = if let Some((_, DocCommentKind::Desugared)) = indent.lines[idx] + && min_indent > 0 + { + min_indent - add_indent + } else { + min_indent + }; + let indent_size = line_docs.char_indices().nth(indent_size).map_or_else( || TextSize::of(line_docs) - TextSize::of("\n"), |(offset, _)| TextSize::new(offset as u32), ); @@ -582,7 +654,7 @@ fn extend_with_attrs<'a, 'db>( node: &SyntaxNode, file_id: HirFileId, expect_inner_attrs: bool, - indent: &mut usize, + indent: &mut Indent, get_cfg_options: &dyn Fn() -> &'a CfgOptions, cfg_options: &mut Option<&'a CfgOptions>, make_resolver: &dyn Fn() -> Resolver<'db>, @@ -665,8 +737,8 @@ pub(crate) fn extract_docs<'a, 'db>( let mut cfg_options = None; + let mut indent = Indent::default(); if let Some(outer_mod_decl) = outer_mod_decl { - let mut indent = usize::MAX; // For outer docs (the `mod foo;` declaration), use the module's own resolver. extend_with_attrs( &mut result, @@ -680,12 +752,9 @@ pub(crate) fn extract_docs<'a, 'db>( &mut cfg_options, resolver, ); - result.remove_indent(indent, 0); result.outline_mod = Some((outer_mod_decl.file_id, result.docs_source_map.len())); } - let inline_source_map_start = result.docs_source_map.len(); - let mut indent = usize::MAX; // For inline docs, use the item's own resolver. extend_with_attrs( &mut result, @@ -714,7 +783,7 @@ pub(crate) fn extract_docs<'a, 'db>( resolver, ); } - result.remove_indent(indent, inline_source_map_start); + result.remove_indent(&indent); result.remove_last_newline(); @@ -734,7 +803,7 @@ mod tests { use crate::test_db::TestDB; - use super::{Docs, IsInnerDoc}; + use super::{DocCommentKind, Docs, Indent, IsInnerDoc}; #[test] fn docs() { @@ -749,12 +818,17 @@ mod tests { outline_inner_docs_start: None, macro_calls: ThinVec::new(), }; - let mut indent = usize::MAX; + let mut indent = Indent::default(); let outer = " foo\n\tbar baz"; let mut ast_offset = TextSize::new(123); for line in outer.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent, ast::CommentShape::Line); + docs.extend_with_doc_str( + line, + ast_offset, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); ast_offset += TextSize::of(line) + TextSize::of("\n"); } @@ -762,11 +836,15 @@ mod tests { ast_offset += TextSize::new(123); let inner = " bar \n baz"; for line in inner.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent, ast::CommentShape::Line); + docs.extend_with_doc_str( + line, + ast_offset, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); ast_offset += TextSize::of(line) + TextSize::of("\n"); } - assert_eq!(indent, 1); expect![[r#" [ DocsSourceMapLine { @@ -797,7 +875,7 @@ mod tests { "#]] .assert_debug_eq(&docs.docs_source_map); - docs.remove_indent(indent, 0); + docs.remove_indent(&indent); assert_eq!(docs.inline_inner_docs_start, Some(TextSize::new(13))); @@ -904,6 +982,34 @@ mod tests { ); } + #[test] + fn sugared_desugared_mix() { + let (_db, file_id) = TestDB::with_single_file(""); + let mut docs = Docs { + docs: String::new(), + docs_source_map: Vec::new(), + outline_mod: None, + inline_file: file_id.into(), + prefix_len: TextSize::new(0), + inline_inner_docs_start: None, + outline_inner_docs_start: None, + macro_calls: ThinVec::new(), + }; + let mut indent = Indent::default(); + + docs.push_doc_lines( + " hello!", + None, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); + docs.push_doc_lines("another", None, DocCommentKind::Desugared, &mut indent); + docs.remove_indent(&indent); + docs.remove_last_newline(); + + assert_eq!(docs.docs(), "hello!\nanother"); + } + /// Extracts the docs of the first comment in `source`, running the same normalization as /// [`super::extract_docs`] does for inline docs. fn comment_docs(source: &str) -> Docs { @@ -924,9 +1030,9 @@ mod tests { outline_inner_docs_start: None, macro_calls: ThinVec::new(), }; - let mut indent = usize::MAX; + let mut indent = Indent::default(); docs.extend_with_doc_comment(comment, &mut indent); - docs.remove_indent(indent, 0); + docs.remove_indent(&indent); docs.remove_last_newline(); docs } diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs index cc139a2ab90a6..54152efe065ea 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -2626,7 +2626,7 @@ fn bar() { fo$0o(); } --- - \<- ` ` here +  \<- ` ` here "#]], ); } diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html index c95b36a1b4f33..ede25fc2050e8 100644 --- a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html @@ -161,18 +161,18 @@ } /// ```rust -/// let _ = example(&[1, 2, 3]); +/// let _ = example(&[1, 2, 3]); /// ``` /// /// ``` -/// loop {} +/// loop {} #[cfg_attr(not(feature = "false"), doc = "loop {}")] #[doc = "loop {}"] /// ``` /// #[cfg_attr(feature = "alloc", doc = "```rust")] #[cfg_attr(not(feature = "alloc"), doc = "```ignore")] -/// let _ = example(&alloc::vec![1, 2, 3]); +/// let _ = example(&alloc::vec![1, 2, 3]); /// ``` pub fn mix_and_match() {} From 15bea686cebaa1a754824a8ca3482823fd301564 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 26 Aug 2026 21:15:16 +0300 Subject: [PATCH 22/30] Add `rustup toolchain install nightly` to Miri CI --- src/tools/rust-analyzer/.github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/rust-analyzer/.github/workflows/ci.yaml b/src/tools/rust-analyzer/.github/workflows/ci.yaml index cc05c9e0101fe..14582f8405f55 100644 --- a/src/tools/rust-analyzer/.github/workflows/ci.yaml +++ b/src/tools/rust-analyzer/.github/workflows/ci.yaml @@ -206,6 +206,7 @@ jobs: - name: Install Rust toolchain run: | + rustup toolchain install nightly rustup default nightly rustup component add miri From 8e8e72cccb94ee2fbcca9736ea2e7fb879d5a75d Mon Sep 17 00:00:00 2001 From: dfireBird Date: Wed, 26 Aug 2026 07:28:27 +0530 Subject: [PATCH 23/30] fix: range pattern inside a parenthesis parsed as tuple pattern --- .../crates/parser/src/grammar/patterns.rs | 10 ++- .../test_data/parser/inline/ok/range_pat.rast | 73 ++++++++++++++++++- .../test_data/parser/inline/ok/range_pat.rs | 7 ++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs b/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs index f8be75b1787f0..fdc3f8c491eaf 100644 --- a/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs +++ b/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs @@ -66,6 +66,12 @@ fn pattern_r(p: &mut Parser<'_>, recovery_set: TokenSet) { fn pattern_single_r(p: &mut Parser<'_>, recovery_set: TokenSet) { // test range_pat // fn main() { + // match () { + // (..1) => (), + // (..=3) => (), + // (..2 | 4) => (), + // } + // // match 92 { // 0 ... 100 => (), // 101 ..= 200 => (), @@ -97,6 +103,7 @@ fn pattern_single_r(p: &mut Parser<'_>, recovery_set: TokenSet) { // (1.., _) => (), // (..=2, _) => (), // } + // // } if p.at(T![..=]) { @@ -484,8 +491,7 @@ fn tuple_pat(p: &mut Parser<'_>) -> CompletedMarker { p.error("expected a pattern"); break; } - has_rest |= p.at(T![..]); - + has_rest |= !p.at(T![..=]) && p.at(T![..]) && !RANGE_PAT_END_FIRST.contains(p.nth(2)); pattern(p); if !p.at(T![')']) { has_comma = true; diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast index d9981c50719f3..ba0198e9d542c 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast @@ -12,6 +12,77 @@ SOURCE_FILE STMT_LIST L_CURLY "{" WHITESPACE "\n " + EXPR_STMT + MATCH_EXPR + MATCH_KW "match" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + WHITESPACE " " + MATCH_ARM_LIST + L_CURLY "{" + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + RANGE_PAT + DOT2 ".." + LITERAL_PAT + LITERAL + INT_NUMBER "1" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + RANGE_PAT + DOT2EQ "..=" + LITERAL_PAT + LITERAL + INT_NUMBER "3" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + OR_PAT + RANGE_PAT + DOT2 ".." + LITERAL_PAT + LITERAL + INT_NUMBER "2" + WHITESPACE " " + PIPE "|" + WHITESPACE " " + LITERAL_PAT + LITERAL + INT_NUMBER "4" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + R_CURLY "}" + WHITESPACE "\n\n " EXPR_STMT MATCH_EXPR MATCH_KW "match" @@ -468,6 +539,6 @@ SOURCE_FILE COMMA "," WHITESPACE "\n " R_CURLY "}" - WHITESPACE "\n" + WHITESPACE "\n\n" R_CURLY "}" WHITESPACE "\n" diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs index b54354211d2dc..69e2da2cf046c 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs @@ -1,4 +1,10 @@ fn main() { + match () { + (..1) => (), + (..=3) => (), + (..2 | 4) => (), + } + match 92 { 0 ... 100 => (), 101 ..= 200 => (), @@ -30,4 +36,5 @@ fn main() { (1.., _) => (), (..=2, _) => (), } + } From d0f5f03307a6b0d88de966da0ad2e281c0993cf5 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Fri, 28 Aug 2026 12:15:24 +0300 Subject: [PATCH 24/30] Print the ABI for fn pointers (if not the default) --- .../crates/hir-ty/src/display.rs | 12 +++++----- .../src/handlers/invalid_cast.rs | 2 +- .../crates/ide/src/hover/tests.rs | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs index 9bb0e5b66beeb..b47baff23ad8a 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs @@ -1974,12 +1974,12 @@ impl<'db> HirDisplay<'db> for PolyFnSig<'db> { if let Safety::Unsafe = fn_sig_kind.safety() { write!(f, "unsafe ")?; } - // FIXME: Enable this when the FIXME on FnAbi regarding PartialEq is fixed. - // if !matches!(abi, FnAbi::Rust) { - // f.write_str("extern \"")?; - // f.write_str(abi.as_str())?; - // f.write_str("\" ")?; - // } + let abi = self.abi(); + if !matches!(abi, ExternAbi::Rust) { + f.write_str("extern \"")?; + f.write_str(abi.as_str())?; + f.write_str("\" ")?; + } write!(f, "fn(")?; f.write_joined(inputs_and_output.inputs(), ", ")?; if fn_sig_kind.c_variadic() { diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs index e1c2053289b96..87647a84cc7ce 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs @@ -230,7 +230,7 @@ fn foo(_x: isize) { } fn main() { let v: u64 = 5; let x = foo as extern "C" fn() -> isize; - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `fn foo(isize)` as `fn() -> isize` + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `fn foo(isize)` as `extern "C" fn() -> isize` let y = v as extern "Rust" fn(isize) -> (isize, isize); //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `u64` as `fn(isize) -> (isize, isize)` y(x()); diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs index cc139a2ab90a6..910ac52ab05cb 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -11997,3 +11997,27 @@ fn main() { let _ = resolved.as_array(db); }); } + +#[test] +fn extern_c_fn_ptr_display() { + check( + r#" +extern "C" fn foo() {} + +fn bar() { + let v$0 = foo as extern "C" fn(); +} + "#, + expect![[r#" + *v* + + ```rust + let v: extern "C" fn() + ``` + + --- + + size = 8, align = 8, niches = 1, no Drop + "#]], + ); +} From e72874dd47206c283767810dfb372fde27ce596b Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Fri, 28 Aug 2026 17:33:59 +0100 Subject: [PATCH 25/30] fix: Panic when hovering a dyn trait with a binder Calling Binder::skip_binder() (specifically in write_bounds_like_dyn_trait) gives you a representation of a type that has escaping bound variables. https://github.com/rust-lang/rust/blob/17fd5b8a37b6667b6cc137f3cc35f09759768a3b/compiler/rustc_type_ir/src/binder.rs#L105 Unfortunately, we then call functions that specifically assert there are no escaping bound variables. We then panic in both hover logic and SCIP generation (e.g. for wasm-tools, starlark-rust, or gluon). See the unit test, which currently fails an assert in debug mode: thread 'tests::display_source_code::render_dyn_ty_under_enclosing_binder' (43348037) panicked at /Users/wilfred/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ra-ap-rustc_type_ir-0.166.0/src/predicate.rs:522:9: assertion failed: !self_ty.has_escaping_bound_vars() and fails with a different assert in release mode: thread 'tests::display_source_code::render_dyn_ty_under_enclosing_binder' (43398166) panicked at crates/hir-ty/src/next_solver/predicate.rs:565:9: `OutlivesPredicate(dyn [Binder { value: Trait(ExistentialTraitRef("Fn"[[()]])), bound_vars: [] }, Binder { value: Projection(ExistentialProjection { def_id: TypeAliasId("Output"), args: [()], term: &'^1_0.Named(FunctionId("test")) u8, .. }), bound_vars: [] }] + 'static, 'static)` has escaping bound vars, so it cannot be wrapped in a dummy binder. Since we're only displaying the type, we can safely use a dummy self_ty when rendering type predicates. (Note that self_ty here refers to the subject of a predicate, i.e. T in `T: Foo`, and isn't necessarily a `Self` type.) This matches rustc, which also has a dummy self in its pretty printer: https://github.com/rust-lang/rust/blob/e7d6a0776b06673b5a6258bd7476f1f39ab86756/compiler/rustc_middle/src/ty/print/pretty.rs#L1417 AI disclosure: Code partly written with GPT-5.6 Sol, but commit message and review by me. --- .../rust-analyzer/crates/hir-ty/src/display.rs | 8 +++++--- .../crates/hir-ty/src/tests/display_source_code.rs | 13 +++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs index b47baff23ad8a..dfa088830543f 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs @@ -1709,11 +1709,13 @@ impl<'db> HirDisplay<'db> for Ty<'db> { write!(f, "?c.{}", ty.var.as_usize())? } TyKind::Dynamic(bounds, region) => { + let self_ty = interner.default_types().types.dyn_trait_dummy_self; + // We want to put auto traits after principal traits, regardless of their written order. let mut bounds_to_display = SmallVec::<[_; 4]>::new(); let mut auto_trait_bounds = SmallVec::<[_; 4]>::new(); for bound in bounds.iter() { - let clause = bound.with_self_ty(interner, *self); + let clause = bound.with_self_ty(interner, self_ty); match bound.skip_binder() { ExistentialPredicate::Trait(_) | ExistentialPredicate::Projection(_) => { bounds_to_display.push(clause); @@ -1725,13 +1727,13 @@ impl<'db> HirDisplay<'db> for Ty<'db> { if f.render_region(region) { bounds_to_display - .push(rustc_type_ir::OutlivesPredicate(*self, region).upcast(interner)); + .push(rustc_type_ir::OutlivesPredicate(self_ty, region).upcast(interner)); } write_bounds_like_dyn_trait_with_prefix( f, "dyn", - Either::Left(*self), + Either::Left(self_ty), &bounds_to_display, SizedByDefault::NotSized, trait_bounds_need_parens, diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs index fe7327134903e..efbb49b0eeb81 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs @@ -90,6 +90,19 @@ fn foo(foo: &dyn for<'a> Foo<'a>) {} ); } +#[test] +fn render_dyn_ty_under_enclosing_binder() { + check_types_source_code( + r#" +//- minicore: fn +fn test(f: impl for<'b> Fn(&dyn Fn() -> &'b u8)) { + f; + //^ impl Fn(&(dyn Fn() -> &u8 + 'static)) +} +"#, + ); +} + #[test] fn sized_bounds_apit() { check_types_source_code( From eb870880ea932e6f788c87e72dfa54235265f6e6 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Sun, 30 Aug 2026 03:34:23 +0300 Subject: [PATCH 26/30] Fix `NamedTempFile` - Use `OpenOptions::write(true)`, otherwise it always fails. - If unable to copy the proc macro DLL in the proc macro server, use the original instead to at least succeed. - For Cargo.lock, use a temporary directory instead as Cargo only accepts literally-named Cargo.lock files, and also because `/proc/self/fd` does not work for other processes. Fortunately unlike the proc macro server we do run `Drop` for those. --- .../crates/proc-macro-srv/src/dylib.rs | 27 ++++++---- .../project-model/src/cargo_config_file.rs | 13 +++-- .../rust-analyzer/crates/stdx/src/tempfile.rs | 49 ++++++++++++++----- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs index 2a5e79d9e5cc5..8e9335df19b77 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs @@ -27,7 +27,7 @@ impl Expander { let lib = lib.canonicalize_utf8()?; let modified_time = fs::metadata(&lib).and_then(|it| it.modified())?; - let file = ensure_file_with_lock_free_access(lib)?; + let file = ensure_file_with_lock_free_access(lib); let library = ProcMacroLibrary::open(file.path())?; Ok(Expander { inner: library, modified_time, _file: file }) @@ -88,19 +88,28 @@ impl ProcMacroLibrary { /// Copy the dylib to temp directory to prevent locking in Windows #[cfg(windows)] -fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> io::Result { +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> NamedTempFile { if std::env::var("RA_DONT_COPY_PROC_MACRO_DLL").is_ok() { - return Ok(NamedTempFile::from_path(path.into_std_path_buf())); + return NamedTempFile::from_path(path.into_std_path_buf()); } - let file_name = path.file_stem().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, format!("File path is invalid: {path}")) - })?; + (|| { + let file_name = path.file_stem().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, format!("File path is invalid: {path}")) + })?; - NamedTempFile::new_from_existing(&format!("proc-macro-srv-{file_name}.dll"), path.as_std_path()) + NamedTempFile::new_from_existing( + &format!("proc-macro-srv-{file_name}.dll"), + path.as_std_path(), + ) + }) + .unwrap_or_else(|err| { + tracing::warn!("failed to create temporary file: {err}"); + NamedTempFile::from_path(path.into_std_path_buf()) + }) } #[cfg(unix)] -fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> io::Result { - Ok(NamedTempFile::from_path(path.into_std_path_buf())) +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> NamedTempFile { + NamedTempFile::from_path(path.into_std_path_buf()) } diff --git a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs index 976f5ddf8abba..126c0b41bb9f4 100644 --- a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs +++ b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs @@ -1,7 +1,7 @@ //! Read `.cargo/config.toml` as a TOML table use paths::{AbsPath, Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; -use stdx::tempfile::NamedTempFile; +use stdx::tempfile::NamedTempDir; use toml::{ Spanned, de::{DeTable, DeValue}, @@ -140,7 +140,7 @@ impl<'a> CargoConfigFileReader<'a> { pub(crate) struct LockfileCopy { pub(crate) path: Utf8PathBuf, pub(crate) usage: LockfileUsage, - _temp_file: NamedTempFile, + _temp_dir: NamedTempDir, } pub(crate) enum LockfileUsage { @@ -194,12 +194,11 @@ pub(crate) fn make_lockfile_copy( return None; }; - let temp_file = - NamedTempFile::new_from_existing("rust-analyzer-Cargo.lock", lockfile_path.as_std_path()) - .ok()?; - let path = Utf8Path::from_path(temp_file.path())?.to_path_buf(); + let temp_dir = NamedTempDir::new("rust-analyzer").ok()?; + let path = temp_dir.path().join("Cargo.lock"); + std::fs::copy(lockfile_path.as_std_path(), &path).ok()?; - Some(LockfileCopy { path, usage, _temp_file: temp_file }) + Some(LockfileCopy { path: Utf8PathBuf::from_path_buf(path).ok()?, usage, _temp_dir: temp_dir }) } #[test] diff --git a/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs index b67d4c46c39a3..fe9ae83ef539e 100644 --- a/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs +++ b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs @@ -45,20 +45,44 @@ impl Drop for NamedTempFile { } } +pub struct NamedTempDir { + path: PathBuf, +} + +impl NamedTempDir { + pub fn new(prefix: &str) -> io::Result { + general_imp::create(prefix, |_options, path| std::fs::create_dir(path)) + .map(|((), path)| NamedTempDir { path }) + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for NamedTempDir { + fn drop(&mut self) { + if std::fs::remove_dir_all(&self.path).is_err() { + tracing::info!("cannot remove temporary directory {}", self.path.display()); + } + } +} + mod general_imp { use std::{ - fs::{File, OpenOptions}, + fs::OpenOptions, io::{self, ErrorKind}, - path::PathBuf, + path::{Path, PathBuf}, sync::atomic::{AtomicU32, Ordering}, }; static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0); - pub(super) fn create( + pub(super) fn create( prefix: &str, - mut options_callback: impl FnMut(&mut OpenOptions), - ) -> io::Result<(File, PathBuf)> { + mut create: impl FnMut(OpenOptions, &Path) -> io::Result, + ) -> io::Result<(T, PathBuf)> { let temp_dir = std::env::temp_dir().canonicalize()?; let pid = std::process::id(); loop { @@ -68,8 +92,7 @@ mod general_imp { )); let mut open_options = OpenOptions::new(); open_options.create_new(true); - options_callback(&mut open_options); - match open_options.open(&path) { + match create(open_options, &path) { Err(e) if e.kind() == ErrorKind::AlreadyExists => {} Err(e) => { return Err(io::Error::new( @@ -114,7 +137,7 @@ mod imp { } pub(super) fn create(prefix: &str) -> io::Result { - let (file, mut path) = general_imp::create(prefix, |_| {})?; + let (file, mut path) = general_imp::create(prefix, |options, path| options.open(path))?; let mut delete_on_drop = true; if let Ok(original_path) = CString::new(path.as_os_str().as_bytes()) { // Unlinking the file will *not* remove it per the POSIX specification since it is open. @@ -139,9 +162,11 @@ mod imp { const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x04000000; pub(super) fn create(prefix: &str) -> io::Result { - let (file, path) = general_imp::create(prefix, |options| { - options.attributes(FILE_ATTRIBUTE_TEMPORARY); - options.custom_flags(FILE_FLAG_DELETE_ON_CLOSE); + let (file, path) = general_imp::create(prefix, |mut options, path| { + options + .attributes(FILE_ATTRIBUTE_TEMPORARY) + .custom_flags(FILE_FLAG_DELETE_ON_CLOSE) + .open(path) })?; Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false }) } @@ -158,7 +183,7 @@ mod imp { use super::*; pub(super) fn create(prefix: &str) -> io::Result { - let (file, path) = general_imp::create(prefix, |_| {})?; + let (file, path) = general_imp::create(prefix, |options, path| options.open(path))?; Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true }) } } From e77df4c1bcd97a5585a7b298d49b43cfd5b69c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9?= Date: Sun, 30 Aug 2026 00:52:33 +0100 Subject: [PATCH 27/30] fix: hover on generic output now shows correct type, instead of impl trait --- src/tools/rust-analyzer/crates/hir/src/lib.rs | 18 ++---------- .../crates/ide/src/hover/tests.rs | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir/src/lib.rs b/src/tools/rust-analyzer/crates/hir/src/lib.rs index 8f747e397c873..9238cdcb3ef85 100644 --- a/src/tools/rust-analyzer/crates/hir/src/lib.rs +++ b/src/tools/rust-analyzer/crates/hir/src/lib.rs @@ -3956,21 +3956,9 @@ impl<'db> GenericSubstitution<'db> { TypeOrConstParamData::TypeParamData(param) => Some(param.name.clone()), TypeOrConstParamData::ConstParamData(_) => None, }); - let parent_len = self.subst.len() - - generics - .iter_type_or_consts() - .filter(|g| matches!(g.1, TypeOrConstParamData::TypeParamData(..))) - .count(); - let container_params = self.subst.as_slice()[..parent_len] - .iter() - .filter_map(|param| param.ty()) - .zip(container_type_params.into_iter().flatten()); - let self_params = self.subst.as_slice()[parent_len..] - .iter() - .filter_map(|param| param.ty()) - .zip(type_params); - container_params - .chain(self_params) + self.subst + .types() + .zip(container_type_params.into_iter().flatten().chain(type_params)) .filter_map(|(ty, name)| { Some(( name?.symbol().clone(), diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs index 4b67dd6b17075..66ccd1924639e 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -12021,3 +12021,31 @@ fn bar() { "#]], ); } + +#[test] +fn subst_impl_trait_arg_with_const_generic() { + check( + r#" +fn main() { + generic$0_tn([()], 1); +} + +fn generic_tn(_: [T; N], _: impl Sized) {} +"#, + expect![[r#" + *generic_tn* + + ```rust + ra_test_fixture + ``` + + ```rust + fn generic_tn(_: [T; {const}], _: impl Sized) + ``` + + --- + + `T` = `()` + "#]], + ); +} From 8517e0f05f445518a924e4c93a390c4ac532e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauren=C8=9Biu=20Nicola?= Date: Mon, 31 Aug 2026 14:19:05 +0300 Subject: [PATCH 28/30] Also proc-macro-srv tests on Windows --- src/tools/rust-analyzer/.github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tools/rust-analyzer/.github/workflows/ci.yaml b/src/tools/rust-analyzer/.github/workflows/ci.yaml index 14582f8405f55..7ea734e7ef1c0 100644 --- a/src/tools/rust-analyzer/.github/workflows/ci.yaml +++ b/src/tools/rust-analyzer/.github/workflows/ci.yaml @@ -41,7 +41,11 @@ jobs: proc-macro-srv: if: github.repository == 'rust-lang/rust-analyzer' name: proc-macro-srv - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - name: Checkout repository From 61f12cc266bc3581360d66cd55d7f27bf3479fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauren=C8=9Biu=20Nicola?= Date: Mon, 31 Aug 2026 14:25:32 +0300 Subject: [PATCH 29/30] Fix proc-macro-srv on Windows --- src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs index 8e9335df19b77..718eb47228eb8 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs @@ -102,11 +102,8 @@ fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> NamedTempFile { &format!("proc-macro-srv-{file_name}.dll"), path.as_std_path(), ) - }) - .unwrap_or_else(|err| { - tracing::warn!("failed to create temporary file: {err}"); - NamedTempFile::from_path(path.into_std_path_buf()) - }) + })() + .unwrap_or_else(|_err| NamedTempFile::from_path(path.into_std_path_buf())) } #[cfg(unix)] From 5d441ff309afd6ceccbf81fbc57a69f2c3b6834e Mon Sep 17 00:00:00 2001 From: MarcoIeni <11428655+MarcoIeni@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:51:43 +0200 Subject: [PATCH 30/30] fix `overprovisioned-secrets` zizmor finding --- src/tools/rust-analyzer/.github/workflows/release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/rust-analyzer/.github/workflows/release.yaml b/src/tools/rust-analyzer/.github/workflows/release.yaml index 7d6e0199abe0b..85ff3d08a78a3 100644 --- a/src/tools/rust-analyzer/.github/workflows/release.yaml +++ b/src/tools/rust-analyzer/.github/workflows/release.yaml @@ -252,9 +252,7 @@ jobs: matrix: include: - cmd: vsce - pat: MARKETPLACE_TOKEN - cmd: ovsx - pat: OPENVSX_TOKEN steps: - name: Install Nodejs uses: actions/setup-node@v6 @@ -277,6 +275,8 @@ jobs: - name: Publish Extension if: github.repository == 'rust-lang/rust-analyzer' + env: + PUBLISH_PAT: ${{ (matrix.cmd == 'vsce' && secrets.MARKETPLACE_TOKEN) || (matrix.cmd == 'ovsx' && secrets.OPENVSX_TOKEN) }} working-directory: ./editors/code - run: npx ${{ matrix.cmd }} publish --skip-duplicate --pat ${{ secrets[matrix.pat] }} --packagePath ../../dist/rust-analyzer-*.vsix ${{ github.ref != 'refs/heads/release' && '--pre-release' || '' }} + run: npx ${{ matrix.cmd }} publish --skip-duplicate --pat "$PUBLISH_PAT" --packagePath ../../dist/rust-analyzer-*.vsix ${{ github.ref != 'refs/heads/release' && '--pre-release' || '' }} timeout-minutes: 2