From 1b19a8aa5ecfc9d7115f291b97d413bd845c89b5 Mon Sep 17 00:00:00 2001 From: Jeremy Kolb Date: Mon, 30 Dec 2019 09:12:06 -0500 Subject: Implement proposed CallHierarchy feature See: https://github.com/microsoft/vscode-languageserver-node/blob/master/protocol/src/protocol.callHierarchy.proposed.ts --- crates/ra_ide/Cargo.toml | 1 + crates/ra_ide/src/call_hierarchy.rs | 337 +++++++++++++++++++++++++ crates/ra_ide/src/call_info.rs | 15 +- crates/ra_ide/src/display/navigation_target.rs | 2 +- crates/ra_ide/src/lib.rs | 20 ++ 5 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 crates/ra_ide/src/call_hierarchy.rs (limited to 'crates/ra_ide') diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml index e3439ae31..2c9f9dce0 100644 --- a/crates/ra_ide/Cargo.toml +++ b/crates/ra_ide/Cargo.toml @@ -13,6 +13,7 @@ wasm = [] [dependencies] either = "1.5" format-buf = "1.0.0" +indexmap = "1.3.0" itertools = "0.8.0" join_to_string = "0.1.3" log = "0.4.5" diff --git a/crates/ra_ide/src/call_hierarchy.rs b/crates/ra_ide/src/call_hierarchy.rs new file mode 100644 index 000000000..75658c20b --- /dev/null +++ b/crates/ra_ide/src/call_hierarchy.rs @@ -0,0 +1,337 @@ +//! Entry point for call-hierarchy + +use indexmap::IndexMap; + +use hir::db::AstDatabase; +use ra_syntax::{ + ast::{self, DocCommentsOwner}, + match_ast, AstNode, TextRange, +}; + +use crate::{ + call_info::FnCallNode, + db::RootDatabase, + display::{ShortLabel, ToNav}, + expand::descend_into_macros, + goto_definition, references, FilePosition, NavigationTarget, RangeInfo, +}; + +#[derive(Default)] +struct CallLocations { + funcs: IndexMap>, +} + +impl CallLocations { + pub fn add(&mut self, target: &NavigationTarget, range: TextRange) { + self.funcs.entry(target.clone()).or_default().push(range); + } + + pub fn into_items(self) -> Vec { + self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() + } +} + +#[derive(Debug, Clone)] +pub struct CallItem { + pub target: NavigationTarget, + pub ranges: Vec, +} + +impl CallItem { + #[cfg(test)] + pub(crate) fn assert_match(&self, expected: &str) { + let actual = self.debug_render(); + test_utils::assert_eq_text!(expected.trim(), actual.trim(),); + } + + #[cfg(test)] + pub(crate) fn debug_render(&self) -> String { + format!("{} : {:?}", self.target.debug_render(), self.ranges) + } +} + +pub(crate) fn call_hierarchy( + db: &RootDatabase, + position: FilePosition, +) -> Option>> { + goto_definition::goto_definition(db, position) +} + +pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option> { + // 1. Find all refs + // 2. Loop through refs and determine unique fndef. This will become our `from: CallHierarchyItem,` in the reply. + // 3. Add ranges relative to the start of the fndef. + let refs = references::find_all_refs(db, position, None)?; + + let mut calls = CallLocations::default(); + + for reference in refs.info.references() { + let file_id = reference.file_range.file_id; + let file = db.parse_or_expand(file_id.into())?; + let token = file.token_at_offset(reference.file_range.range.start()).next()?; + let token = descend_into_macros(db, file_id, token); + let syntax = token.value.parent(); + + // This target is the containing function + if let Some(nav) = syntax.ancestors().find_map(|node| { + match_ast! { + match node { + ast::FnDef(it) => { + Some(NavigationTarget::from_named( + db, + token.with_value(&it), + it.doc_comment_text(), + it.short_label(), + )) + }, + _ => { None }, + } + } + }) { + let relative_range = reference.file_range.range; + calls.add(&nav, relative_range); + } + } + + Some(calls.into_items()) +} + +pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option> { + let file_id = position.file_id; + let file = db.parse_or_expand(file_id.into())?; + let token = file.token_at_offset(position.offset).next()?; + let token = descend_into_macros(db, file_id, token); + let syntax = token.value.parent(); + + let mut calls = CallLocations::default(); + + syntax + .descendants() + .filter_map(|node| FnCallNode::with_node_exact(&node)) + .filter_map(|call_node| { + let name_ref = call_node.name_ref()?; + let name_ref = token.with_value(name_ref.syntax()); + + let analyzer = hir::SourceAnalyzer::new(db, name_ref, None); + + if let Some(func_target) = match &call_node { + FnCallNode::CallExpr(expr) => { + //FIXME: Type::as_callable is broken + let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; + match callable_def { + hir::CallableDef::FunctionId(it) => { + let fn_def: hir::Function = it.into(); + let nav = fn_def.to_nav(db); + Some(nav) + } + _ => None, + } + } + FnCallNode::MethodCallExpr(expr) => { + let function = analyzer.resolve_method_call(&expr)?; + Some(function.to_nav(db)) + } + FnCallNode::MacroCallExpr(expr) => { + let macro_def = analyzer.resolve_macro_call(db, name_ref.with_value(&expr))?; + Some(macro_def.to_nav(db)) + } + } { + Some((func_target.clone(), name_ref.value.text_range())) + } else { + None + } + }) + .for_each(|(nav, range)| calls.add(&nav, range)); + + Some(calls.into_items()) +} + +#[cfg(test)] +mod tests { + use ra_db::FilePosition; + + use crate::mock_analysis::analysis_and_position; + + fn check_hierarchy( + fixture: &str, + expected: &str, + expected_incoming: &[&str], + expected_outgoing: &[&str], + ) { + let (analysis, pos) = analysis_and_position(fixture); + + let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; + assert_eq!(navs.len(), 1); + let nav = navs.pop().unwrap(); + nav.assert_match(expected); + + let item_pos = FilePosition { file_id: nav.file_id(), offset: nav.range().start() }; + let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap(); + assert_eq!(incoming_calls.len(), expected_incoming.len()); + + for call in 0..incoming_calls.len() { + incoming_calls[call].assert_match(expected_incoming[call]); + } + + let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap(); + assert_eq!(outgoing_calls.len(), expected_outgoing.len()); + + for call in 0..outgoing_calls.len() { + outgoing_calls[call].assert_match(expected_outgoing[call]); + } + } + + #[test] + fn test_call_hierarchy_on_ref() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller() { + call<|>ee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_on_def() { + check_hierarchy( + r#" + //- /lib.rs + fn call<|>ee() {} + fn caller() { + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_same_fn() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller() { + call<|>ee(); + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 58) [18; 24) : [[33; 39), [47; 53)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_different_fn() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller1() { + call<|>ee(); + } + + fn caller2() { + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &[ + "caller1 FN_DEF FileId(1) [15; 45) [18; 25) : [[34; 40)]", + "caller2 FN_DEF FileId(1) [46; 76) [49; 56) : [[65; 71)]", + ], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_different_files() { + check_hierarchy( + r#" + //- /lib.rs + mod foo; + use foo::callee; + + fn caller() { + call<|>ee(); + } + + //- /foo/mod.rs + pub fn callee() {} + "#, + "callee FN_DEF FileId(2) [0; 18) [7; 13)", + &["caller FN_DEF FileId(1) [26; 55) [29; 35) : [[44; 50)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_outgoing() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn call<|>er() { + callee(); + callee(); + } + "#, + "caller FN_DEF FileId(1) [15; 58) [18; 24)", + &[], + &["callee FN_DEF FileId(1) [0; 14) [3; 9) : [[33; 39), [47; 53)]"], + ); + } + + #[test] + fn test_call_hierarchy_outgoing_in_different_files() { + check_hierarchy( + r#" + //- /lib.rs + mod foo; + use foo::callee; + + fn call<|>er() { + callee(); + } + + //- /foo/mod.rs + pub fn callee() {} + "#, + "caller FN_DEF FileId(1) [26; 55) [29; 35)", + &[], + &["callee FN_DEF FileId(2) [0; 18) [7; 13) : [[44; 50)]"], + ); + } + + #[test] + fn test_call_hierarchy_incoming_outgoing() { + check_hierarchy( + r#" + //- /lib.rs + fn caller1() { + call<|>er2(); + } + + fn caller2() { + caller3(); + } + + fn caller3() { + + } + "#, + "caller2 FN_DEF FileId(1) [32; 63) [35; 42)", + &["caller1 FN_DEF FileId(1) [0; 31) [3; 10) : [[19; 26)]"], + &["caller3 FN_DEF FileId(1) [64; 80) [67; 74) : [[51; 58)]"], + ); + } +} diff --git a/crates/ra_ide/src/call_info.rs b/crates/ra_ide/src/call_info.rs index 2c2b6fa48..a7023529b 100644 --- a/crates/ra_ide/src/call_info.rs +++ b/crates/ra_ide/src/call_info.rs @@ -88,7 +88,7 @@ pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option Option { + pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option { + match_ast! { + match node { + ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) }, + ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) }, + ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) }, + _ => { None }, + } + } + } + + pub(crate) fn name_ref(&self) -> Option { match self { FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, diff --git a/crates/ra_ide/src/display/navigation_target.rs b/crates/ra_ide/src/display/navigation_target.rs index b9ae67828..f2e45fa31 100644 --- a/crates/ra_ide/src/display/navigation_target.rs +++ b/crates/ra_ide/src/display/navigation_target.rs @@ -19,7 +19,7 @@ use super::short_label::ShortLabel; /// /// Typically, a `NavigationTarget` corresponds to some element in the source /// code, like a function or a struct, but this is not strictly required. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NavigationTarget { file_id: FileId, name: SmolStr, diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 779a81b2c..06497617b 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -24,6 +24,7 @@ mod goto_definition; mod goto_type_definition; mod extend_selection; mod hover; +mod call_hierarchy; mod call_info; mod syntax_highlighting; mod parent_module; @@ -62,6 +63,7 @@ use crate::{db::LineIndexDatabase, display::ToNav, symbol_index::FileSymbol}; pub use crate::{ assists::{Assist, AssistId}, + call_hierarchy::CallItem, change::{AnalysisChange, LibraryData}, completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, diagnostics::Severity, @@ -412,6 +414,24 @@ impl Analysis { self.with_db(|db| call_info::call_info(db, position)) } + /// Computes call hierarchy candidates for the given file position. + pub fn call_hierarchy( + &self, + position: FilePosition, + ) -> Cancelable>>> { + self.with_db(|db| call_hierarchy::call_hierarchy(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn incoming_calls(&self, position: FilePosition) -> Cancelable>> { + self.with_db(|db| call_hierarchy::incoming_calls(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn outgoing_calls(&self, position: FilePosition) -> Cancelable>> { + self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) + } + /// Returns a `mod name;` declaration which created the current module. pub fn parent_module(&self, position: FilePosition) -> Cancelable> { self.with_db(|db| parent_module::parent_module(db, position)) -- cgit v1.2.3