| //! Entry point for call-hierarchy |
| |
| use std::iter; |
| |
| use hir::Semantics; |
| use ide_db::{ |
| defs::{Definition, NameClass, NameRefClass}, |
| helpers::pick_best_token, |
| search::FileReference, |
| FileRange, FxIndexMap, RootDatabase, |
| }; |
| use syntax::{ast, AstNode, SyntaxKind::IDENT}; |
| |
| use crate::{goto_definition, FilePosition, NavigationTarget, RangeInfo, TryToNav}; |
| |
| #[derive(Debug, Clone)] |
| pub struct CallItem { |
| pub target: NavigationTarget, |
| pub ranges: Vec<FileRange>, |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub struct CallHierarchyConfig { |
| /// Whether to exclude tests from the call hierarchy |
| pub exclude_tests: bool, |
| } |
| |
| pub(crate) fn call_hierarchy( |
| db: &RootDatabase, |
| position: FilePosition, |
| ) -> Option<RangeInfo<Vec<NavigationTarget>>> { |
| goto_definition::goto_definition(db, position) |
| } |
| |
| pub(crate) fn incoming_calls( |
| db: &RootDatabase, |
| CallHierarchyConfig { exclude_tests }: CallHierarchyConfig, |
| FilePosition { file_id, offset }: FilePosition, |
| ) -> Option<Vec<CallItem>> { |
| let sema = &Semantics::new(db); |
| |
| let file = sema.parse_guess_edition(file_id); |
| let file = file.syntax(); |
| let mut calls = CallLocations::default(); |
| |
| let references = sema |
| .find_nodes_at_offset_with_descend(file, offset) |
| .filter_map(move |node| match node { |
| ast::NameLike::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? { |
| NameRefClass::Definition(def @ Definition::Function(_)) => Some(def), |
| _ => None, |
| }, |
| ast::NameLike::Name(name) => match NameClass::classify(sema, &name)? { |
| NameClass::Definition(def @ Definition::Function(_)) => Some(def), |
| _ => None, |
| }, |
| ast::NameLike::Lifetime(_) => None, |
| }) |
| .flat_map(|func| func.usages(sema).all()); |
| |
| for (_, references) in references { |
| let references = |
| references.iter().filter_map(|FileReference { name, .. }| name.as_name_ref()); |
| for name in references { |
| // This target is the containing function |
| let def_nav = sema.ancestors_with_macros(name.syntax().clone()).find_map(|node| { |
| let def = ast::Fn::cast(node).and_then(|fn_| sema.to_def(&fn_))?; |
| // We should return def before check if it is a test, so that we |
| // will not continue to search for outer fn in nested fns |
| def.try_to_nav(sema.db).map(|nav| (def, nav)) |
| }); |
| |
| if let Some((def, nav)) = def_nav { |
| if exclude_tests && def.is_test(db) { |
| continue; |
| } |
| |
| let range = sema.original_range(name.syntax()); |
| calls.add(nav.call_site, range.into()); |
| if let Some(other) = nav.def_site { |
| calls.add(other, range.into()); |
| } |
| } |
| } |
| } |
| |
| Some(calls.into_items()) |
| } |
| |
| pub(crate) fn outgoing_calls( |
| db: &RootDatabase, |
| CallHierarchyConfig { exclude_tests }: CallHierarchyConfig, |
| FilePosition { file_id, offset }: FilePosition, |
| ) -> Option<Vec<CallItem>> { |
| let sema = Semantics::new(db); |
| let file = sema.parse_guess_edition(file_id); |
| let file = file.syntax(); |
| let token = pick_best_token(file.token_at_offset(offset), |kind| match kind { |
| IDENT => 1, |
| _ => 0, |
| })?; |
| let mut calls = CallLocations::default(); |
| |
| sema.descend_into_macros_exact(token) |
| .into_iter() |
| .filter_map(|it| it.parent_ancestors().nth(1).and_then(ast::Item::cast)) |
| .filter_map(|item| match item { |
| ast::Item::Const(c) => c.body().map(|it| it.syntax().descendants()), |
| ast::Item::Fn(f) => f.body().map(|it| it.syntax().descendants()), |
| ast::Item::Static(s) => s.body().map(|it| it.syntax().descendants()), |
| _ => None, |
| }) |
| .flatten() |
| .filter_map(ast::CallableExpr::cast) |
| .filter_map(|call_node| { |
| let (nav_target, range) = match call_node { |
| ast::CallableExpr::Call(call) => { |
| let expr = call.expr()?; |
| let callable = sema.type_of_expr(&expr)?.original.as_callable(db)?; |
| match callable.kind() { |
| hir::CallableKind::Function(it) => { |
| if exclude_tests && it.is_test(db) { |
| return None; |
| } |
| it.try_to_nav(db) |
| } |
| hir::CallableKind::TupleEnumVariant(it) => it.try_to_nav(db), |
| hir::CallableKind::TupleStruct(it) => it.try_to_nav(db), |
| _ => None, |
| } |
| .zip(Some(sema.original_range(expr.syntax()))) |
| } |
| ast::CallableExpr::MethodCall(expr) => { |
| let function = sema.resolve_method_call(&expr)?; |
| if exclude_tests && function.is_test(db) { |
| return None; |
| } |
| function |
| .try_to_nav(db) |
| .zip(Some(sema.original_range(expr.name_ref()?.syntax()))) |
| } |
| }?; |
| Some(nav_target.into_iter().zip(iter::repeat(range))) |
| }) |
| .flatten() |
| .for_each(|(nav, range)| calls.add(nav, range.into())); |
| |
| Some(calls.into_items()) |
| } |
| |
| #[derive(Default)] |
| struct CallLocations { |
| funcs: FxIndexMap<NavigationTarget, Vec<FileRange>>, |
| } |
| |
| impl CallLocations { |
| fn add(&mut self, target: NavigationTarget, range: FileRange) { |
| self.funcs.entry(target).or_default().push(range); |
| } |
| |
| fn into_items(self) -> Vec<CallItem> { |
| self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use expect_test::{expect, Expect}; |
| use ide_db::FilePosition; |
| use itertools::Itertools; |
| |
| use crate::fixture; |
| |
| fn check_hierarchy( |
| exclude_tests: bool, |
| ra_fixture: &str, |
| expected_nav: Expect, |
| expected_incoming: Expect, |
| expected_outgoing: Expect, |
| ) { |
| fn debug_render(item: crate::CallItem) -> String { |
| format!( |
| "{} : {}", |
| item.target.debug_render(), |
| item.ranges.iter().format_with(", ", |range, f| f(&format_args!( |
| "{:?}:{:?}", |
| range.file_id, range.range |
| ))) |
| ) |
| } |
| |
| let (analysis, pos) = fixture::position(ra_fixture); |
| |
| let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; |
| assert_eq!(navs.len(), 1); |
| let nav = navs.pop().unwrap(); |
| expected_nav.assert_eq(&nav.debug_render()); |
| |
| let config = crate::CallHierarchyConfig { exclude_tests }; |
| |
| let item_pos = |
| FilePosition { file_id: nav.file_id, offset: nav.focus_or_full_range().start() }; |
| let incoming_calls = analysis.incoming_calls(config, item_pos).unwrap().unwrap(); |
| expected_incoming.assert_eq(&incoming_calls.into_iter().map(debug_render).join("\n")); |
| |
| let outgoing_calls = analysis.outgoing_calls(config, item_pos).unwrap().unwrap(); |
| expected_outgoing.assert_eq(&outgoing_calls.into_iter().map(debug_render).join("\n")); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_on_ref() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn callee() {} |
| fn caller() { |
| call$0ee(); |
| } |
| "#, |
| expect![["callee Function FileId(0) 0..14 3..9"]], |
| expect!["caller Function FileId(0) 15..44 18..24 : FileId(0):33..39"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_on_def() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn call$0ee() {} |
| fn caller() { |
| callee(); |
| } |
| "#, |
| expect![["callee Function FileId(0) 0..14 3..9"]], |
| expect!["caller Function FileId(0) 15..44 18..24 : FileId(0):33..39"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_same_fn() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn callee() {} |
| fn caller() { |
| call$0ee(); |
| callee(); |
| } |
| "#, |
| expect![["callee Function FileId(0) 0..14 3..9"]], |
| expect!["caller Function FileId(0) 15..58 18..24 : FileId(0):33..39, FileId(0):47..53"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_different_fn() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn callee() {} |
| fn caller1() { |
| call$0ee(); |
| } |
| |
| fn caller2() { |
| callee(); |
| } |
| "#, |
| expect![["callee Function FileId(0) 0..14 3..9"]], |
| expect![[r#" |
| caller1 Function FileId(0) 15..45 18..25 : FileId(0):34..40 |
| caller2 Function FileId(0) 47..77 50..57 : FileId(0):66..72"#]], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_tests_mod() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs cfg:test |
| fn callee() {} |
| fn caller1() { |
| call$0ee(); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_caller() { |
| callee(); |
| } |
| } |
| "#, |
| expect![["callee Function FileId(0) 0..14 3..9"]], |
| expect![[r#" |
| caller1 Function FileId(0) 15..45 18..25 : FileId(0):34..40 |
| test_caller Function FileId(0) 95..149 110..121 tests : FileId(0):134..140"#]], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_different_files() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| mod foo; |
| use foo::callee; |
| |
| fn caller() { |
| call$0ee(); |
| } |
| |
| //- /foo/mod.rs |
| pub fn callee() {} |
| "#, |
| expect!["callee Function FileId(1) 0..18 7..13 foo"], |
| expect!["caller Function FileId(0) 27..56 30..36 : FileId(0):45..51"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_outgoing() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn callee() {} |
| fn call$0er() { |
| callee(); |
| callee(); |
| } |
| "#, |
| expect![["caller Function FileId(0) 15..58 18..24"]], |
| expect![[]], |
| expect!["callee Function FileId(0) 0..14 3..9 : FileId(0):33..39, FileId(0):47..53"], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_outgoing_in_different_files() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| mod foo; |
| use foo::callee; |
| |
| fn call$0er() { |
| callee(); |
| } |
| |
| //- /foo/mod.rs |
| pub fn callee() {} |
| "#, |
| expect![["caller Function FileId(0) 27..56 30..36"]], |
| expect![[]], |
| expect!["callee Function FileId(1) 0..18 7..13 foo : FileId(0):45..51"], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_incoming_outgoing() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| fn caller1() { |
| call$0er2(); |
| } |
| |
| fn caller2() { |
| caller3(); |
| } |
| |
| fn caller3() { |
| |
| } |
| "#, |
| expect![["caller2 Function FileId(0) 33..64 36..43"]], |
| expect!["caller1 Function FileId(0) 0..31 3..10 : FileId(0):19..26"], |
| expect!["caller3 Function FileId(0) 66..83 69..76 : FileId(0):52..59"], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_issue_5103() { |
| check_hierarchy( |
| false, |
| r#" |
| fn a() { |
| b() |
| } |
| |
| fn b() {} |
| |
| fn main() { |
| a$0() |
| } |
| "#, |
| expect![["a Function FileId(0) 0..18 3..4"]], |
| expect!["main Function FileId(0) 31..52 34..38 : FileId(0):47..48"], |
| expect!["b Function FileId(0) 20..29 23..24 : FileId(0):13..14"], |
| ); |
| |
| check_hierarchy( |
| false, |
| r#" |
| fn a() { |
| b$0() |
| } |
| |
| fn b() {} |
| |
| fn main() { |
| a() |
| } |
| "#, |
| expect![["b Function FileId(0) 20..29 23..24"]], |
| expect!["a Function FileId(0) 0..18 3..4 : FileId(0):13..14"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_macros_incoming() { |
| check_hierarchy( |
| false, |
| r#" |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| } |
| } |
| define!(callee) |
| fn caller() { |
| call!(call$0ee); |
| } |
| "#, |
| expect![[r#"callee Function FileId(0) 144..159 152..158"#]], |
| expect!["caller Function FileId(0) 160..194 163..169 : FileId(0):184..190"], |
| expect![[]], |
| ); |
| check_hierarchy( |
| false, |
| r#" |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| } |
| } |
| define!(cal$0lee) |
| fn caller() { |
| call!(callee); |
| } |
| "#, |
| expect![[r#"callee Function FileId(0) 144..159 152..158"#]], |
| expect!["caller Function FileId(0) 160..194 163..169 : FileId(0):184..190"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_macros_outgoing() { |
| check_hierarchy( |
| false, |
| r#" |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| } |
| } |
| define!(callee) |
| fn caller$0() { |
| call!(callee); |
| } |
| "#, |
| expect![[r#"caller Function FileId(0) 160..194 163..169"#]], |
| expect![[]], |
| // FIXME |
| expect![[]], |
| ); |
| } |
| #[test] |
| fn test_call_hierarchy_in_macros_incoming_different_files() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| #[macro_use] |
| mod foo; |
| define!(callee) |
| fn caller() { |
| call!(call$0ee); |
| } |
| //- /foo.rs |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| } |
| } |
| "#, |
| expect!["callee Function FileId(0) 22..37 30..36"], |
| expect!["caller Function FileId(0) 38..72 41..47 : FileId(0):62..68"], |
| expect![[]], |
| ); |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| #[macro_use] |
| mod foo; |
| define!(cal$0lee) |
| fn caller() { |
| call!(callee); |
| } |
| //- /foo.rs |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| } |
| } |
| "#, |
| expect!["callee Function FileId(0) 22..37 30..36"], |
| expect!["caller Function FileId(0) 38..72 41..47 : FileId(0):62..68"], |
| expect![[]], |
| ); |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| #[macro_use] |
| mod foo; |
| define!(cal$0lee) |
| call!(callee); |
| //- /foo.rs |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| fn caller() { |
| $ident() |
| } |
| fn $ident() { |
| $ident() |
| } |
| } |
| } |
| "#, |
| expect!["callee Function FileId(0) 22..37 30..36"], |
| expect![[r#" |
| callee Function FileId(0) 38..52 44..50 : FileId(0):44..50 |
| caller Function FileId(0) 38..52 : FileId(0):44..50 |
| caller Function FileId(1) 130..136 130..136 : FileId(0):44..50"#]], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_in_macros_outgoing_different_files() { |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| #[macro_use] |
| mod foo; |
| define!(callee) |
| fn caller$0() { |
| call!(callee); |
| } |
| //- /foo.rs |
| macro_rules! define { |
| ($ident:ident) => { |
| fn $ident {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| callee() |
| } |
| } |
| "#, |
| expect!["caller Function FileId(0) 38..72 41..47"], |
| expect![[]], |
| // FIXME |
| expect![[]], |
| ); |
| check_hierarchy( |
| false, |
| r#" |
| //- /lib.rs |
| #[macro_use] |
| mod foo; |
| define!(callee) |
| fn caller$0() { |
| call!(callee); |
| } |
| //- /foo.rs |
| macro_rules! define { |
| () => { |
| fn callee {} |
| } |
| } |
| macro_rules! call { |
| ($ident:ident) => { |
| $ident() |
| callee() |
| } |
| } |
| "#, |
| expect!["caller Function FileId(0) 38..72 41..47"], |
| expect![[]], |
| // FIXME |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_trait_method_call_hierarchy() { |
| check_hierarchy( |
| false, |
| r#" |
| trait T1 { |
| fn call$0ee(); |
| } |
| |
| struct S1; |
| |
| impl T1 for S1 { |
| fn callee() {} |
| } |
| |
| fn caller() { |
| S1::callee(); |
| } |
| "#, |
| expect!["callee Function FileId(0) 15..27 18..24 T1"], |
| expect!["caller Function FileId(0) 82..115 85..91 : FileId(0):104..110"], |
| expect![[]], |
| ); |
| } |
| |
| #[test] |
| fn test_call_hierarchy_excluding_tests() { |
| check_hierarchy( |
| false, |
| r#" |
| fn main() { |
| f1(); |
| } |
| |
| fn f1$0() { |
| f2(); f3(); |
| } |
| |
| fn f2() { |
| f1(); f3(); |
| } |
| |
| #[test] |
| fn f3() { |
| f1(); f2(); |
| } |
| "#, |
| expect!["f1 Function FileId(0) 25..52 28..30"], |
| expect![[r#" |
| main Function FileId(0) 0..23 3..7 : FileId(0):16..18 |
| f2 Function FileId(0) 54..81 57..59 : FileId(0):68..70 |
| f3 Function FileId(0) 83..118 94..96 : FileId(0):105..107"#]], |
| expect![[r#" |
| f2 Function FileId(0) 54..81 57..59 : FileId(0):39..41 |
| f3 Function FileId(0) 83..118 94..96 : FileId(0):45..47"#]], |
| ); |
| |
| check_hierarchy( |
| true, |
| r#" |
| fn main() { |
| f1(); |
| } |
| |
| fn f1$0() { |
| f2(); f3(); |
| } |
| |
| fn f2() { |
| f1(); f3(); |
| } |
| |
| #[test] |
| fn f3() { |
| f1(); f2(); |
| } |
| "#, |
| expect!["f1 Function FileId(0) 25..52 28..30"], |
| expect![[r#" |
| main Function FileId(0) 0..23 3..7 : FileId(0):16..18 |
| f2 Function FileId(0) 54..81 57..59 : FileId(0):68..70"#]], |
| expect!["f2 Function FileId(0) 54..81 57..59 : FileId(0):39..41"], |
| ); |
| } |
| } |