blob: 0fd587a728408ded9d9e76effc00669378b0f061 [file] [log] [blame]
//! Implementation of "adjustment" inlay hints:
//! ```no_run
//! let _: u32 = /* <never-to-any> */ 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::{base_db::salsa, 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<InlayHint>,
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<Item = _> = 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>",
"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<T>` or `Rc<T>` 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>",
"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>",
"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>",
"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>",
"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>",
"mut ptr to const ptr",
"Coerces `*mut T` to `*const T`. Safe because const pointers restrict what you can do.",
),
PointerCast::ArrayToPointer => (
"<array-ptr-to-element-ptr>",
"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>",
"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(|| {
salsa::attach(sema.db, || {
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::<String>(),
);
}
if let Some(post) = &post {
b.insert(
post.range.end(),
post.label.parts.iter().map(|part| &*part.text).collect::<String>(),
);
}
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
fn main() {
let _: u32 = loop {};
//^^^^^^^<never-to-any>
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;
//^^^^<fn-item-to-fn-pointer>
let _: unsafe fn() = main;
//^^^^<safe-fn-pointer-to-unsafe-fn-pointer><fn-item-to-fn-pointer>
let _: unsafe fn() = main as fn();
//^^^^^^^^^^^^<safe-fn-pointer-to-unsafe-fn-pointer>(
//^^^^^^^^^^^^)
//^^^^<fn-item-to-fn-pointer>
let _: fn() = || {};
//^^^^^<closure-to-fn-pointer>
let _: unsafe fn() = || {};
//^^^^^<closure-to-unsafe-fn-pointer>
let _: *const u32 = &mut 0u32 as *mut u32;
//^^^^^^^^^^^^^^^^^^^^^<mut-ptr-to-const-ptr>(
//^^^^^^^^^^^^^^^^^^^^^)
//^^^^^^^^^&raw mut *
let _: &mut [_] = &mut [0; 0];
//^^^^^^^^^^^<unsize>&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 []);
//^^^^^^^<unsize>&mut *
let _: &mut [u32] = { &mut [] };
//^^^^^^^<unsize>&mut *
let _: &mut [u32] = unsafe { &mut [] };
//^^^^^^^<unsize>&mut *
let _: &mut [u32] = if true {
&mut []
//^^^^^^^<unsize>&mut *
} else {
loop {}
//^^^^^^^<never-to-any>
};
let _: &mut [u32] = match () { () => &mut [] };
//^^^^^^^<unsize>&mut *
let _: &mut dyn Fn() = &mut || ();
//^^^^^^^^^^<unsize>&mut *
() == ();
// ^^&
// ^^&
(()) == {()};
// ^^&
// ^^^^&
let closure: &dyn Fn = &|| ();
//^^^^^^<unsize>&*
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<usize> 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
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.<unsize>
let _: &mut [u32] = { &mut [] };
//^^^^^^^(
//^^^^^^^).*.&mut.<unsize>
let _: &mut [u32] = unsafe { &mut [] };
//^^^^^^^(
//^^^^^^^).*.&mut.<unsize>
let _: &mut [u32] = if true {
&mut []
//^^^^^^^(
//^^^^^^^).*.&mut.<unsize>
} else {
loop {}
//^^^^^^^.<never-to-any>
};
let _: &mut [u32] = match () { () => &mut [] };
//^^^^^^^(
//^^^^^^^).*.&mut.<unsize>
let _: &mut dyn Fn() = &mut || ();
//^^^^^^^^^^(
//^^^^^^^^^^).*.&mut.<unsize>
() == ();
// ^^.&
// ^^.&
(()) == {()};
// ^^.&
// ^^^^.&
let closure: &dyn Fn = &|| ();
//^^^^^^(
//^^^^^^).*.&.<unsize>
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<usize> 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 {};
//^^^^^^^<never-to-any>
Struct.by_ref();
//^^^^^^.&
let (): () = return ();
//^^^^^^^^^<never-to-any>
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 {};
//^^^^^^^.<never-to-any>
Struct.by_ref();
//^^^^^^.&
let (): () = return ();
//^^^^^^^^^<never-to-any>
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;
//^^^^^^<never-to-any>
let (): () = return;
//^^^^^^<never-to-any>
}
"#,
)
}
// 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<RHS = Self> {}
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)
}
"#,
);
}
}