blob: 6d6bd315ebb45131cb89f1c0484a211ff2eb35eb [file] [log] [blame]
//! Implementation of "adjustment" inlay hints:
//! ```no_run
//! let _: u32 = /* <never-to-any> */ loop {};
//! let _: &u32 = /* &* */ &mut 0;
//! ```
use either::Either;
use hir::{
Adjust, Adjustment, AutoBorrow, HirDisplay, Mutability, OverloadedDeref, PointerCast, Safety,
Semantics,
};
use ide_db::RootDatabase;
use stdx::never;
use syntax::{
ast::{self, make, AstNode},
ted,
};
use crate::{
AdjustmentHints, AdjustmentHintsMode, InlayHint, InlayHintLabel, InlayHintPosition,
InlayHintsConfig, InlayKind, InlayTooltip,
};
pub(super) fn hints(
acc: &mut Vec<InlayHint>,
sema: &Semantics<'_, RootDatabase>,
config: &InlayHintsConfig,
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 {
if !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 adjustments = sema.expr_adjustments(desc_expr).filter(|it| !it.is_empty())?;
if let ast::Expr::BlockExpr(_) | ast::Expr::IfExpr(_) | ast::Expr::MatchExpr(_) = desc_expr {
if let [Adjustment { kind: Adjust::Deref(_), source, .. }, Adjustment { kind: Adjust::Borrow(_), source: _, target }] =
&*adjustments
{
// Don't show unnecessary reborrows for these, they will just repeat the inner ones again
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);
if needs_outer_parens {
acc.push(InlayHint::opening_paren_before(
InlayKind::Adjustment,
expr.syntax().text_range(),
));
}
if postfix && needs_inner_parens {
acc.push(InlayHint::opening_paren_before(
InlayKind::Adjustment,
expr.syntax().text_range(),
));
acc.push(InlayHint::closing_paren_after(InlayKind::Adjustment, expr.syntax().text_range()));
}
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 _);
for Adjustment { source, target, kind } in iter {
if source == target {
cov_mark::hit!(same_type_adjustment);
continue;
}
// FIXME: Add some nicer tooltips to each of these
let (text, coercion) = match kind {
Adjust::NeverToAny if config.adjustment_hints == AdjustmentHints::Always => {
("<never-to-any>", "never to any")
}
Adjust::Deref(None) => ("*", "dereference"),
Adjust::Deref(Some(OverloadedDeref(Mutability::Shared))) => {
("*", "`Deref` dereference")
}
Adjust::Deref(Some(OverloadedDeref(Mutability::Mut))) => {
("*", "`DerefMut` dereference")
}
Adjust::Borrow(AutoBorrow::Ref(Mutability::Shared)) => ("&", "borrow"),
Adjust::Borrow(AutoBorrow::Ref(Mutability::Mut)) => ("&mut ", "unique borrow"),
Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Shared)) => {
("&raw const ", "const pointer borrow")
}
Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Mut)) => {
("&raw mut ", "mut pointer borrow")
}
// 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 => {
match cast {
PointerCast::ReifyFnPointer => {
("<fn-item-to-fn-pointer>", "fn item to fn pointer")
}
PointerCast::UnsafeFnPointer => (
"<safe-fn-pointer-to-unsafe-fn-pointer>",
"safe fn pointer to unsafe fn pointer",
),
PointerCast::ClosureFnPointer(Safety::Unsafe) => {
("<closure-to-unsafe-fn-pointer>", "closure to unsafe fn pointer")
}
PointerCast::ClosureFnPointer(Safety::Safe) => {
("<closure-to-fn-pointer>", "closure to fn pointer")
}
PointerCast::MutToConstPointer => {
("<mut-ptr-to-const-ptr>", "mut ptr to const ptr")
}
PointerCast::ArrayToPointer => ("<array-ptr-to-element-ptr>", ""),
PointerCast::Unsize => ("<unsize>", "unsize"),
}
}
_ => continue,
};
acc.push(InlayHint {
range: expr.syntax().text_range(),
pad_left: false,
pad_right: false,
position: if postfix { InlayHintPosition::After } else { InlayHintPosition::Before },
kind: InlayKind::Adjustment,
label: InlayHintLabel::simple(
if postfix { format!(".{}", text.trim_end()) } else { text.to_owned() },
Some(InlayTooltip::Markdown(format!(
"`{}` → `{}` ({coercion} coercion)",
source.display(sema.db),
target.display(sema.db),
))),
None,
),
text_edit: None,
});
}
if !postfix && needs_inner_parens {
acc.push(InlayHint::opening_paren_before(
InlayKind::Adjustment,
expr.syntax().text_range(),
));
acc.push(InlayHint::closing_paren_after(InlayKind::Adjustment, expr.syntax().text_range()));
}
if needs_outer_parens {
acc.push(InlayHint::closing_paren_after(InlayKind::Adjustment, expr.syntax().text_range()));
}
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 {std::cmp::Ordering::*, AdjustmentHintsMode::*};
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) {
// This is a very miserable pile of hacks...
//
// `Expr::needs_parens_in` requires that the expression is the child of the other expression,
// that is supposed to be its parent.
//
// But we want to check what would happen if we add `*`/`.*` to the inner expression.
// To check for inner we need `` expr.needs_parens_in(`*expr`) ``,
// to check for outer we need `` `*expr`.needs_parens_in(parent) ``,
// where "expr" is the `expr` parameter, `*expr` is the edited `expr`,
// and "parent" is the parent of the original expression...
//
// For this we utilize mutable trees, which is a HACK, but it works.
//
// FIXME: comeup with a better API for `needs_parens_in`, so that we don't have to do *this*
// Make `&expr`/`expr?`
let dummy_expr = {
// `make::*` function go through a string, so they parse wrongly.
// for example `` make::expr_try(`|| a`) `` would result in a
// `|| (a?)` and not `(|| a)?`.
//
// Thus we need dummy parens to preserve the relationship we want.
// The parens are then simply ignored by the following code.
let dummy_paren = make::expr_paren(expr.clone());
if postfix {
make::expr_try(dummy_paren)
} else {
make::expr_ref(dummy_paren, false)
}
};
// Do the dark mutable tree magic.
// This essentially makes `dummy_expr` and `expr` switch places (families),
// so that `expr`'s parent is not `dummy_expr`'s parent.
let dummy_expr = dummy_expr.clone_for_update();
let expr = expr.clone_for_update();
ted::replace(expr.syntax(), dummy_expr.syntax());
let parent = dummy_expr.syntax().parent();
let Some(expr) = (|| {
if postfix {
let ast::Expr::TryExpr(e) = &dummy_expr else { return None };
let Some(ast::Expr::ParenExpr(e)) = e.expr() else { return None };
e.expr()
} else {
let ast::Expr::RefExpr(e) = &dummy_expr else { return None };
let Some(ast::Expr::ParenExpr(e)) = e.expr() else { return None };
e.expr()
}
})() else {
never!("broken syntax tree?\n{:?}\n{:?}", expr, dummy_expr);
return (true, true);
};
// At this point
// - `parent` is the parent of the original expression
// - `dummy_expr` is the original expression wrapped in the operator we want (`*`/`.*`)
// - `expr` is the clone of the original expression (with `dummy_expr` as the parent)
let needs_outer_parens = parent.map_or(false, |p| dummy_expr.needs_parens_in(p));
let needs_inner_parens = expr.needs_parens_in(dummy_expr.syntax().clone());
(needs_outer_parens, needs_inner_parens)
}
#[cfg(test)]
mod tests {
use crate::{
inlay_hints::tests::{check_with_config, DISABLED_CONFIG},
AdjustmentHints, AdjustmentHintsMode, InlayHintsConfig,
};
#[test]
fn adjustment_hints() {
check_with_config(
InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG },
r#"
//- minicore: coerce_unsized, fn, eq, index
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>
//^^^^^^^^^^^^(
//^^^^^^^^^^^^)
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>
//^^^^^^^^^^^^^^^^^^^^^(
//^^^^^^^^^^^^^^^^^^^^^)
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 = || ();
closure();
//^^^^^^^(
//^^^^^^^&
//^^^^^^^)
Struct[0];
//^^^^^^(
//^^^^^^&
//^^^^^^)
&mut Struct[0];
//^^^^^^(
//^^^^^^&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
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 = || ();
closure();
//^^^^^^^.&
Struct[0];
//^^^^^^.&
&mut Struct[0];
//^^^^^^.&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>
}
"#,
)
}
}