//! Implementation of "adjustment" inlay hints: //! ```no_run //! let _: u32 = /* */ loop {}; //! let _: &u32 = /* &* */ &mut 0; //! ``` use std::ops::Not; use either::Either; use hir::{ Adjust, Adjustment, AutoBorrow, DisplayTarget, HirDisplay, Mutability, OverloadedDeref, PointerCast, Safety, }; use ide_db::famous_defs::FamousDefs; use ide_db::text_edit::TextEditBuilder; use syntax::ast::{self, AstNode, prec::ExprPrecedence}; use crate::{ AdjustmentHints, AdjustmentHintsMode, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintPosition, InlayHintsConfig, InlayKind, InlayTooltip, }; pub(super) fn hints( acc: &mut Vec, FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig<'_>, display_target: DisplayTarget, expr: &ast::Expr, ) -> Option<()> { if config.adjustment_hints_hide_outside_unsafe && !sema.is_inside_unsafe(expr) { return None; } if config.adjustment_hints == AdjustmentHints::Never { return None; } // ParenExpr resolve to their contained expressions HIR so they will dupe these hints if let ast::Expr::ParenExpr(_) = expr { return None; } if let ast::Expr::BlockExpr(b) = expr && !b.is_standalone() { return None; } let descended = sema.descend_node_into_attributes(expr.clone()).pop(); let desc_expr = descended.as_ref().unwrap_or(expr); let mut adjustments = sema.expr_adjustments(desc_expr).filter(|it| !it.is_empty())?; if config.adjustment_hints_disable_reborrows { // Remove consecutive deref-ref, i.e. reborrows. let mut i = 0; while i < adjustments.len().saturating_sub(1) { let [current, next, ..] = &adjustments[i..] else { unreachable!() }; if matches!(current.kind, Adjust::Deref(None)) && matches!(next.kind, Adjust::Borrow(AutoBorrow::Ref(_))) { adjustments.splice(i..i + 2, []); } else { i += 1; } } } if let ast::Expr::BlockExpr(_) | ast::Expr::IfExpr(_) | ast::Expr::MatchExpr(_) = desc_expr { // Don't show unnecessary reborrows for these, they will just repeat the inner ones again if matches!( &*adjustments, [Adjustment { kind: Adjust::Deref(_), source, .. }, Adjustment { kind: Adjust::Borrow(_), target, .. }] if source == target ) { return None; } } let (postfix, needs_outer_parens, needs_inner_parens) = mode_and_needs_parens_for_adjustment_hints(expr, config.adjustment_hints_mode); let range = expr.syntax().text_range(); let mut pre = InlayHint { range, position: InlayHintPosition::Before, pad_left: false, pad_right: false, kind: InlayKind::Adjustment, label: InlayHintLabel::default(), text_edit: None, resolve_parent: Some(range), }; let mut post = InlayHint { range, position: InlayHintPosition::After, pad_left: false, pad_right: false, kind: InlayKind::Adjustment, label: InlayHintLabel::default(), text_edit: None, resolve_parent: Some(range), }; if needs_outer_parens || (postfix && needs_inner_parens) { pre.label.append_str("("); } if postfix && needs_inner_parens { post.label.append_str(")"); } let mut iter = if postfix { Either::Left(adjustments.into_iter()) } else { Either::Right(adjustments.into_iter().rev()) }; let iter: &mut dyn Iterator = iter.as_mut().either(|it| it as _, |it| it as _); let mut has_adjustments = false; let mut allow_edit = !postfix; for Adjustment { source, target, kind } in iter { if source == target { cov_mark::hit!(same_type_adjustment); continue; } has_adjustments = true; let (text, coercion, detailed_tooltip) = match kind { Adjust::NeverToAny if config.adjustment_hints == AdjustmentHints::Always => { allow_edit = false; ( "", "never to any", "Coerces the never type `!` into any other type. This happens in code paths that never return, like after `panic!()` or `return`.", ) } Adjust::Deref(None) => ( "*", "dereference", "Built-in dereference of a reference to access the underlying value. The compiler inserts `*` to get the value from `&T`.", ), Adjust::Deref(Some(OverloadedDeref(Mutability::Shared))) => ( "*", "`Deref` dereference", "Dereference via the `Deref` trait. Used for types like `Box` or `Rc` so they act like plain `T`.", ), Adjust::Deref(Some(OverloadedDeref(Mutability::Mut))) => ( "*", "`DerefMut` dereference", "Mutable dereference using the `DerefMut` trait. Enables smart pointers to give mutable access to their inner values.", ), Adjust::Borrow(AutoBorrow::Ref(Mutability::Shared)) => ( "&", "shared borrow", "Inserts `&` to create a shared reference. Lets you use a value without moving or cloning it.", ), Adjust::Borrow(AutoBorrow::Ref(Mutability::Mut)) => ( "&mut ", "mutable borrow", "Inserts `&mut` to create a unique, mutable reference. Lets you modify a value without taking ownership.", ), Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Shared)) => ( "&raw const ", "const raw pointer", "Converts a reference to a raw const pointer `*const T`. Often used when working with FFI or unsafe code.", ), Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Mut)) => ( "&raw mut ", "mut raw pointer", "Converts a mutable reference to a raw mutable pointer `*mut T`. Allows mutation in unsafe contexts.", ), // some of these could be represented via `as` casts, but that's not too nice and // handling everything as a prefix expr makes the `(` and `)` insertion easier Adjust::Pointer(cast) if config.adjustment_hints == AdjustmentHints::Always => { allow_edit = false; match cast { PointerCast::ReifyFnPointer => ( "", "fn item to fn pointer", "Converts a named function to a function pointer `fn()`. Useful when passing functions as values.", ), PointerCast::UnsafeFnPointer => ( "", "safe fn pointer to unsafe fn pointer", "Coerces a safe function pointer to an unsafe one. Allows calling it in an unsafe context.", ), PointerCast::ClosureFnPointer(Safety::Unsafe) => ( "", "closure to unsafe fn pointer", "Converts a non-capturing closure to an unsafe function pointer. Required for use in `extern` or unsafe APIs.", ), PointerCast::ClosureFnPointer(Safety::Safe) => ( "", "closure to fn pointer", "Converts a non-capturing closure to a function pointer. Lets closures behave like plain functions.", ), PointerCast::MutToConstPointer => ( "", "mut ptr to const ptr", "Coerces `*mut T` to `*const T`. Safe because const pointers restrict what you can do.", ), PointerCast::ArrayToPointer => ( "", "array to pointer", "Converts an array to a pointer to its first element. Similar to how arrays decay to pointers in C.", ), PointerCast::Unsize => ( "", "unsize coercion", "Converts a sized type to an unsized one. Used for things like turning arrays into slices or concrete types into trait objects.", ), } } _ => continue, }; let label = InlayHintLabelPart { text: if postfix { format!(".{}", text.trim_end()) } else { text.to_owned() }, linked_location: None, tooltip: Some(config.lazy_tooltip(|| { InlayTooltip::Markdown(format!( "`{}` → `{}`\n\n**{}**\n\n{}", source.display(sema.db, display_target), target.display(sema.db, display_target), coercion, detailed_tooltip )) })), }; if postfix { &mut post } else { &mut pre }.label.append_part(label); } if !has_adjustments { return None; } if !postfix && needs_inner_parens { pre.label.append_str("("); } if needs_outer_parens || (!postfix && needs_inner_parens) { post.label.append_str(")"); } let mut pre = pre.label.parts.is_empty().not().then_some(pre); let mut post = post.label.parts.is_empty().not().then_some(post); if pre.is_none() && post.is_none() { return None; } if allow_edit { let edit = Some(config.lazy_text_edit(|| { let mut b = TextEditBuilder::default(); if let Some(pre) = &pre { b.insert( pre.range.start(), pre.label.parts.iter().map(|part| &*part.text).collect::(), ); } if let Some(post) = &post { b.insert( post.range.end(), post.label.parts.iter().map(|part| &*part.text).collect::(), ); } b.finish() })); match (&mut pre, &mut post) { (Some(pre), Some(post)) => { pre.text_edit = edit.clone(); post.text_edit = edit; } (Some(pre), None) => pre.text_edit = edit, (None, Some(post)) => post.text_edit = edit, (None, None) => (), } } acc.extend(pre); acc.extend(post); Some(()) } /// Returns whatever the hint should be postfix and if we need to add parentheses on the inside and/or outside of `expr`, /// if we are going to add (`postfix`) adjustments hints to it. fn mode_and_needs_parens_for_adjustment_hints( expr: &ast::Expr, mode: AdjustmentHintsMode, ) -> (bool, bool, bool) { use {AdjustmentHintsMode::*, std::cmp::Ordering::*}; match mode { Prefix | Postfix => { let postfix = matches!(mode, Postfix); let (inside, outside) = needs_parens_for_adjustment_hints(expr, postfix); (postfix, inside, outside) } PreferPrefix | PreferPostfix => { let prefer_postfix = matches!(mode, PreferPostfix); let (pre_inside, pre_outside) = needs_parens_for_adjustment_hints(expr, false); let prefix = (false, pre_inside, pre_outside); let pre_count = pre_inside as u8 + pre_outside as u8; let (post_inside, post_outside) = needs_parens_for_adjustment_hints(expr, true); let postfix = (true, post_inside, post_outside); let post_count = post_inside as u8 + post_outside as u8; match pre_count.cmp(&post_count) { Less => prefix, Greater => postfix, Equal if prefer_postfix => postfix, Equal => prefix, } } } } /// Returns whatever we need to add parentheses on the inside and/or outside of `expr`, /// if we are going to add (`postfix`) adjustments hints to it. fn needs_parens_for_adjustment_hints(expr: &ast::Expr, postfix: bool) -> (bool, bool) { let prec = expr.precedence(); if postfix { let needs_inner_parens = prec.needs_parentheses_in(ExprPrecedence::Postfix); // given we are the higher precedence, no parent expression will have stronger requirements let needs_outer_parens = false; (needs_outer_parens, needs_inner_parens) } else { let needs_inner_parens = prec.needs_parentheses_in(ExprPrecedence::Prefix); let parent = expr .syntax() .parent() .and_then(ast::Expr::cast) // if we are already wrapped, great, no need to wrap again .filter(|it| !matches!(it, ast::Expr::ParenExpr(_))) .map(|it| it.precedence()) .filter(|&prec| prec != ExprPrecedence::Unambiguous); // if we have no parent, we don't need outer parens to disambiguate // otherwise anything with higher precedence than what we insert needs to wrap us let needs_outer_parens = parent .is_some_and(|parent_prec| ExprPrecedence::Prefix.needs_parentheses_in(parent_prec)); (needs_outer_parens, needs_inner_parens) } } #[cfg(test)] mod tests { use crate::{ AdjustmentHints, AdjustmentHintsMode, InlayHintsConfig, inlay_hints::tests::{DISABLED_CONFIG, check_with_config}, }; #[test] fn adjustment_hints_prefix() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG }, r#" //- minicore: coerce_unsized, fn, eq, index, dispatch_from_dyn, builtin_impls fn main() { let _: u32 = loop {}; //^^^^^^^ let _: &u32 = &mut 0; //^^^^^^&* let _: &mut u32 = &mut 0; //^^^^^^&mut * let _: *const u32 = &mut 0; //^^^^^^&raw const * let _: *mut u32 = &mut 0; //^^^^^^&raw mut * let _: fn() = main; //^^^^ let _: unsafe fn() = main; //^^^^ let _: unsafe fn() = main as fn(); //^^^^^^^^^^^^( //^^^^^^^^^^^^) //^^^^ let _: fn() = || {}; //^^^^^ let _: unsafe fn() = || {}; //^^^^^ let _: *const u32 = &mut 0u32 as *mut u32; //^^^^^^^^^^^^^^^^^^^^^( //^^^^^^^^^^^^^^^^^^^^^) //^^^^^^^^^&raw mut * let _: &mut [_] = &mut [0; 0]; //^^^^^^^^^^^&mut * Struct.consume(); Struct.by_ref(); //^^^^^^(& //^^^^^^) Struct.by_ref_mut(); //^^^^^^(&mut $ //^^^^^^) (&Struct).consume(); //^^^^^^^* (&Struct).by_ref(); //^^^^^^^&* (&mut Struct).consume(); //^^^^^^^^^^^* (&mut Struct).by_ref(); //^^^^^^^^^^^&* (&mut Struct).by_ref_mut(); //^^^^^^^^^^^&mut * // Check that block-like expressions don't duplicate hints let _: &mut [u32] = (&mut []); //^^^^^^^&mut * let _: &mut [u32] = { &mut [] }; //^^^^^^^&mut * let _: &mut [u32] = unsafe { &mut [] }; //^^^^^^^&mut * let _: &mut [u32] = if true { &mut [] //^^^^^^^&mut * } else { loop {} //^^^^^^^ }; let _: &mut [u32] = match () { () => &mut [] }; //^^^^^^^&mut * let _: &mut dyn Fn() = &mut || (); //^^^^^^^^^^&mut * () == (); // ^^& // ^^& (()) == {()}; // ^^& // ^^^^& let closure: &dyn Fn = &|| (); //^^^^^^&* closure(); Struct[0]; //^^^^^^(& //^^^^^^) &mut Struct[0]; //^^^^^^(&mut $ //^^^^^^) let _: (&mut (),) = (&mut (),); //^^^^^^^&mut * } #[derive(Copy, Clone)] struct Struct; impl Struct { fn consume(self) {} fn by_ref(&self) {} fn by_ref_mut(&mut self) {} } struct StructMut; impl core::ops::Index for Struct { type Output = (); } impl core::ops::IndexMut for Struct {} "#, ); } #[test] fn adjustment_hints_postfix() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_mode: AdjustmentHintsMode::Postfix, ..DISABLED_CONFIG }, r#" //- minicore: coerce_unsized, fn, eq, index, dispatch_from_dyn, builtin_impls fn main() { Struct.consume(); Struct.by_ref(); //^^^^^^.& Struct.by_ref_mut(); //^^^^^^.&mut (&Struct).consume(); //^^^^^^^( //^^^^^^^).* (&Struct).by_ref(); //^^^^^^^( //^^^^^^^).*.& (&mut Struct).consume(); //^^^^^^^^^^^( //^^^^^^^^^^^).* (&mut Struct).by_ref(); //^^^^^^^^^^^( //^^^^^^^^^^^).*.& (&mut Struct).by_ref_mut(); //^^^^^^^^^^^( //^^^^^^^^^^^).*.&mut // Check that block-like expressions don't duplicate hints let _: &mut [u32] = (&mut []); //^^^^^^^( //^^^^^^^).*.&mut. let _: &mut [u32] = { &mut [] }; //^^^^^^^( //^^^^^^^).*.&mut. let _: &mut [u32] = unsafe { &mut [] }; //^^^^^^^( //^^^^^^^).*.&mut. let _: &mut [u32] = if true { &mut [] //^^^^^^^( //^^^^^^^).*.&mut. } else { loop {} //^^^^^^^. }; let _: &mut [u32] = match () { () => &mut [] }; //^^^^^^^( //^^^^^^^).*.&mut. let _: &mut dyn Fn() = &mut || (); //^^^^^^^^^^( //^^^^^^^^^^).*.&mut. () == (); // ^^.& // ^^.& (()) == {()}; // ^^.& // ^^^^.& let closure: &dyn Fn = &|| (); //^^^^^^( //^^^^^^).*.&. closure(); Struct[0]; //^^^^^^.& &mut Struct[0]; //^^^^^^.&mut let _: (&mut (),) = (&mut (),); //^^^^^^^( //^^^^^^^).*.&mut } #[derive(Copy, Clone)] struct Struct; impl Struct { fn consume(self) {} fn by_ref(&self) {} fn by_ref_mut(&mut self) {} } struct StructMut; impl core::ops::Index for Struct { type Output = (); } impl core::ops::IndexMut for Struct {} "#, ); } #[test] fn adjustment_hints_prefer_prefix() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_mode: AdjustmentHintsMode::PreferPrefix, ..DISABLED_CONFIG }, r#" fn main() { let _: u32 = loop {}; //^^^^^^^ Struct.by_ref(); //^^^^^^.& let (): () = return (); //^^^^^^^^^ struct Struct; impl Struct { fn by_ref(&self) {} } } "#, ) } #[test] fn adjustment_hints_prefer_postfix() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_mode: AdjustmentHintsMode::PreferPostfix, ..DISABLED_CONFIG }, r#" fn main() { let _: u32 = loop {}; //^^^^^^^. Struct.by_ref(); //^^^^^^.& let (): () = return (); //^^^^^^^^^ struct Struct; impl Struct { fn by_ref(&self) {} } } "#, ) } #[test] fn never_to_never_is_never_shown() { cov_mark::check!(same_type_adjustment); check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG }, r#" fn never() -> ! { return loop {}; } fn or_else() { let () = () else { return }; } "#, ) } #[test] fn adjustment_hints_unsafe_only() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_hide_outside_unsafe: true, ..DISABLED_CONFIG }, r#" unsafe fn enabled() { f(&&()); //^^^^&** } fn disabled() { f(&&()); } fn mixed() { f(&&()); unsafe { f(&&()); //^^^^&** } } const _: () = { f(&&()); unsafe { f(&&()); //^^^^&** } }; static STATIC: () = { f(&&()); unsafe { f(&&()); //^^^^&** } }; enum E { Disable = { f(&&()); 0 }, Enable = unsafe { f(&&()); 1 }, //^^^^&** } const fn f(_: &()) {} "#, ) } #[test] fn adjustment_hints_unsafe_only_with_item() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_hide_outside_unsafe: true, ..DISABLED_CONFIG }, r#" fn a() { struct Struct; impl Struct { fn by_ref(&self) {} } _ = Struct.by_ref(); _ = unsafe { Struct.by_ref() }; //^^^^^^(& //^^^^^^) } "#, ); } #[test] fn let_stmt_explicit_ty() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG }, r#" fn main() { let () = return; //^^^^^^ let (): () = return; //^^^^^^ } "#, ) } // regression test for a stackoverflow in hir display code #[test] fn adjustment_hints_method_call_on_impl_trait_self() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG }, r#" //- minicore: slice, coerce_unsized trait T {} fn hello(it: &&[impl T]) { it.len(); //^^(&** //^^) } "#, ); } #[test] fn disable_reborrows() { check_with_config( InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, adjustment_hints_disable_reborrows: true, ..DISABLED_CONFIG }, r#" #![rustc_coherence_is_core] trait ToOwned { type Owned; fn to_owned(&self) -> Self::Owned; } struct String; impl ToOwned for str { type Owned = String; fn to_owned(&self) -> Self::Owned { String } } fn a(s: &String) {} fn main() { let s = "".to_owned(); a(&s) } "#, ); } }