//! Implementation of "type" inlay hints: //! ```no_run //! fn f(a: i32, b: i32) -> i32 { a + b } //! let _x /* i32 */= f(4, 4); //! ``` use hir::{Semantics, TypeInfo}; use ide_db::{base_db::FileId, famous_defs::FamousDefs, RootDatabase}; use itertools::Itertools; use syntax::{ ast::{self, AstNode, HasName}, match_ast, }; use crate::{ inlay_hints::closure_has_block_body, InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, }; use super::label_of_ty; pub(super) fn hints( acc: &mut Vec, famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig, file_id: FileId, pat: &ast::IdentPat, ) -> Option<()> { if !config.type_hints { return None; } let descended = sema.descend_node_into_attributes(pat.clone()).pop(); let desc_pat = descended.as_ref().unwrap_or(pat); let ty = sema.type_of_pat(&desc_pat.clone().into())?.original; if should_not_display_type_hint(sema, config, pat, &ty) { return None; } let label = label_of_ty(famous_defs, config, ty)?; if config.hide_named_constructor_hints && is_named_constructor(sema, pat, &label.to_string()).is_some() { return None; } acc.push(InlayHint { range: match pat.name() { Some(name) => name.syntax().text_range(), None => pat.syntax().text_range(), }, kind: InlayKind::TypeHint, label, tooltip: pat .name() .map(|it| it.syntax().text_range()) .map(|it| InlayTooltip::HoverRanged(file_id, it)), }); Some(()) } fn should_not_display_type_hint( sema: &Semantics<'_, RootDatabase>, config: &InlayHintsConfig, bind_pat: &ast::IdentPat, pat_ty: &hir::Type, ) -> bool { let db = sema.db; if pat_ty.is_unknown() { return true; } if let Some(hir::Adt::Struct(s)) = pat_ty.as_adt() { if s.fields(db).is_empty() && s.name(db).to_smol_str() == bind_pat.to_string() { return true; } } if config.hide_closure_initialization_hints { if let Some(parent) = bind_pat.syntax().parent() { if let Some(it) = ast::LetStmt::cast(parent) { if let Some(ast::Expr::ClosureExpr(closure)) = it.initializer() { if closure_has_block_body(&closure) { return true; } } } } } for node in bind_pat.syntax().ancestors() { match_ast! { match node { ast::LetStmt(it) => return it.ty().is_some(), // FIXME: We might wanna show type hints in parameters for non-top level patterns as well ast::Param(it) => return it.ty().is_some(), ast::MatchArm(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), ast::LetExpr(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), ast::IfExpr(_) => return false, ast::WhileExpr(_) => return false, ast::ForExpr(it) => { // We *should* display hint only if user provided "in {expr}" and we know the type of expr (and it's not unit). // Type of expr should be iterable. return it.in_token().is_none() || it.iterable() .and_then(|iterable_expr| sema.type_of_expr(&iterable_expr)) .map(TypeInfo::original) .map_or(true, |iterable_ty| iterable_ty.is_unknown() || iterable_ty.is_unit()) }, _ => (), } } } false } fn is_named_constructor( sema: &Semantics<'_, RootDatabase>, pat: &ast::IdentPat, ty_name: &str, ) -> Option<()> { let let_node = pat.syntax().parent()?; let expr = match_ast! { match let_node { ast::LetStmt(it) => it.initializer(), ast::LetExpr(it) => it.expr(), _ => None, } }?; let expr = sema.descend_node_into_attributes(expr.clone()).pop().unwrap_or(expr); // unwrap postfix expressions let expr = match expr { ast::Expr::TryExpr(it) => it.expr(), ast::Expr::AwaitExpr(it) => it.expr(), expr => Some(expr), }?; let expr = match expr { ast::Expr::CallExpr(call) => match call.expr()? { ast::Expr::PathExpr(path) => path, _ => return None, }, ast::Expr::PathExpr(path) => path, _ => return None, }; let path = expr.path()?; let callable = sema.type_of_expr(&ast::Expr::PathExpr(expr))?.original.as_callable(sema.db); let callable_kind = callable.map(|it| it.kind()); let qual_seg = match callable_kind { Some(hir::CallableKind::Function(_) | hir::CallableKind::TupleEnumVariant(_)) => { path.qualifier()?.segment() } _ => path.segment(), }?; let ctor_name = match qual_seg.kind()? { ast::PathSegmentKind::Name(name_ref) => { match qual_seg.generic_arg_list().map(|it| it.generic_args()) { Some(generics) => format!("{name_ref}<{}>", generics.format(", ")), None => name_ref.to_string(), } } ast::PathSegmentKind::Type { type_ref: Some(ty), trait_ref: None } => ty.to_string(), _ => return None, }; (ctor_name == ty_name).then_some(()) } fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir::Type) -> bool { if let Some(hir::Adt::Enum(enum_data)) = pat_ty.as_adt() { let pat_text = bind_pat.to_string(); enum_data .variants(db) .into_iter() .map(|variant| variant.name(db).to_smol_str()) .any(|enum_name| enum_name == pat_text) } else { false } } #[cfg(test)] mod tests { // This module also contains tests for super::closure_ret use expect_test::expect; use syntax::{TextRange, TextSize}; use test_utils::extract_annotations; use crate::{fixture, inlay_hints::InlayHintsConfig}; use crate::inlay_hints::tests::{ check, check_expect, check_with_config, DISABLED_CONFIG, DISABLED_CONFIG_WITH_LINKS, TEST_CONFIG, }; use crate::ClosureReturnTypeHints; #[track_caller] fn check_types(ra_fixture: &str) { check_with_config(InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG }, ra_fixture); } #[test] fn type_hints_only() { check_types( r#" fn foo(a: i32, b: i32) -> i32 { a + b } fn main() { let _x = foo(4, 4); //^^ i32 }"#, ); } #[test] fn type_hints_bindings_after_at() { check_types( r#" //- minicore: option fn main() { let ref foo @ bar @ ref mut baz = 0; //^^^ &i32 //^^^ i32 //^^^ &mut i32 let [x @ ..] = [0]; //^ [i32; 1] if let x @ Some(_) = Some(0) {} //^ Option let foo @ (bar, baz) = (3, 3); //^^^ (i32, i32) //^^^ i32 //^^^ i32 }"#, ); } #[test] fn default_generic_types_should_not_be_displayed() { check( r#" struct Test { k: K, t: T } fn main() { let zz = Test { t: 23u8, k: 33 }; //^^ Test let zz_ref = &zz; //^^^^^^ &Test let test = || zz; //^^^^ || -> Test }"#, ); } #[test] fn shorten_iterators_in_associated_params() { check_types( r#" //- minicore: iterators use core::iter; pub struct SomeIter {} impl SomeIter { pub fn new() -> Self { SomeIter {} } pub fn push(&mut self, t: T) {} } impl Iterator for SomeIter { type Item = T; fn next(&mut self) -> Option { None } } fn main() { let mut some_iter = SomeIter::new(); //^^^^^^^^^ SomeIter>> some_iter.push(iter::repeat(2).take(2)); let iter_of_iters = some_iter.take(2); //^^^^^^^^^^^^^ impl Iterator> } "#, ); } #[test] fn iterator_hint_regression_issue_12674() { // Ensure we don't crash while solving the projection type of iterators. check_expect( InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG_WITH_LINKS }, r#" //- minicore: iterators struct S(T); impl S { fn iter(&self) -> Iter<'_, T> { loop {} } } struct Iter<'a, T: 'a>(&'a T); impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { loop {} } } struct Container<'a> { elements: S<&'a str>, } struct SliceIter<'a, T>(&'a T); impl<'a, T> Iterator for SliceIter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { loop {} } } fn main(a: SliceIter<'_, Container>) { a .filter_map(|c| Some(c.elements.iter().filter_map(|v| Some(v)))) .map(|e| e); } "#, expect![[r#" [ InlayHint { range: 484..554, kind: ChainingHint, label: [ "impl Iterator>", ], tooltip: Some( HoverRanged( FileId( 0, ), 484..554, ), ), }, InlayHint { range: 484..485, kind: ChainingHint, label: [ "", InlayHintLabelPart { text: "SliceIter", linked_location: Some( FileRange { file_id: FileId( 0, ), range: 289..298, }, ), }, "<", InlayHintLabelPart { text: "Container", linked_location: Some( FileRange { file_id: FileId( 0, ), range: 238..247, }, ), }, ">", ], tooltip: Some( HoverRanged( FileId( 0, ), 484..485, ), ), }, ] "#]], ); } #[test] fn infer_call_method_return_associated_types_with_generic() { check_types( r#" pub trait Default { fn default() -> Self; } pub trait Foo { type Bar: Default; } pub fn quux() -> T::Bar { let y = Default::default(); //^ ::Bar y } "#, ); } #[test] fn fn_hints() { check_types( r#" //- minicore: fn, sized fn foo() -> impl Fn() { loop {} } fn foo1() -> impl Fn(f64) { loop {} } fn foo2() -> impl Fn(f64, f64) { loop {} } fn foo3() -> impl Fn(f64, f64) -> u32 { loop {} } fn foo4() -> &'static dyn Fn(f64, f64) -> u32 { loop {} } fn foo5() -> &'static dyn Fn(&'static dyn Fn(f64, f64) -> u32, f64) -> u32 { loop {} } fn foo6() -> impl Fn(f64, f64) -> u32 + Sized { loop {} } fn foo7() -> *const (impl Fn(f64, f64) -> u32 + Sized) { loop {} } fn main() { let foo = foo(); // ^^^ impl Fn() let foo = foo1(); // ^^^ impl Fn(f64) let foo = foo2(); // ^^^ impl Fn(f64, f64) let foo = foo3(); // ^^^ impl Fn(f64, f64) -> u32 let foo = foo4(); // ^^^ &dyn Fn(f64, f64) -> u32 let foo = foo5(); // ^^^ &dyn Fn(&dyn Fn(f64, f64) -> u32, f64) -> u32 let foo = foo6(); // ^^^ impl Fn(f64, f64) -> u32 let foo = foo7(); // ^^^ *const impl Fn(f64, f64) -> u32 } "#, ) } #[test] fn check_hint_range_limit() { let fixture = r#" //- minicore: fn, sized fn foo() -> impl Fn() { loop {} } fn foo1() -> impl Fn(f64) { loop {} } fn foo2() -> impl Fn(f64, f64) { loop {} } fn foo3() -> impl Fn(f64, f64) -> u32 { loop {} } fn foo4() -> &'static dyn Fn(f64, f64) -> u32 { loop {} } fn foo5() -> &'static dyn Fn(&'static dyn Fn(f64, f64) -> u32, f64) -> u32 { loop {} } fn foo6() -> impl Fn(f64, f64) -> u32 + Sized { loop {} } fn foo7() -> *const (impl Fn(f64, f64) -> u32 + Sized) { loop {} } fn main() { let foo = foo(); let foo = foo1(); let foo = foo2(); // ^^^ impl Fn(f64, f64) let foo = foo3(); // ^^^ impl Fn(f64, f64) -> u32 let foo = foo4(); let foo = foo5(); let foo = foo6(); let foo = foo7(); } "#; let (analysis, file_id) = fixture::file(fixture); let expected = extract_annotations(&analysis.file_text(file_id).unwrap()); let inlay_hints = analysis .inlay_hints( &InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG }, file_id, Some(TextRange::new(TextSize::from(500), TextSize::from(600))), ) .unwrap(); let actual = inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::>(); assert_eq!(expected, actual, "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}"); } #[test] fn fn_hints_ptr_rpit_fn_parentheses() { check_types( r#" //- minicore: fn, sized trait Trait {} fn foo1() -> *const impl Fn() { loop {} } fn foo2() -> *const (impl Fn() + Sized) { loop {} } fn foo3() -> *const (impl Fn() + ?Sized) { loop {} } fn foo4() -> *const (impl Sized + Fn()) { loop {} } fn foo5() -> *const (impl ?Sized + Fn()) { loop {} } fn foo6() -> *const (impl Fn() + Trait) { loop {} } fn foo7() -> *const (impl Fn() + Sized + Trait) { loop {} } fn foo8() -> *const (impl Fn() + ?Sized + Trait) { loop {} } fn foo9() -> *const (impl Fn() -> u8 + ?Sized) { loop {} } fn foo10() -> *const (impl Fn() + Sized + ?Sized) { loop {} } fn main() { let foo = foo1(); // ^^^ *const impl Fn() let foo = foo2(); // ^^^ *const impl Fn() let foo = foo3(); // ^^^ *const (impl Fn() + ?Sized) let foo = foo4(); // ^^^ *const impl Fn() let foo = foo5(); // ^^^ *const (impl Fn() + ?Sized) let foo = foo6(); // ^^^ *const (impl Fn() + Trait) let foo = foo7(); // ^^^ *const (impl Fn() + Trait) let foo = foo8(); // ^^^ *const (impl Fn() + Trait + ?Sized) let foo = foo9(); // ^^^ *const (impl Fn() -> u8 + ?Sized) let foo = foo10(); // ^^^ *const impl Fn() } "#, ) } #[test] fn unit_structs_have_no_type_hints() { check_types( r#" //- minicore: result struct SyntheticSyntax; fn main() { match Ok(()) { Ok(_) => (), Err(SyntheticSyntax) => (), } }"#, ); } #[test] fn let_statement() { check_types( r#" #[derive(PartialEq)] enum Option { None, Some(T) } #[derive(PartialEq)] struct Test { a: Option, b: u8 } fn main() { struct InnerStruct {} let test = 54; //^^^^ i32 let test: i32 = 33; let mut test = 33; //^^^^ i32 let _ = 22; let test = "test"; //^^^^ &str let test = InnerStruct {}; //^^^^ InnerStruct let test = unresolved(); let test = (42, 'a'); //^^^^ (i32, char) let (a, (b, (c,)) = (2, (3, (9.2,)); //^ i32 ^ i32 ^ f64 let &x = &92; //^ i32 }"#, ); } #[test] fn if_expr() { check_types( r#" //- minicore: option struct Test { a: Option, b: u8 } fn main() { let test = Some(Test { a: Some(3), b: 1 }); //^^^^ Option if let None = &test {}; if let test = &test {}; //^^^^ &Option if let Some(test) = &test {}; //^^^^ &Test if let Some(Test { a, b }) = &test {}; //^ &Option ^ &u8 if let Some(Test { a: x, b: y }) = &test {}; //^ &Option ^ &u8 if let Some(Test { a: Some(x), b: y }) = &test {}; //^ &u32 ^ &u8 if let Some(Test { a: None, b: y }) = &test {}; //^ &u8 if let Some(Test { b: y, .. }) = &test {}; //^ &u8 if test == None {} }"#, ); } #[test] fn while_expr() { check_types( r#" //- minicore: option struct Test { a: Option, b: u8 } fn main() { let test = Some(Test { a: Some(3), b: 1 }); //^^^^ Option while let Some(Test { a: Some(x), b: y }) = &test {}; //^ &u32 ^ &u8 }"#, ); } #[test] fn match_arm_list() { check_types( r#" //- minicore: option struct Test { a: Option, b: u8 } fn main() { match Some(Test { a: Some(3), b: 1 }) { None => (), test => (), //^^^^ Option Some(Test { a: Some(x), b: y }) => (), //^ u32 ^ u8 _ => {} } }"#, ); } #[test] fn complete_for_hint() { check_types( r#" //- minicore: iterator pub struct Vec {} impl Vec { pub fn new() -> Self { Vec {} } pub fn push(&mut self, t: T) {} } impl IntoIterator for Vec { type Item = T; type IntoIter = IntoIter; } struct IntoIter {} impl Iterator for IntoIter { type Item = T; } fn main() { let mut data = Vec::new(); //^^^^ Vec<&str> data.push("foo"); for i in data { //^ &str let z = i; //^ &str } } "#, ); } #[test] fn multi_dyn_trait_bounds() { check_types( r#" pub struct Vec {} impl Vec { pub fn new() -> Self { Vec {} } } pub struct Box {} trait Display {} auto trait Sync {} fn main() { // The block expression wrapping disables the constructor hint hiding logic let _v = { Vec::>::new() }; //^^ Vec> let _v = { Vec::>::new() }; //^^ Vec> let _v = { Vec::>::new() }; //^^ Vec> } "#, ); } #[test] fn shorten_iterator_hints() { check_types( r#" //- minicore: iterators use core::iter; struct MyIter; impl Iterator for MyIter { type Item = (); fn next(&mut self) -> Option { None } } fn main() { let _x = MyIter; //^^ MyIter let _x = iter::repeat(0); //^^ impl Iterator fn generic(t: T) { let _x = iter::repeat(t); //^^ impl Iterator let _chained = iter::repeat(t).take(10); //^^^^^^^^ impl Iterator } } "#, ); } #[test] fn skip_constructor_and_enum_type_hints() { check_with_config( InlayHintsConfig { type_hints: true, hide_named_constructor_hints: true, ..DISABLED_CONFIG }, r#" //- minicore: try, option use core::ops::ControlFlow; mod x { pub mod y { pub struct Foo; } pub struct Foo; pub enum AnotherEnum { Variant() }; } struct Struct; struct TupleStruct(); impl Struct { fn new() -> Self { Struct } fn try_new() -> ControlFlow<(), Self> { ControlFlow::Continue(Struct) } } struct Generic(T); impl Generic { fn new() -> Self { Generic(0) } } enum Enum { Variant(u32) } fn times2(value: i32) -> i32 { 2 * value } fn main() { let enumb = Enum::Variant(0); let strukt = x::Foo; let strukt = x::y::Foo; let strukt = Struct; let strukt = Struct::new(); let tuple_struct = TupleStruct(); let generic0 = Generic::new(); // ^^^^^^^^ Generic let generic1 = Generic(0); // ^^^^^^^^ Generic let generic2 = Generic::::new(); let generic3 = >::new(); let generic4 = Generic::(0); let option = Some(0); // ^^^^^^ Option let func = times2; // ^^^^ fn times2(i32) -> i32 let closure = |x: i32| x * 2; // ^^^^^^^ |i32| -> i32 } fn fallible() -> ControlFlow<()> { let strukt = Struct::try_new()?; } "#, ); } #[test] fn shows_constructor_type_hints_when_enabled() { check_types( r#" //- minicore: try use core::ops::ControlFlow; struct Struct; struct TupleStruct(); impl Struct { fn new() -> Self { Struct } fn try_new() -> ControlFlow<(), Self> { ControlFlow::Continue(Struct) } } struct Generic(T); impl Generic { fn new() -> Self { Generic(0) } } fn main() { let strukt = Struct::new(); // ^^^^^^ Struct let tuple_struct = TupleStruct(); // ^^^^^^^^^^^^ TupleStruct let generic0 = Generic::new(); // ^^^^^^^^ Generic let generic1 = Generic::::new(); // ^^^^^^^^ Generic let generic2 = >::new(); // ^^^^^^^^ Generic } fn fallible() -> ControlFlow<()> { let strukt = Struct::try_new()?; // ^^^^^^ Struct } "#, ); } #[test] fn closures() { check( r#" fn main() { let mut start = 0; //^^^^^ i32 (0..2).for_each(|increment | { start += increment; }); //^^^^^^^^^ i32 let multiply = //^^^^^^^^ |i32, i32| -> i32 | a, b| a * b //^ i32 ^ i32 ; let _: i32 = multiply(1, 2); //^ a ^ b let multiply_ref = &multiply; //^^^^^^^^^^^^ &|i32, i32| -> i32 let return_42 = || 42; //^^^^^^^^^ || -> i32 || { 42 }; //^^ i32 }"#, ); } #[test] fn return_type_hints_for_closure_without_block() { check_with_config( InlayHintsConfig { closure_return_type_hints: ClosureReturnTypeHints::Always, ..DISABLED_CONFIG }, r#" fn main() { let a = || { 0 }; //^^ i32 let b = || 0; //^^ i32 }"#, ); } #[test] fn skip_closure_type_hints() { check_with_config( InlayHintsConfig { type_hints: true, hide_closure_initialization_hints: true, ..DISABLED_CONFIG }, r#" //- minicore: fn fn main() { let multiple_2 = |x: i32| { x * 2 }; let multiple_2 = |x: i32| x * 2; // ^^^^^^^^^^ |i32| -> i32 let (not) = (|x: bool| { !x }); // ^^^ |bool| -> bool let (is_zero, _b) = (|x: usize| { x == 0 }, false); // ^^^^^^^ |usize| -> bool // ^^ bool let plus_one = |x| { x + 1 }; // ^ u8 foo(plus_one); let add_mul = bar(|x: u8| { x + 1 }); // ^^^^^^^ impl FnOnce(u8) -> u8 + ?Sized let closure = if let Some(6) = add_mul(2).checked_sub(1) { // ^^^^^^^ fn(i32) -> i32 |x: i32| { x * 2 } } else { |x: i32| { x * 3 } }; } fn foo(f: impl FnOnce(u8) -> u8) {} fn bar(f: impl FnOnce(u8) -> u8) -> impl FnOnce(u8) -> u8 { move |x: u8| f(x) * 2 } "#, ); } #[test] fn hint_truncation() { check_with_config( InlayHintsConfig { max_length: Some(8), ..TEST_CONFIG }, r#" struct Smol(T); struct VeryLongOuterName(T); fn main() { let a = Smol(0u32); //^ Smol let b = VeryLongOuterName(0usize); //^ VeryLongOuterName<…> let c = Smol(Smol(0u32)) //^ Smol> }"#, ); } }