From 1b19a8aa5ecfc9d7115f291b97d413bd845c89b5 Mon Sep 17 00:00:00 2001
From: Jeremy Kolb <kjeremy@gmail.com>
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<NavigationTarget, Vec<TextRange>>,
+}
+
+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<CallItem> {
+        self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct CallItem {
+    pub target: NavigationTarget,
+    pub ranges: Vec<TextRange>,
+}
+
+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<RangeInfo<Vec<NavigationTarget>>> {
+    goto_definition::goto_definition(db, position)
+}
+
+pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> {
+    // 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<Vec<CallItem>> {
+    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<Cal
 }
 
 #[derive(Debug)]
-enum FnCallNode {
+pub(crate) enum FnCallNode {
     CallExpr(ast::CallExpr),
     MethodCallExpr(ast::MethodCallExpr),
     MacroCallExpr(ast::MacroCall),
@@ -108,7 +108,18 @@ impl FnCallNode {
         })
     }
 
-    fn name_ref(&self) -> Option<ast::NameRef> {
+    pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option<FnCallNode> {
+        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<ast::NameRef> {
         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<Option<RangeInfo<Vec<NavigationTarget>>>> {
+        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<Option<Vec<CallItem>>> {
+        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<Option<Vec<CallItem>>> {
+        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<Vec<NavigationTarget>> {
         self.with_db(|db| parent_module::parent_module(db, position))
-- 
cgit v1.2.3


From c7b2bc1363c8422122a0d1fbd5ff68b5984cf173 Mon Sep 17 00:00:00 2001
From: kjeremy <kjeremy@gmail.com>
Date: Wed, 8 Jan 2020 11:33:04 -0500
Subject: Move private API down

---
 crates/ra_ide/src/call_hierarchy.rs | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

(limited to 'crates/ra_ide')

diff --git a/crates/ra_ide/src/call_hierarchy.rs b/crates/ra_ide/src/call_hierarchy.rs
index 75658c20b..1cb712e32 100644
--- a/crates/ra_ide/src/call_hierarchy.rs
+++ b/crates/ra_ide/src/call_hierarchy.rs
@@ -16,21 +16,6 @@ use crate::{
     goto_definition, references, FilePosition, NavigationTarget, RangeInfo,
 };
 
-#[derive(Default)]
-struct CallLocations {
-    funcs: IndexMap<NavigationTarget, Vec<TextRange>>,
-}
-
-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<CallItem> {
-        self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect()
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct CallItem {
     pub target: NavigationTarget,
@@ -146,6 +131,21 @@ pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Optio
     Some(calls.into_items())
 }
 
+#[derive(Default)]
+struct CallLocations {
+    funcs: IndexMap<NavigationTarget, Vec<TextRange>>,
+}
+
+impl CallLocations {
+    fn add(&mut self, target: &NavigationTarget, range: TextRange) {
+        self.funcs.entry(target.clone()).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 ra_db::FilePosition;
-- 
cgit v1.2.3