From f5579e603cd1f4a9d58f87ada505c48cba40bb89 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Thu, 22 Jan 2026 22:53:35 +0800 Subject: [PATCH 01/53] Support else-branch for move_guard Example --- ```rust fn main() { match 92 { x @ 0..30 $0if x % 3 == 0 => false, x @ 0..30 if x % 2 == 0 => true, _ => false } } ``` **Before this PR** ```rust fn main() { match 92 { x @ 0..30 => if x % 3 == 0 { false }, x @ 0..30 if x % 2 == 0 => true, _ => false } } ``` **After this PR** ```rust fn main() { match 92 { x @ 0..30 => if x % 3 == 0 { false } else if x % 2 == 0 { true }, _ => false } } ``` --- .../ide-assists/src/handlers/move_guard.rs | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs index 84f02bdfdba6..a5da3def6d23 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs @@ -1,6 +1,7 @@ -use itertools::Itertools; +use itertools::{Itertools, chain}; use syntax::{ SyntaxKind::WHITESPACE, + TextRange, ast::{ AstNode, BlockExpr, ElseBranch, Expr, IfExpr, MatchArm, Pat, edit::AstNodeEdit, make, prec::ExprPrecedence, syntax_factory::SyntaxFactory, @@ -44,13 +45,26 @@ pub(crate) fn move_guard_to_arm_body(acc: &mut Assists, ctx: &AssistContext<'_>) cov_mark::hit!(move_guard_inapplicable_in_arm_body); return None; } - let space_before_guard = guard.syntax().prev_sibling_or_token(); + let rest_arms = rest_arms(&match_arm, ctx.selection_trimmed())?; + let space_before_delete = chain( + guard.syntax().prev_sibling_or_token(), + rest_arms.iter().filter_map(|it| it.syntax().prev_sibling_or_token()), + ); let space_after_arrow = match_arm.fat_arrow_token()?.next_sibling_or_token(); - let guard_condition = guard.condition()?.reset_indent(); let arm_expr = match_arm.expr()?; - let then_branch = crate::utils::wrap_block(&arm_expr); - let if_expr = make::expr_if(guard_condition, then_branch, None).indent(arm_expr.indent_level()); + let if_branch = chain([&match_arm], &rest_arms) + .rfold(None, |else_branch, arm| { + if let Some(guard) = arm.guard() { + let then_branch = crate::utils::wrap_block(&arm.expr()?); + let guard_condition = guard.condition()?.reset_indent(); + Some(make::expr_if(guard_condition, then_branch, else_branch).into()) + } else { + arm.expr().map(|it| crate::utils::wrap_block(&it).into()) + } + })? + .indent(arm_expr.indent_level()); + let ElseBranch::IfExpr(if_expr) = if_branch else { return None }; let target = guard.syntax().text_range(); acc.add( @@ -59,10 +73,13 @@ pub(crate) fn move_guard_to_arm_body(acc: &mut Assists, ctx: &AssistContext<'_>) target, |builder| { let mut edit = builder.make_editor(match_arm.syntax()); - if let Some(element) = space_before_guard - && element.kind() == WHITESPACE - { - edit.delete(element); + for element in space_before_delete { + if element.kind() == WHITESPACE { + edit.delete(element); + } + } + for rest_arm in &rest_arms { + edit.delete(rest_arm.syntax()); } if let Some(element) = space_after_arrow && element.kind() == WHITESPACE @@ -221,6 +238,25 @@ pub(crate) fn move_arm_cond_to_match_guard( ) } +fn rest_arms(match_arm: &MatchArm, selection: TextRange) -> Option> { + match_arm + .parent_match() + .match_arm_list()? + .arms() + .skip_while(|it| it != match_arm) + .skip(1) + .take_while(move |it| { + selection.is_empty() || crate::utils::is_selected(it, selection, false) + }) + .take_while(move |it| { + it.pat() + .zip(match_arm.pat()) + .is_some_and(|(a, b)| a.syntax().text() == b.syntax().text()) + }) + .collect::>() + .into() +} + // Parses an if-else-if chain to get the conditions and the then branches until we encounter an else // branch or the end. fn parse_if_chain(if_expr: IfExpr) -> Option<(Vec<(Expr, BlockExpr)>, Option)> { @@ -344,6 +380,62 @@ fn main() { ); } + #[test] + fn move_multiple_guard_to_arm_body_works() { + check_assist( + move_guard_to_arm_body, + r#" +fn main() { + match 92 { + x @ 0..30 $0if x % 3 == 0 => false, + x @ 0..30 if x % 2 == 0 => true, + _ => false + } +} +"#, + r#" +fn main() { + match 92 { + x @ 0..30 => if x % 3 == 0 { + false + } else if x % 2 == 0 { + true + }, + _ => false + } +} +"#, + ); + + check_assist( + move_guard_to_arm_body, + r#" +fn main() { + match 92 { + x @ 0..30 $0if x % 3 == 0 => false, + x @ 0..30 if x % 2 == 0 => true, + x @ 0..30 => false, + _ => true + } +} +"#, + r#" +fn main() { + match 92 { + x @ 0..30 => if x % 3 == 0 { + false + } else if x % 2 == 0 { + true + } else { + false + }, + _ => true + } +} +"#, + ); + } + #[test] fn move_guard_to_block_arm_body_works() { check_assist( From da577034a9da89aa72107d3debf9161bdb30ef59 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Thu, 22 Jan 2026 23:31:01 +0800 Subject: [PATCH 02/53] Add selection test case --- .../ide-assists/src/handlers/move_guard.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs index a5da3def6d23..afe63d17b8c6 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs @@ -432,6 +432,33 @@ fn main() { _ => true } } +"#, + ); + + check_assist( + move_guard_to_arm_body, + r#" +fn main() { + match 92 { + x @ 0..30 $0if x % 3 == 0 => false, + x @ 0..30 $0if x % 2 == 0 => true, + x @ 0..30 => false, + _ => true + } +} +"#, + r#" +fn main() { + match 92 { + x @ 0..30 => if x % 3 == 0 { + false + } else if x % 2 == 0 { + true + }, + x @ 0..30 => false, + _ => true + } +} "#, ); } From 2bae85ec5221b6276d82a63adcdb15f11a9ea131 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 24 Jan 2026 11:06:07 -0800 Subject: [PATCH 03/53] Remove `derive(Copy)` on `ArrayWindows` The derived `T: Copy` constraint is not appropriate for an iterator by reference, but we generally do not want `Copy` on iterators anyway. --- library/core/src/slice/iter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/slice/iter.rs b/library/core/src/slice/iter.rs index a289b0d6df40..03c503f169c8 100644 --- a/library/core/src/slice/iter.rs +++ b/library/core/src/slice/iter.rs @@ -2175,7 +2175,7 @@ unsafe impl Sync for ChunksExactMut<'_, T> where T: Sync {} /// /// [`array_windows`]: slice::array_windows /// [slices]: slice -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] #[stable(feature = "array_windows", since = "1.94.0")] #[must_use = "iterators are lazy and do nothing unless consumed"] pub struct ArrayWindows<'a, T: 'a, const N: usize> { From 90521553e6fd9bb952fb0f064f7fa022d85d0493 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 24 Jan 2026 11:08:25 -0800 Subject: [PATCH 04/53] Manually `impl Clone for ArrayWindows` This implementation doesn't need the derived `T: Clone`. --- library/core/src/slice/iter.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/core/src/slice/iter.rs b/library/core/src/slice/iter.rs index 03c503f169c8..af63ffa9b0bf 100644 --- a/library/core/src/slice/iter.rs +++ b/library/core/src/slice/iter.rs @@ -2175,7 +2175,7 @@ unsafe impl Sync for ChunksExactMut<'_, T> where T: Sync {} /// /// [`array_windows`]: slice::array_windows /// [slices]: slice -#[derive(Debug, Clone)] +#[derive(Debug)] #[stable(feature = "array_windows", since = "1.94.0")] #[must_use = "iterators are lazy and do nothing unless consumed"] pub struct ArrayWindows<'a, T: 'a, const N: usize> { @@ -2189,6 +2189,14 @@ pub(super) const fn new(slice: &'a [T]) -> Self { } } +// FIXME(#26925) Remove in favor of `#[derive(Clone)]` +#[stable(feature = "array_windows", since = "1.94.0")] +impl Clone for ArrayWindows<'_, T, N> { + fn clone(&self) -> Self { + Self { v: self.v } + } +} + #[stable(feature = "array_windows", since = "1.94.0")] impl<'a, T, const N: usize> Iterator for ArrayWindows<'a, T, N> { type Item = &'a [T; N]; From 08cd2ac33db7d2d9c16ebb1dbbe407c399799abc Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 24 Jan 2026 11:10:40 -0800 Subject: [PATCH 05/53] `impl FusedIterator for ArrayWindows` --- library/core/src/slice/iter.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/src/slice/iter.rs b/library/core/src/slice/iter.rs index af63ffa9b0bf..6e881b1e8b74 100644 --- a/library/core/src/slice/iter.rs +++ b/library/core/src/slice/iter.rs @@ -2260,6 +2260,9 @@ fn is_empty(&self) -> bool { } } +#[stable(feature = "array_windows", since = "1.94.0")] +impl FusedIterator for ArrayWindows<'_, T, N> {} + /// An iterator over a slice in (non-overlapping) chunks (`chunk_size` elements at a /// time), starting at the end of the slice. /// From 8f7d556c75be80449be60795085f081615c49227 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 24 Jan 2026 11:13:51 -0800 Subject: [PATCH 06/53] `impl TrustedLen for ArrayWindows` --- library/core/src/slice/iter.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/src/slice/iter.rs b/library/core/src/slice/iter.rs index 6e881b1e8b74..c5dffcf7e8f7 100644 --- a/library/core/src/slice/iter.rs +++ b/library/core/src/slice/iter.rs @@ -2260,6 +2260,9 @@ fn is_empty(&self) -> bool { } } +#[unstable(feature = "trusted_len", issue = "37572")] +unsafe impl TrustedLen for ArrayWindows<'_, T, N> {} + #[stable(feature = "array_windows", since = "1.94.0")] impl FusedIterator for ArrayWindows<'_, T, N> {} From 129d552d3faa0756af4478d0c243888c6c5a1feb Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 24 Jan 2026 11:19:34 -0800 Subject: [PATCH 07/53] `impl TrustedRandomAccess for ArrayWindows` --- library/core/src/slice/iter.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/library/core/src/slice/iter.rs b/library/core/src/slice/iter.rs index c5dffcf7e8f7..ac096afb38af 100644 --- a/library/core/src/slice/iter.rs +++ b/library/core/src/slice/iter.rs @@ -2232,6 +2232,14 @@ fn nth(&mut self, n: usize) -> Option { fn last(self) -> Option { self.v.last_chunk() } + + unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> Self::Item { + // SAFETY: since the caller guarantees that `idx` is in bounds, + // which means that `idx` cannot overflow an `isize`, and the + // "slice" created by `cast_array` is a subslice of `self.v` + // thus is guaranteed to be valid for the lifetime `'a` of `self.v`. + unsafe { &*self.v.as_ptr().add(idx).cast_array() } + } } #[stable(feature = "array_windows", since = "1.94.0")] @@ -2266,6 +2274,16 @@ unsafe impl TrustedLen for ArrayWindows<'_, T, N> {} #[stable(feature = "array_windows", since = "1.94.0")] impl FusedIterator for ArrayWindows<'_, T, N> {} +#[doc(hidden)] +#[unstable(feature = "trusted_random_access", issue = "none")] +unsafe impl TrustedRandomAccess for ArrayWindows<'_, T, N> {} + +#[doc(hidden)] +#[unstable(feature = "trusted_random_access", issue = "none")] +unsafe impl TrustedRandomAccessNoCoerce for ArrayWindows<'_, T, N> { + const MAY_HAVE_SIDE_EFFECT: bool = false; +} + /// An iterator over a slice in (non-overlapping) chunks (`chunk_size` elements at a /// time), starting at the end of the slice. /// From 8321b2562b547dd101a0d8f05b147a1567565cdf Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Mon, 26 Jan 2026 13:31:25 +0800 Subject: [PATCH 08/53] Add test case for select non-first branch --- .../ide-assists/src/handlers/move_guard.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs index afe63d17b8c6..fdc5a0fbda35 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_guard.rs @@ -439,6 +439,32 @@ fn main() { move_guard_to_arm_body, r#" fn main() { + match 92 { + x @ 0..30 if x % 3 == 0 => false, + x @ 0..30 $0if x % 2 == 0$0 => true, + x @ 0..30 => false, + _ => true + } +} +"#, + r#" +fn main() { + match 92 { + x @ 0..30 if x % 3 == 0 => false, + x @ 0..30 => if x % 2 == 0 { + true + }, + x @ 0..30 => false, + _ => true + } +} +"#, + ); + + check_assist( + move_guard_to_arm_body, + r#" +fn main() { match 92 { x @ 0..30 $0if x % 3 == 0 => false, x @ 0..30 $0if x % 2 == 0 => true, From 5a568d5df421453caa44e812cc5e93eef418faec Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Wed, 28 Jan 2026 00:13:32 +0530 Subject: [PATCH 09/53] fix linking of postcard test --- .../crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs b/src/tools/rust-analyzer/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs index 33ca1d791de7..ba9657a9bb45 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv-cli/tests/bidirectional_postcard.rs @@ -1,4 +1,8 @@ #![cfg(feature = "sysroot-abi")] +#![cfg_attr(feature = "in-rust-tree", feature(rustc_private))] + +#[cfg(feature = "in-rust-tree")] +extern crate rustc_driver as _; mod common { pub(crate) mod utils; From c99fca1f2d247f46f6a9082a9b482b16a86077e9 Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sat, 31 Jan 2026 05:46:06 +0800 Subject: [PATCH 10/53] feat: fallback let postfix completions in condition Easy to input other patterns, or bind variable in let-chain Example --- ```rust fn main() { let bar = 2; if bar.$0 } ``` **Before this PR** No complete 'let' **After this PR** ```rust fn main() { let bar = 2; if let $1 = bar } ``` --- .../ide-completion/src/completions/postfix.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs index 7f67ef848ece..4b244c9025a5 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs @@ -151,6 +151,10 @@ pub(crate) fn complete_postfix( .add_to(acc, ctx.db); } }, + _ if is_in_cond => { + postfix_snippet("let", "let", &format!("let $1 = {receiver_text}")) + .add_to(acc, ctx.db); + } _ if matches!(second_ancestor.kind(), STMT_LIST | EXPR_STMT) => { postfix_snippet("let", "let", &format!("let $0 = {receiver_text};")) .add_to(acc, ctx.db); @@ -744,6 +748,25 @@ fn main() { ); } + #[test] + fn iflet_fallback_cond() { + check_edit( + "let", + r#" +fn main() { + let bar = 2; + if bar.$0 +} +"#, + r#" +fn main() { + let bar = 2; + if let $1 = bar +} +"#, + ); + } + #[test] fn option_letelse() { check_edit( From 4e5b6455b888a7f556f22dcc9add7196d6991444 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 31 Jan 2026 09:31:54 +0100 Subject: [PATCH 11/53] fix: Fix more glob issues --- .../crates/hir-def/src/item_scope.rs | 18 +++++ .../crates/hir-def/src/nameres/collector.rs | 77 +++++++++++++------ .../crates/hir-ty/src/tests/regression.rs | 29 +++++++ 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-def/src/item_scope.rs b/src/tools/rust-analyzer/crates/hir-def/src/item_scope.rs index 9e1efb977786..1303773b59d4 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/item_scope.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/item_scope.rs @@ -893,6 +893,24 @@ pub(crate) fn update_visibility_macros(&mut self, name: &Name, vis: Visibility) self.macros.get_mut(name).expect("tried to update visibility of non-existent macro"); res.vis = vis; } + + pub(crate) fn update_def_types(&mut self, name: &Name, def: ModuleDefId, vis: Visibility) { + let res = self.types.get_mut(name).expect("tried to update def of non-existent type"); + res.def = def; + res.vis = vis; + } + + pub(crate) fn update_def_values(&mut self, name: &Name, def: ModuleDefId, vis: Visibility) { + let res = self.values.get_mut(name).expect("tried to update def of non-existent value"); + res.def = def; + res.vis = vis; + } + + pub(crate) fn update_def_macros(&mut self, name: &Name, def: MacroId, vis: Visibility) { + let res = self.macros.get_mut(name).expect("tried to update def of non-existent macro"); + res.def = def; + res.vis = vis; + } } impl PerNs { diff --git a/src/tools/rust-analyzer/crates/hir-def/src/nameres/collector.rs b/src/tools/rust-analyzer/crates/hir-def/src/nameres/collector.rs index 323060f61d15..f51524c1b551 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/nameres/collector.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/nameres/collector.rs @@ -1209,42 +1209,69 @@ fn push_res_and_update_glob_vis( // `ItemScope::push_res_with_import()`. if let Some(def) = defs.types && let Some(prev_def) = prev_defs.types - && def.def == prev_def.def - && self.from_glob_import.contains_type(module_id, name.clone()) - && def.vis != prev_def.vis - && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) { - changed = true; - // This import is being handled here, don't pass it down to - // `ItemScope::push_res_with_import()`. - defs.types = None; - self.def_map.modules[module_id].scope.update_visibility_types(name, def.vis); + if def.def == prev_def.def + && self.from_glob_import.contains_type(module_id, name.clone()) + && def.vis != prev_def.vis + && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) + { + changed = true; + // This import is being handled here, don't pass it down to + // `ItemScope::push_res_with_import()`. + defs.types = None; + self.def_map.modules[module_id].scope.update_visibility_types(name, def.vis); + } + // When the source module's definition changed (e.g., due to an explicit import + // shadowing a glob), propagate the new definition to modules that glob-import from it. + // We check that the previous definition came from the same glob import to avoid + // incorrectly overwriting definitions from different glob sources. + // + // Note this is not a perfect fix, but it makes + // https://github.com/rust-lang/rust-analyzer/issues/19224 work for now until we + // implement a proper glob graph + else if def.def != prev_def.def && prev_def.import == def_import_type { + changed = true; + defs.types = None; + self.def_map.modules[module_id].scope.update_def_types(name, def.def, def.vis); + } } if let Some(def) = defs.values && let Some(prev_def) = prev_defs.values - && def.def == prev_def.def - && self.from_glob_import.contains_value(module_id, name.clone()) - && def.vis != prev_def.vis - && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) { - changed = true; - // See comment above. - defs.values = None; - self.def_map.modules[module_id].scope.update_visibility_values(name, def.vis); + if def.def == prev_def.def + && self.from_glob_import.contains_value(module_id, name.clone()) + && def.vis != prev_def.vis + && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) + { + changed = true; + defs.values = None; + self.def_map.modules[module_id].scope.update_visibility_values(name, def.vis); + } else if def.def != prev_def.def + && prev_def.import.map(ImportOrExternCrate::from) == def_import_type + { + changed = true; + defs.values = None; + self.def_map.modules[module_id].scope.update_def_values(name, def.def, def.vis); + } } if let Some(def) = defs.macros && let Some(prev_def) = prev_defs.macros - && def.def == prev_def.def - && self.from_glob_import.contains_macro(module_id, name.clone()) - && def.vis != prev_def.vis - && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) { - changed = true; - // See comment above. - defs.macros = None; - self.def_map.modules[module_id].scope.update_visibility_macros(name, def.vis); + if def.def == prev_def.def + && self.from_glob_import.contains_macro(module_id, name.clone()) + && def.vis != prev_def.vis + && def.vis.max(self.db, prev_def.vis, &self.def_map) == Some(def.vis) + { + changed = true; + defs.macros = None; + self.def_map.modules[module_id].scope.update_visibility_macros(name, def.vis); + } else if def.def != prev_def.def && prev_def.import == def_import_type { + changed = true; + defs.macros = None; + self.def_map.modules[module_id].scope.update_def_macros(name, def.def, def.vis); + } } } 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 4f1480c39366..f6736905b8a1 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 @@ -2645,3 +2645,32 @@ fn bar(v: Self::Assoc) { "#, ); } + +#[test] +fn issue_21560() { + check_no_mismatches( + r#" +mod bindings { + use super::*; + pub type HRESULT = i32; +} +use bindings::*; + + +mod error { + use super::*; + pub fn nonzero_hresult(hr: HRESULT) -> crate::HRESULT { + hr + } +} +pub use error::*; + +mod hresult { + use super::*; + pub struct HRESULT(pub i32); +} +pub use hresult::HRESULT; + + "#, + ); +} From 41577bca066750322eedd056df12b338ffcaab89 Mon Sep 17 00:00:00 2001 From: Abdul Date: Mon, 2 Feb 2026 01:48:12 +0530 Subject: [PATCH 12/53] refactor: Remove unused comments related to SyntaxErrorKind --- .../rust-analyzer/crates/syntax/src/syntax_error.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/tools/rust-analyzer/crates/syntax/src/syntax_error.rs b/src/tools/rust-analyzer/crates/syntax/src/syntax_error.rs index 1c902893abc6..6f00ef4ed584 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/syntax_error.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/syntax_error.rs @@ -9,16 +9,6 @@ #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SyntaxError(String, TextRange); -// FIXME: there was an unused SyntaxErrorKind previously (before this enum was removed) -// It was introduced in this PR: https://github.com/rust-lang/rust-analyzer/pull/846/files#diff-827da9b03b8f9faa1bade5cdd44d5dafR95 -// but it was not removed by a mistake. -// -// So, we need to find a place where to stick validation for attributes in match clauses. -// Code before refactor: -// InvalidMatchInnerAttr => { -// write!(f, "Inner attributes are only allowed directly after the opening brace of the match expression") -// } - impl SyntaxError { pub fn new(message: impl Into, range: TextRange) -> Self { Self(message.into(), range) From c836e24b8960b26dca4c35143545ead6f27ecf7d Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Mon, 2 Feb 2026 17:29:28 +0200 Subject: [PATCH 13/53] Cover more cases where we need parentheses in `&(impl Trait1 + Trait2)` And refactor the mechanism to be more maintainable. --- .../crates/hir-ty/src/display.rs | 201 +++++++++--------- .../crates/hir-ty/src/tests/coercion.rs | 2 +- .../hir-ty/src/tests/display_source_code.rs | 4 +- .../crates/hir-ty/src/tests/traits.rs | 2 +- .../rust-analyzer/crates/hir/src/display.rs | 2 + .../crates/ide/src/inlay_hints/bind_pat.rs | 17 ++ 6 files changed, 119 insertions(+), 109 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 43b428c3fa51..4e77e8be364b 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs @@ -12,7 +12,6 @@ use hir_def::{ FindPathConfig, GenericDefId, GenericParamId, HasModule, LocalFieldId, Lookup, ModuleDefId, ModuleId, TraitId, - db::DefDatabase, expr_store::{ExpressionStore, path::Path}, find_path::{self, PrefixKind}, hir::generics::{TypeOrConstParamData, TypeParamProvenance, WherePredicate}, @@ -100,6 +99,9 @@ pub struct HirFormatter<'a, 'db> { display_kind: DisplayKind, display_target: DisplayTarget, bounds_formatting_ctx: BoundsFormattingCtx<'db>, + /// Whether formatting `impl Trait1 + Trait2` or `dyn Trait1 + Trait2` needs parentheses around it, + /// for example when formatting `&(impl Trait1 + Trait2)`. + trait_bounds_need_parens: bool, } // FIXME: To consider, ref and dyn trait lifetimes can be omitted if they are `'_`, path args should @@ -331,6 +333,7 @@ fn display_source_code<'a>( show_container_bounds: false, display_lifetimes: DisplayLifetime::OnlyNamedOrStatic, bounds_formatting_ctx: Default::default(), + trait_bounds_need_parens: false, }) { Ok(()) => {} Err(HirDisplayError::FmtError) => panic!("Writing to String can't fail!"), @@ -566,6 +569,7 @@ pub fn write_to(&self, f: &mut F) -> Result { show_container_bounds: self.show_container_bounds, display_lifetimes: self.display_lifetimes, bounds_formatting_ctx: Default::default(), + trait_bounds_need_parens: false, }) } @@ -612,7 +616,11 @@ fn hir_fmt(&self, f: &mut HirFormatter<'_, 'db>) -> Result { } } -fn write_projection<'db>(f: &mut HirFormatter<'_, 'db>, alias: &AliasTy<'db>) -> Result { +fn write_projection<'db>( + f: &mut HirFormatter<'_, 'db>, + alias: &AliasTy<'db>, + needs_parens_if_multi: bool, +) -> Result { if f.should_truncate() { return write!(f, "{TYPE_HINT_TRUNCATION}"); } @@ -650,6 +658,7 @@ fn write_projection<'db>(f: &mut HirFormatter<'_, 'db>, alias: &AliasTy<'db>) -> Either::Left(Ty::new_alias(f.interner, AliasTyKind::Projection, *alias)), &bounds, SizedByDefault::NotSized, + needs_parens_if_multi, ) }); } @@ -1056,7 +1065,7 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) return write!(f, "{TYPE_HINT_TRUNCATION}"); } - use TyKind; + let trait_bounds_need_parens = mem::replace(&mut f.trait_bounds_need_parens, false); match self.kind() { TyKind::Never => write!(f, "!")?, TyKind::Str => write!(f, "str")?, @@ -1077,103 +1086,34 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) c.hir_fmt(f)?; write!(f, "]")?; } - kind @ (TyKind::RawPtr(t, m) | TyKind::Ref(_, t, m)) => { - if let TyKind::Ref(l, _, _) = kind { - f.write_char('&')?; - if f.render_region(l) { - l.hir_fmt(f)?; - f.write_char(' ')?; - } + TyKind::Ref(l, t, m) => { + f.write_char('&')?; + if f.render_region(l) { + l.hir_fmt(f)?; + f.write_char(' ')?; + } + match m { + rustc_ast_ir::Mutability::Not => (), + rustc_ast_ir::Mutability::Mut => f.write_str("mut ")?, + } + + f.trait_bounds_need_parens = true; + t.hir_fmt(f)?; + f.trait_bounds_need_parens = false; + } + TyKind::RawPtr(t, m) => { + write!( + f, + "*{}", match m { - rustc_ast_ir::Mutability::Not => (), - rustc_ast_ir::Mutability::Mut => f.write_str("mut ")?, + rustc_ast_ir::Mutability::Not => "const ", + rustc_ast_ir::Mutability::Mut => "mut ", } - } else { - write!( - f, - "*{}", - match m { - rustc_ast_ir::Mutability::Not => "const ", - rustc_ast_ir::Mutability::Mut => "mut ", - } - )?; - } + )?; - // FIXME: all this just to decide whether to use parentheses... - let (preds_to_print, has_impl_fn_pred) = match t.kind() { - TyKind::Dynamic(bounds, region) => { - let contains_impl_fn = - bounds.iter().any(|bound| match bound.skip_binder() { - ExistentialPredicate::Trait(trait_ref) => { - let trait_ = trait_ref.def_id.0; - fn_traits(f.lang_items()).any(|it| it == trait_) - } - _ => false, - }); - let render_lifetime = f.render_region(region); - (bounds.len() + render_lifetime as usize, contains_impl_fn) - } - TyKind::Alias(AliasTyKind::Opaque, ty) => { - let opaque_ty_id = match ty.def_id { - SolverDefId::InternedOpaqueTyId(id) => id, - _ => unreachable!(), - }; - let impl_trait_id = db.lookup_intern_impl_trait_id(opaque_ty_id); - if let ImplTraitId::ReturnTypeImplTrait(func, _) = impl_trait_id { - let data = impl_trait_id.predicates(db); - let bounds = - || data.iter_instantiated_copied(f.interner, ty.args.as_slice()); - let mut len = bounds().count(); - - // Don't count Sized but count when it absent - // (i.e. when explicit ?Sized bound is set). - let default_sized = SizedByDefault::Sized { anchor: func.krate(db) }; - let sized_bounds = bounds() - .filter(|b| { - matches!( - b.kind().skip_binder(), - ClauseKind::Trait(trait_ref) - if default_sized.is_sized_trait( - trait_ref.def_id().0, - db, - ), - ) - }) - .count(); - match sized_bounds { - 0 => len += 1, - _ => { - len = len.saturating_sub(sized_bounds); - } - } - - let contains_impl_fn = bounds().any(|bound| { - if let ClauseKind::Trait(trait_ref) = bound.kind().skip_binder() { - let trait_ = trait_ref.def_id().0; - fn_traits(f.lang_items()).any(|it| it == trait_) - } else { - false - } - }); - (len, contains_impl_fn) - } else { - (0, false) - } - } - _ => (0, false), - }; - - if has_impl_fn_pred && preds_to_print <= 2 { - return t.hir_fmt(f); - } - - if preds_to_print > 1 { - write!(f, "(")?; - t.hir_fmt(f)?; - write!(f, ")")?; - } else { - t.hir_fmt(f)?; - } + f.trait_bounds_need_parens = true; + t.hir_fmt(f)?; + f.trait_bounds_need_parens = false; } TyKind::Tuple(tys) => { if tys.len() == 1 { @@ -1328,7 +1268,9 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) hir_fmt_generics(f, parameters.as_slice(), Some(def.def_id().0.into()), None)?; } - TyKind::Alias(AliasTyKind::Projection, alias_ty) => write_projection(f, &alias_ty)?, + TyKind::Alias(AliasTyKind::Projection, alias_ty) => { + write_projection(f, &alias_ty, trait_bounds_need_parens)? + } TyKind::Foreign(alias) => { let type_alias = db.type_alias_signature(alias.0); f.start_location_link(alias.0.into()); @@ -1363,6 +1305,7 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) Either::Left(*self), &bounds, SizedByDefault::Sized { anchor: krate }, + trait_bounds_need_parens, )?; } TyKind::Closure(id, substs) => { @@ -1525,6 +1468,7 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) Either::Left(*self), &bounds, SizedByDefault::Sized { anchor: krate }, + trait_bounds_need_parens, )?; } }, @@ -1567,6 +1511,7 @@ fn hir_fmt(&self, f @ &mut HirFormatter { db, .. }: &mut HirFormatter<'_, 'db>) Either::Left(*self), &bounds_to_display, SizedByDefault::NotSized, + trait_bounds_need_parens, )?; } TyKind::Error(_) => { @@ -1806,11 +1751,11 @@ pub enum SizedByDefault { } impl SizedByDefault { - fn is_sized_trait(self, trait_: TraitId, db: &dyn DefDatabase) -> bool { + fn is_sized_trait(self, trait_: TraitId, interner: DbInterner<'_>) -> bool { match self { Self::NotSized => false, - Self::Sized { anchor } => { - let sized_trait = hir_def::lang_item::lang_items(db, anchor).Sized; + Self::Sized { .. } => { + let sized_trait = interner.lang_items().Sized; Some(trait_) == sized_trait } } @@ -1823,16 +1768,62 @@ pub fn write_bounds_like_dyn_trait_with_prefix<'db>( this: Either, Region<'db>>, predicates: &[Clause<'db>], default_sized: SizedByDefault, + needs_parens_if_multi: bool, ) -> Result { + let needs_parens = + needs_parens_if_multi && trait_bounds_need_parens(f, this, predicates, default_sized); + if needs_parens { + write!(f, "(")?; + } write!(f, "{prefix}")?; if !predicates.is_empty() || predicates.is_empty() && matches!(default_sized, SizedByDefault::Sized { .. }) { write!(f, " ")?; - write_bounds_like_dyn_trait(f, this, predicates, default_sized) - } else { - Ok(()) + write_bounds_like_dyn_trait(f, this, predicates, default_sized)?; } + if needs_parens { + write!(f, ")")?; + } + Ok(()) +} + +fn trait_bounds_need_parens<'db>( + f: &mut HirFormatter<'_, 'db>, + this: Either, Region<'db>>, + predicates: &[Clause<'db>], + default_sized: SizedByDefault, +) -> bool { + // This needs to be kept in sync with `write_bounds_like_dyn_trait()`. + let mut distinct_bounds = 0usize; + let mut is_sized = false; + for p in predicates { + match p.kind().skip_binder() { + ClauseKind::Trait(trait_ref) => { + let trait_ = trait_ref.def_id().0; + if default_sized.is_sized_trait(trait_, f.interner) { + is_sized = true; + if matches!(default_sized, SizedByDefault::Sized { .. }) { + // Don't print +Sized, but rather +?Sized if absent. + continue; + } + } + + distinct_bounds += 1; + } + ClauseKind::TypeOutlives(to) if Either::Left(to.0) == this => distinct_bounds += 1, + ClauseKind::RegionOutlives(lo) if Either::Right(lo.0) == this => distinct_bounds += 1, + _ => {} + } + } + + if let SizedByDefault::Sized { .. } = default_sized + && !is_sized + { + distinct_bounds += 1; + } + + distinct_bounds > 1 } fn write_bounds_like_dyn_trait<'db>( @@ -1855,7 +1846,7 @@ fn write_bounds_like_dyn_trait<'db>( match p.kind().skip_binder() { ClauseKind::Trait(trait_ref) => { let trait_ = trait_ref.def_id().0; - if default_sized.is_sized_trait(trait_, f.db) { + if default_sized.is_sized_trait(trait_, f.interner) { is_sized = true; if matches!(default_sized, SizedByDefault::Sized { .. }) { // Don't print +Sized, but rather +?Sized if absent. diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/coercion.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/coercion.rs index 36630ab587cd..438699b40983 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/coercion.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/coercion.rs @@ -608,7 +608,7 @@ trait Foo {} fn test(f: impl Foo, g: &(impl Foo + ?Sized)) { let _: &dyn Foo = &f; let _: &dyn Foo = g; - //^ expected &'? (dyn Foo + 'static), got &'? impl Foo + ?Sized + //^ expected &'? (dyn Foo + 'static), got &'? (impl Foo + ?Sized) } "#, ); 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 dc3869930dcf..37da7fc87563 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 @@ -111,7 +111,7 @@ fn test( b; //^ impl Foo c; - //^ &impl Foo + ?Sized + //^ &(impl Foo + ?Sized) d; //^ S ref_any; @@ -192,7 +192,7 @@ fn test( b; //^ fn(impl Foo) -> impl Foo c; -} //^ fn(&impl Foo + ?Sized) -> &impl Foo + ?Sized +} //^ fn(&(impl Foo + ?Sized)) -> &(impl Foo + ?Sized) "#, ); } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/traits.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/traits.rs index 390553c0d7a9..1a0bd44798fa 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/traits.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/traits.rs @@ -4819,7 +4819,7 @@ fn allowed3(baz: impl Baz>) {} 431..433 '{}': () 447..450 'baz': impl Baz 480..482 '{}': () - 500..503 'baz': impl Baz + 500..503 'baz': impl Baz 544..546 '{}': () 560..563 'baz': impl Baz> 598..600 '{}': () diff --git a/src/tools/rust-analyzer/crates/hir/src/display.rs b/src/tools/rust-analyzer/crates/hir/src/display.rs index 1f9af564c359..b4440dfa1826 100644 --- a/src/tools/rust-analyzer/crates/hir/src/display.rs +++ b/src/tools/rust-analyzer/crates/hir/src/display.rs @@ -587,6 +587,7 @@ fn hir_fmt(&self, f: &mut HirFormatter<'_, 'db>) -> Result { Either::Left(ty), &predicates, SizedByDefault::Sized { anchor: krate }, + false, ); } }, @@ -614,6 +615,7 @@ fn hir_fmt(&self, f: &mut HirFormatter<'_, 'db>) -> Result { Either::Left(ty), &predicates, default_sized, + false, )?; } Ok(()) diff --git a/src/tools/rust-analyzer/crates/ide/src/inlay_hints/bind_pat.rs b/src/tools/rust-analyzer/crates/ide/src/inlay_hints/bind_pat.rs index c74e3104c14e..caf7cc714d33 100644 --- a/src/tools/rust-analyzer/crates/ide/src/inlay_hints/bind_pat.rs +++ b/src/tools/rust-analyzer/crates/ide/src/inlay_hints/bind_pat.rs @@ -1382,4 +1382,21 @@ fn f<'a>() { "#]], ); } + + #[test] + fn ref_multi_trait_impl_trait() { + check_with_config( + InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG }, + r#" +//- minicore: sized +trait Eq {} +trait Ord {} + +fn foo(argument: &(impl Eq + Ord)) { + let x = argument; + // ^ &(impl Eq + Ord) +} + "#, + ); + } } From dbb9b622d51e11e9b6ec75d4e04e881a37a67eba Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Fri, 30 Jan 2026 17:53:36 +0000 Subject: [PATCH 14/53] internal: Clarify that CargoCheck applies to all check commands CargoCheckMessage and CargoCheckParser were misleading, because they could apply to any tool that emits diagnostics. For example, it may be rustc directly rather than via cargo. Clarify this in the names. This is most noticeable when working on custom check commands in rust-project.json. --- .../crates/rust-analyzer/src/flycheck.rs | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs index 512c231990cb..580bba546ce2 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs @@ -403,9 +403,9 @@ struct FlycheckActor { /// doesn't provide a way to read sub-process output without blocking, so we /// have to wrap sub-processes output handling in a thread and pass messages /// back over a channel. - command_handle: Option>, + command_handle: Option>, /// The receiver side of the channel mentioned above. - command_receiver: Option>, + command_receiver: Option>, diagnostics_cleared_for: FxHashSet, diagnostics_received: DiagnosticsReceived, } @@ -420,7 +420,7 @@ enum DiagnosticsReceived { #[allow(clippy::large_enum_variant)] enum Event { RequestStateChange(StateChange), - CheckEvent(Option), + CheckEvent(Option), } /// This is stable behaviour. Don't change. @@ -579,7 +579,7 @@ fn run(mut self, inbox: Receiver) { let (sender, receiver) = unbounded(); match CommandHandle::spawn( command, - CargoCheckParser, + CheckParser, sender, match &self.config { FlycheckConfig::Automatic { cargo_options, .. } => { @@ -699,7 +699,7 @@ fn run(mut self, inbox: Receiver) { self.report_progress(Progress::DidFinish(res)); } Event::CheckEvent(Some(message)) => match message { - CargoCheckMessage::CompilerArtifact(msg) => { + CheckMessage::CompilerArtifact(msg) => { tracing::trace!( flycheck_id = self.id, artifact = msg.target.name, @@ -729,7 +729,7 @@ fn run(mut self, inbox: Receiver) { }); } } - CargoCheckMessage::Diagnostic { diagnostic, package_id } => { + CheckMessage::Diagnostic { diagnostic, package_id } => { tracing::trace!( flycheck_id = self.id, message = diagnostic.message, @@ -942,15 +942,18 @@ fn send(&self, check_task: FlycheckMessage) { } #[allow(clippy::large_enum_variant)] -enum CargoCheckMessage { +enum CheckMessage { + /// A message from `cargo check`, including details like the path + /// to the relevant `Cargo.toml`. CompilerArtifact(cargo_metadata::Artifact), + /// A diagnostic message from rustc itself. Diagnostic { diagnostic: Diagnostic, package_id: Option }, } -struct CargoCheckParser; +struct CheckParser; -impl JsonLinesParser for CargoCheckParser { - fn from_line(&self, line: &str, error: &mut String) -> Option { +impl JsonLinesParser for CheckParser { + fn from_line(&self, line: &str, error: &mut String) -> Option { let mut deserializer = serde_json::Deserializer::from_str(line); deserializer.disable_recursion_limit(); if let Ok(message) = JsonMessage::deserialize(&mut deserializer) { @@ -958,10 +961,10 @@ fn from_line(&self, line: &str, error: &mut String) -> Option // Skip certain kinds of messages to only spend time on what's useful JsonMessage::Cargo(message) => match message { cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => { - Some(CargoCheckMessage::CompilerArtifact(artifact)) + Some(CheckMessage::CompilerArtifact(artifact)) } cargo_metadata::Message::CompilerMessage(msg) => { - Some(CargoCheckMessage::Diagnostic { + Some(CheckMessage::Diagnostic { diagnostic: msg.message, package_id: Some(PackageSpecifier::Cargo { package_id: Arc::new(msg.package_id), @@ -971,7 +974,7 @@ fn from_line(&self, line: &str, error: &mut String) -> Option _ => None, }, JsonMessage::Rustc(message) => { - Some(CargoCheckMessage::Diagnostic { diagnostic: message, package_id: None }) + Some(CheckMessage::Diagnostic { diagnostic: message, package_id: None }) } }; } @@ -981,7 +984,7 @@ fn from_line(&self, line: &str, error: &mut String) -> Option None } - fn from_eof(&self) -> Option { + fn from_eof(&self) -> Option { None } } From df2a3f8e042301bc6f54f83fdf3b2fba66eec53a Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Fri, 30 Jan 2026 17:53:36 +0000 Subject: [PATCH 15/53] internal: Document and rename DiagnosticsReceived variants --- .../crates/rust-analyzer/src/flycheck.rs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs index 512c231990cb..2f38901ce12f 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs @@ -410,11 +410,17 @@ struct FlycheckActor { diagnostics_received: DiagnosticsReceived, } -#[derive(PartialEq)] +#[derive(PartialEq, Debug)] enum DiagnosticsReceived { - Yes, - No, - YesAndClearedForAll, + /// We started a flycheck, but we haven't seen any diagnostics yet. + NotYet, + /// We received a non-zero number of diagnostics from rustc or clippy (via + /// cargo or custom check command). This means there were errors or + /// warnings. + AtLeastOne, + /// We received a non-zero number of diagnostics, and the scope is + /// workspace, so we've discarded the previous workspace diagnostics. + AtLeastOneAndClearedWorkspace, } #[allow(clippy::large_enum_variant)] @@ -511,7 +517,7 @@ fn new( command_handle: None, command_receiver: None, diagnostics_cleared_for: Default::default(), - diagnostics_received: DiagnosticsReceived::No, + diagnostics_received: DiagnosticsReceived::NotYet, } } @@ -640,7 +646,7 @@ fn run(mut self, inbox: Receiver) { error ); } - if self.diagnostics_received == DiagnosticsReceived::No { + if self.diagnostics_received == DiagnosticsReceived::NotYet { tracing::trace!(flycheck_id = self.id, "clearing diagnostics"); // We finished without receiving any diagnostics. // Clear everything for good measure @@ -736,8 +742,8 @@ fn run(mut self, inbox: Receiver) { package_id = package_id.as_ref().map(|it| it.as_str()), "diagnostic received" ); - if self.diagnostics_received == DiagnosticsReceived::No { - self.diagnostics_received = DiagnosticsReceived::Yes; + if self.diagnostics_received == DiagnosticsReceived::NotYet { + self.diagnostics_received = DiagnosticsReceived::AtLeastOne; } if let Some(package_id) = &package_id { if self.diagnostics_cleared_for.insert(package_id.clone()) { @@ -754,9 +760,10 @@ fn run(mut self, inbox: Receiver) { }); } } else if self.diagnostics_received - != DiagnosticsReceived::YesAndClearedForAll + != DiagnosticsReceived::AtLeastOneAndClearedWorkspace { - self.diagnostics_received = DiagnosticsReceived::YesAndClearedForAll; + self.diagnostics_received = + DiagnosticsReceived::AtLeastOneAndClearedWorkspace; self.send(FlycheckMessage::ClearDiagnostics { id: self.id, kind: ClearDiagnosticsKind::All(ClearScope::Workspace), @@ -792,7 +799,7 @@ fn cancel_check_process(&mut self) { fn clear_diagnostics_state(&mut self) { self.diagnostics_cleared_for.clear(); - self.diagnostics_received = DiagnosticsReceived::No; + self.diagnostics_received = DiagnosticsReceived::NotYet; } fn explicit_check_command( From 93dece9ae76af98e5e2fa6588091242ad5dd1639 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Mon, 2 Feb 2026 16:09:59 +0000 Subject: [PATCH 16/53] fix: Stale diagnostics with rust-project.json and rustc JSON PR #18043 changed flycheck to be scoped to the relevant package. This broke projects using check commands that invoke rustc directly, because diagnostic JSON from rustc doesn't contain the package ID. This was visible in the rust-analyzer logs when RA_LOG is set to `rust_analyzer::flycheck=trace`. Before: 2026-02-02T07:03:48.020184937-08:00 TRACE diagnostic received flycheck_id=0 mismatched types package_id=None scope=Workspace ... 2026-02-02T07:03:55.082046488-08:00 TRACE clearing diagnostics flycheck_id=0 scope=Workspace After: 2026-02-02T07:14:32.760707785-08:00 TRACE diagnostic received flycheck_id=0 mismatched types package_id=None scope=Package { package: BuildInfo { label: "fbcode//rust_devx/rust-guess-deps:rust-guess-deps" }, workspace_deps: Some({}) } ... 2026-02-02T07:14:48.355981415-08:00 TRACE clearing diagnostics flycheck_id=0 scope=Package { package: BuildInfo { label: "fbcode//rust_devx/rust-guess-deps:rust-guess-deps" }, workspace_deps: Some({}) } Previously r-a assumed that a diagnostic without a package ID applied to the whole workspace. We would insert the diagnostic at the workspace level, but then only clear diagnostics for the package. As a result, red squiggles would get 'stuck'. Users who had fixed compilation issues would still see the old red squiggles until they introduced a new compilation error. Instead, always apply diagnostics to the current package if flycheck is scoped to a package and the diagnostic doesn't specify a package. This makes CargoCheckEvent(None) and CargoCheckEvent(Some(_)) more consistent, as they now both match on scope. --- .../crates/rust-analyzer/src/flycheck.rs | 88 ++++++++++++------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs index 2f38901ce12f..616025ac1928 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/flycheck.rs @@ -740,42 +740,70 @@ fn run(mut self, inbox: Receiver) { flycheck_id = self.id, message = diagnostic.message, package_id = package_id.as_ref().map(|it| it.as_str()), + scope = ?self.scope, "diagnostic received" ); - if self.diagnostics_received == DiagnosticsReceived::NotYet { - self.diagnostics_received = DiagnosticsReceived::AtLeastOne; - } - if let Some(package_id) = &package_id { - if self.diagnostics_cleared_for.insert(package_id.clone()) { - tracing::trace!( - flycheck_id = self.id, - package_id = package_id.as_str(), - "clearing diagnostics" - ); - self.send(FlycheckMessage::ClearDiagnostics { + + match &self.scope { + FlycheckScope::Workspace => { + if self.diagnostics_received == DiagnosticsReceived::NotYet { + self.send(FlycheckMessage::ClearDiagnostics { + id: self.id, + kind: ClearDiagnosticsKind::All(ClearScope::Workspace), + }); + + self.diagnostics_received = + DiagnosticsReceived::AtLeastOneAndClearedWorkspace; + } + + if let Some(package_id) = package_id { + tracing::warn!( + "Ignoring package label {:?} and applying diagnostics to the whole workspace", + package_id + ); + } + + self.send(FlycheckMessage::AddDiagnostic { id: self.id, - kind: ClearDiagnosticsKind::All(ClearScope::Package( - package_id.clone(), - )), + generation: self.generation, + package_id: None, + workspace_root: self.root.clone(), + diagnostic, + }); + } + FlycheckScope::Package { package: flycheck_package, .. } => { + if self.diagnostics_received == DiagnosticsReceived::NotYet { + self.diagnostics_received = DiagnosticsReceived::AtLeastOne; + } + + // If the package has been set in the diagnostic JSON, respect that. Otherwise, use the + // package that the current flycheck is scoped to. This is useful when a project is + // directly using rustc for its checks (e.g. custom check commands in rust-project.json). + let package_id = package_id.unwrap_or(flycheck_package.clone()); + + if self.diagnostics_cleared_for.insert(package_id.clone()) { + tracing::trace!( + flycheck_id = self.id, + package_id = package_id.as_str(), + "clearing diagnostics" + ); + self.send(FlycheckMessage::ClearDiagnostics { + id: self.id, + kind: ClearDiagnosticsKind::All(ClearScope::Package( + package_id.clone(), + )), + }); + } + + self.send(FlycheckMessage::AddDiagnostic { + id: self.id, + generation: self.generation, + package_id: Some(package_id), + workspace_root: self.root.clone(), + diagnostic, }); } - } else if self.diagnostics_received - != DiagnosticsReceived::AtLeastOneAndClearedWorkspace - { - self.diagnostics_received = - DiagnosticsReceived::AtLeastOneAndClearedWorkspace; - self.send(FlycheckMessage::ClearDiagnostics { - id: self.id, - kind: ClearDiagnosticsKind::All(ClearScope::Workspace), - }); } - self.send(FlycheckMessage::AddDiagnostic { - id: self.id, - generation: self.generation, - package_id, - workspace_root: self.root.clone(), - diagnostic, - }); } }, } From 301ff3fc3c4a876caff8cb3b604735c2e6d2c40c Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Tue, 3 Feb 2026 02:25:31 +0200 Subject: [PATCH 17/53] Infer the expected len in `include_bytes!()`, to avoid mismatches Since we can't read the real file size. --- .../crates/hir-expand/src/builtin/fn_macro.rs | 19 +++++++++++-------- .../crates/hir-ty/src/tests/simple.rs | 10 ++++++++++ .../crates/intern/src/symbol/symbols.rs | 1 + .../crates/test-utils/src/minicore.rs | 9 +++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir-expand/src/builtin/fn_macro.rs b/src/tools/rust-analyzer/crates/hir-expand/src/builtin/fn_macro.rs index 6e4b96b05088..3029bca1d51f 100644 --- a/src/tools/rust-analyzer/crates/hir-expand/src/builtin/fn_macro.rs +++ b/src/tools/rust-analyzer/crates/hir-expand/src/builtin/fn_macro.rs @@ -830,15 +830,18 @@ fn include_bytes_expand( span: Span, ) -> ExpandResult { // FIXME: actually read the file here if the user asked for macro expansion - let res = tt::TopSubtree::invisible_from_leaves( + let underscore = sym::underscore; + let zero = tt::Literal { + text_and_suffix: sym::_0_u8, span, - [tt::Leaf::Literal(tt::Literal { - text_and_suffix: Symbol::empty(), - span, - kind: tt::LitKind::ByteStrRaw(1), - suffix_len: 0, - })], - ); + kind: tt::LitKind::Integer, + suffix_len: 3, + }; + // We don't use a real length since we can't know the file length, so we use an underscore + // to infer it. + let res = quote! {span => + &[#zero; #underscore] + }; ExpandResult::ok(res) } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/simple.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/simple.rs index 98503452d348..b0b4e1b0ccaf 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/simple.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/simple.rs @@ -4056,3 +4056,13 @@ fn foo() { "#]], ); } + +#[test] +fn include_bytes_len_mismatch() { + check_no_mismatches( + r#" +//- minicore: include_bytes +static S: &[u8; 158] = include_bytes!("/foo/bar/baz.txt"); + "#, + ); +} diff --git a/src/tools/rust-analyzer/crates/intern/src/symbol/symbols.rs b/src/tools/rust-analyzer/crates/intern/src/symbol/symbols.rs index 2be4e41f4f1a..e956faa01f56 100644 --- a/src/tools/rust-analyzer/crates/intern/src/symbol/symbols.rs +++ b/src/tools/rust-analyzer/crates/intern/src/symbol/symbols.rs @@ -110,6 +110,7 @@ pub(super) fn prefill() -> DashMap> { win64_dash_unwind = "win64-unwind", x86_dash_interrupt = "x86-interrupt", rust_dash_preserve_dash_none = "preserve-none", + _0_u8 = "0_u8", @PLAIN: __ra_fixup, diff --git a/src/tools/rust-analyzer/crates/test-utils/src/minicore.rs b/src/tools/rust-analyzer/crates/test-utils/src/minicore.rs index 48c3e8952507..bac769134741 100644 --- a/src/tools/rust-analyzer/crates/test-utils/src/minicore.rs +++ b/src/tools/rust-analyzer/crates/test-utils/src/minicore.rs @@ -43,6 +43,7 @@ //! dispatch_from_dyn: unsize, pin //! hash: sized //! include: +//! include_bytes: //! index: sized //! infallible: //! int_impl: size_of, transmute @@ -2040,6 +2041,14 @@ macro_rules! include { } // endregion:include + // region:include_bytes + #[rustc_builtin_macro] + #[macro_export] + macro_rules! include_bytes { + ($file:expr $(,)?) => {{ /* compiler built-in */ }}; + } + // endregion:include_bytes + // region:concat #[rustc_builtin_macro] #[macro_export] From dc1ac63b3b4b687763f2bc764aef49e29acf380c Mon Sep 17 00:00:00 2001 From: Geoffry Song Date: Mon, 2 Feb 2026 13:41:22 -0800 Subject: [PATCH 18/53] Parse `try bikeshed T {}` syntax --- .../parser/src/grammar/expressions/atom.rs | 6 +++ .../parser/src/syntax_kind/generated.rs | 8 +++ .../parser/err/0042_weird_blocks.rast | 3 +- .../parser/inline/ok/try_block_expr.rast | 37 ++++++++++++- .../parser/inline/ok/try_block_expr.rs | 1 + .../rust-analyzer/crates/syntax/rust.ungram | 5 +- .../crates/syntax/src/ast/generated/nodes.rs | 54 ++++++++++++++++++- .../xtask/src/codegen/grammar/ast_src.rs | 2 +- 8 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/tools/rust-analyzer/crates/parser/src/grammar/expressions/atom.rs b/src/tools/rust-analyzer/crates/parser/src/grammar/expressions/atom.rs index d83e2eb2b4ae..b75474ee2b86 100644 --- a/src/tools/rust-analyzer/crates/parser/src/grammar/expressions/atom.rs +++ b/src/tools/rust-analyzer/crates/parser/src/grammar/expressions/atom.rs @@ -976,11 +976,17 @@ fn break_expr(p: &mut Parser<'_>, r: Restrictions) -> CompletedMarker { // test try_block_expr // fn foo() { // let _ = try {}; +// let _ = try bikeshed T {}; // } fn try_block_expr(p: &mut Parser<'_>, m: Option) -> CompletedMarker { assert!(p.at(T![try])); let m = m.unwrap_or_else(|| p.start()); + let try_modifier = p.start(); p.bump(T![try]); + if p.eat_contextual_kw(T![bikeshed]) { + type_(p); + } + try_modifier.complete(p, TRY_BLOCK_MODIFIER); if p.at(T!['{']) { stmt_list(p); } else { diff --git a/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs b/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs index 5d22d966b2b7..a2295e449550 100644 --- a/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs +++ b/src/tools/rust-analyzer/crates/parser/src/syntax_kind/generated.rs @@ -114,6 +114,7 @@ pub enum SyntaxKind { ATT_SYNTAX_KW, AUTO_KW, AWAIT_KW, + BIKESHED_KW, BUILTIN_KW, CLOBBER_ABI_KW, DEFAULT_KW, @@ -285,6 +286,7 @@ pub enum SyntaxKind { STRUCT, TOKEN_TREE, TRAIT, + TRY_BLOCK_MODIFIER, TRY_EXPR, TUPLE_EXPR, TUPLE_FIELD, @@ -458,6 +460,7 @@ pub const fn text(self) -> &'static str { | STRUCT | TOKEN_TREE | TRAIT + | TRY_BLOCK_MODIFIER | TRY_EXPR | TUPLE_EXPR | TUPLE_FIELD @@ -596,6 +599,7 @@ pub const fn text(self) -> &'static str { ASM_KW => "asm", ATT_SYNTAX_KW => "att_syntax", AUTO_KW => "auto", + BIKESHED_KW => "bikeshed", BUILTIN_KW => "builtin", CLOBBER_ABI_KW => "clobber_abi", DEFAULT_KW => "default", @@ -698,6 +702,7 @@ pub fn is_contextual_keyword(self, edition: Edition) -> bool { ASM_KW => true, ATT_SYNTAX_KW => true, AUTO_KW => true, + BIKESHED_KW => true, BUILTIN_KW => true, CLOBBER_ABI_KW => true, DEFAULT_KW => true, @@ -788,6 +793,7 @@ pub fn is_keyword(self, edition: Edition) -> bool { ASM_KW => true, ATT_SYNTAX_KW => true, AUTO_KW => true, + BIKESHED_KW => true, BUILTIN_KW => true, CLOBBER_ABI_KW => true, DEFAULT_KW => true, @@ -941,6 +947,7 @@ pub fn from_contextual_keyword(ident: &str, edition: Edition) -> Option ASM_KW, "att_syntax" => ATT_SYNTAX_KW, "auto" => AUTO_KW, + "bikeshed" => BIKESHED_KW, "builtin" => BUILTIN_KW, "clobber_abi" => CLOBBER_ABI_KW, "default" => DEFAULT_KW, @@ -1112,6 +1119,7 @@ pub fn from_char(c: char) -> Option { [asm] => { $ crate :: SyntaxKind :: ASM_KW }; [att_syntax] => { $ crate :: SyntaxKind :: ATT_SYNTAX_KW }; [auto] => { $ crate :: SyntaxKind :: AUTO_KW }; + [bikeshed] => { $ crate :: SyntaxKind :: BIKESHED_KW }; [builtin] => { $ crate :: SyntaxKind :: BUILTIN_KW }; [clobber_abi] => { $ crate :: SyntaxKind :: CLOBBER_ABI_KW }; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW }; diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/err/0042_weird_blocks.rast b/src/tools/rust-analyzer/crates/parser/test_data/parser/err/0042_weird_blocks.rast index d6d2e75cca67..9e4e9dbf9d25 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/err/0042_weird_blocks.rast +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/err/0042_weird_blocks.rast @@ -45,7 +45,8 @@ SOURCE_FILE WHITESPACE " " EXPR_STMT BLOCK_EXPR - TRY_KW "try" + TRY_BLOCK_MODIFIER + TRY_KW "try" WHITESPACE " " LITERAL INT_NUMBER "92" diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rast b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rast index aec8fbf4775c..472ce711c5fe 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rast +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rast @@ -21,7 +21,42 @@ SOURCE_FILE EQ "=" WHITESPACE " " BLOCK_EXPR - TRY_KW "try" + TRY_BLOCK_MODIFIER + TRY_KW "try" + WHITESPACE " " + STMT_LIST + L_CURLY "{" + R_CURLY "}" + SEMICOLON ";" + WHITESPACE "\n " + LET_STMT + LET_KW "let" + WHITESPACE " " + WILDCARD_PAT + UNDERSCORE "_" + WHITESPACE " " + EQ "=" + WHITESPACE " " + BLOCK_EXPR + TRY_BLOCK_MODIFIER + TRY_KW "try" + WHITESPACE " " + BIKESHED_KW "bikeshed" + WHITESPACE " " + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + IDENT "T" + GENERIC_ARG_LIST + L_ANGLE "<" + TYPE_ARG + PATH_TYPE + PATH + PATH_SEGMENT + NAME_REF + IDENT "U" + R_ANGLE ">" WHITESPACE " " STMT_LIST L_CURLY "{" diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rs b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rs index 0f1b41eb64b4..719980473c3b 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rs +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/try_block_expr.rs @@ -1,3 +1,4 @@ fn foo() { let _ = try {}; + let _ = try bikeshed T {}; } diff --git a/src/tools/rust-analyzer/crates/syntax/rust.ungram b/src/tools/rust-analyzer/crates/syntax/rust.ungram index 991fe7d83a0e..544053408f73 100644 --- a/src/tools/rust-analyzer/crates/syntax/rust.ungram +++ b/src/tools/rust-analyzer/crates/syntax/rust.ungram @@ -472,8 +472,11 @@ RefExpr = TryExpr = Attr* Expr '?' +TryBlockModifier = + 'try' ('bikeshed' Type)? + BlockExpr = - Attr* Label? ('try' | 'unsafe' | ('async' 'move'?) | ('gen' 'move'?) | 'const') StmtList + Attr* Label? (TryBlockModifier | 'unsafe' | ('async' 'move'?) | ('gen' 'move'?) | 'const') StmtList PrefixExpr = Attr* op:('-' | '!' | '*') Expr diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs index 7b9f5b9166bb..c4e72eafa793 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/generated/nodes.rs @@ -323,6 +323,8 @@ pub fn label(&self) -> Option