Skip to content

Commit 2e66bed

Browse files
bors[bot]kjeremy
andauthored
Merge #2698
2698: Call Hierarchy r=kjeremy a=kjeremy Support experiment incoming and outgoing calls. Fixes #2546 Co-authored-by: Jeremy Kolb <kjeremy@gmail.com> Co-authored-by: kjeremy <kjeremy@gmail.com>
2 parents 928ecd0 + c7b2bc1 commit 2e66bed

File tree

11 files changed

+502
-15
lines changed

11 files changed

+502
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ra_ide/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ wasm = []
1313
[dependencies]
1414
either = "1.5"
1515
format-buf = "1.0.0"
16+
indexmap = "1.3.0"
1617
itertools = "0.8.0"
1718
join_to_string = "0.1.3"
1819
log = "0.4.5"

crates/ra_ide/src/call_hierarchy.rs

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
//! Entry point for call-hierarchy
2+
3+
use indexmap::IndexMap;
4+
5+
use hir::db::AstDatabase;
6+
use ra_syntax::{
7+
ast::{self, DocCommentsOwner},
8+
match_ast, AstNode, TextRange,
9+
};
10+
11+
use crate::{
12+
call_info::FnCallNode,
13+
db::RootDatabase,
14+
display::{ShortLabel, ToNav},
15+
expand::descend_into_macros,
16+
goto_definition, references, FilePosition, NavigationTarget, RangeInfo,
17+
};
18+
19+
#[derive(Debug, Clone)]
20+
pub struct CallItem {
21+
pub target: NavigationTarget,
22+
pub ranges: Vec<TextRange>,
23+
}
24+
25+
impl CallItem {
26+
#[cfg(test)]
27+
pub(crate) fn assert_match(&self, expected: &str) {
28+
let actual = self.debug_render();
29+
test_utils::assert_eq_text!(expected.trim(), actual.trim(),);
30+
}
31+
32+
#[cfg(test)]
33+
pub(crate) fn debug_render(&self) -> String {
34+
format!("{} : {:?}", self.target.debug_render(), self.ranges)
35+
}
36+
}
37+
38+
pub(crate) fn call_hierarchy(
39+
db: &RootDatabase,
40+
position: FilePosition,
41+
) -> Option<RangeInfo<Vec<NavigationTarget>>> {
42+
goto_definition::goto_definition(db, position)
43+
}
44+
45+
pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> {
46+
// 1. Find all refs
47+
// 2. Loop through refs and determine unique fndef. This will become our `from: CallHierarchyItem,` in the reply.
48+
// 3. Add ranges relative to the start of the fndef.
49+
let refs = references::find_all_refs(db, position, None)?;
50+
51+
let mut calls = CallLocations::default();
52+
53+
for reference in refs.info.references() {
54+
let file_id = reference.file_range.file_id;
55+
let file = db.parse_or_expand(file_id.into())?;
56+
let token = file.token_at_offset(reference.file_range.range.start()).next()?;
57+
let token = descend_into_macros(db, file_id, token);
58+
let syntax = token.value.parent();
59+
60+
// This target is the containing function
61+
if let Some(nav) = syntax.ancestors().find_map(|node| {
62+
match_ast! {
63+
match node {
64+
ast::FnDef(it) => {
65+
Some(NavigationTarget::from_named(
66+
db,
67+
token.with_value(&it),
68+
it.doc_comment_text(),
69+
it.short_label(),
70+
))
71+
},
72+
_ => { None },
73+
}
74+
}
75+
}) {
76+
let relative_range = reference.file_range.range;
77+
calls.add(&nav, relative_range);
78+
}
79+
}
80+
81+
Some(calls.into_items())
82+
}
83+
84+
pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> {
85+
let file_id = position.file_id;
86+
let file = db.parse_or_expand(file_id.into())?;
87+
let token = file.token_at_offset(position.offset).next()?;
88+
let token = descend_into_macros(db, file_id, token);
89+
let syntax = token.value.parent();
90+
91+
let mut calls = CallLocations::default();
92+
93+
syntax
94+
.descendants()
95+
.filter_map(|node| FnCallNode::with_node_exact(&node))
96+
.filter_map(|call_node| {
97+
let name_ref = call_node.name_ref()?;
98+
let name_ref = token.with_value(name_ref.syntax());
99+
100+
let analyzer = hir::SourceAnalyzer::new(db, name_ref, None);
101+
102+
if let Some(func_target) = match &call_node {
103+
FnCallNode::CallExpr(expr) => {
104+
//FIXME: Type::as_callable is broken
105+
let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?;
106+
match callable_def {
107+
hir::CallableDef::FunctionId(it) => {
108+
let fn_def: hir::Function = it.into();
109+
let nav = fn_def.to_nav(db);
110+
Some(nav)
111+
}
112+
_ => None,
113+
}
114+
}
115+
FnCallNode::MethodCallExpr(expr) => {
116+
let function = analyzer.resolve_method_call(&expr)?;
117+
Some(function.to_nav(db))
118+
}
119+
FnCallNode::MacroCallExpr(expr) => {
120+
let macro_def = analyzer.resolve_macro_call(db, name_ref.with_value(&expr))?;
121+
Some(macro_def.to_nav(db))
122+
}
123+
} {
124+
Some((func_target.clone(), name_ref.value.text_range()))
125+
} else {
126+
None
127+
}
128+
})
129+
.for_each(|(nav, range)| calls.add(&nav, range));
130+
131+
Some(calls.into_items())
132+
}
133+
134+
#[derive(Default)]
135+
struct CallLocations {
136+
funcs: IndexMap<NavigationTarget, Vec<TextRange>>,
137+
}
138+
139+
impl CallLocations {
140+
fn add(&mut self, target: &NavigationTarget, range: TextRange) {
141+
self.funcs.entry(target.clone()).or_default().push(range);
142+
}
143+
144+
fn into_items(self) -> Vec<CallItem> {
145+
self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect()
146+
}
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use ra_db::FilePosition;
152+
153+
use crate::mock_analysis::analysis_and_position;
154+
155+
fn check_hierarchy(
156+
fixture: &str,
157+
expected: &str,
158+
expected_incoming: &[&str],
159+
expected_outgoing: &[&str],
160+
) {
161+
let (analysis, pos) = analysis_and_position(fixture);
162+
163+
let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info;
164+
assert_eq!(navs.len(), 1);
165+
let nav = navs.pop().unwrap();
166+
nav.assert_match(expected);
167+
168+
let item_pos = FilePosition { file_id: nav.file_id(), offset: nav.range().start() };
169+
let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap();
170+
assert_eq!(incoming_calls.len(), expected_incoming.len());
171+
172+
for call in 0..incoming_calls.len() {
173+
incoming_calls[call].assert_match(expected_incoming[call]);
174+
}
175+
176+
let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap();
177+
assert_eq!(outgoing_calls.len(), expected_outgoing.len());
178+
179+
for call in 0..outgoing_calls.len() {
180+
outgoing_calls[call].assert_match(expected_outgoing[call]);
181+
}
182+
}
183+
184+
#[test]
185+
fn test_call_hierarchy_on_ref() {
186+
check_hierarchy(
187+
r#"
188+
//- /lib.rs
189+
fn callee() {}
190+
fn caller() {
191+
call<|>ee();
192+
}
193+
"#,
194+
"callee FN_DEF FileId(1) [0; 14) [3; 9)",
195+
&["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"],
196+
&[],
197+
);
198+
}
199+
200+
#[test]
201+
fn test_call_hierarchy_on_def() {
202+
check_hierarchy(
203+
r#"
204+
//- /lib.rs
205+
fn call<|>ee() {}
206+
fn caller() {
207+
callee();
208+
}
209+
"#,
210+
"callee FN_DEF FileId(1) [0; 14) [3; 9)",
211+
&["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"],
212+
&[],
213+
);
214+
}
215+
216+
#[test]
217+
fn test_call_hierarchy_in_same_fn() {
218+
check_hierarchy(
219+
r#"
220+
//- /lib.rs
221+
fn callee() {}
222+
fn caller() {
223+
call<|>ee();
224+
callee();
225+
}
226+
"#,
227+
"callee FN_DEF FileId(1) [0; 14) [3; 9)",
228+
&["caller FN_DEF FileId(1) [15; 58) [18; 24) : [[33; 39), [47; 53)]"],
229+
&[],
230+
);
231+
}
232+
233+
#[test]
234+
fn test_call_hierarchy_in_different_fn() {
235+
check_hierarchy(
236+
r#"
237+
//- /lib.rs
238+
fn callee() {}
239+
fn caller1() {
240+
call<|>ee();
241+
}
242+
243+
fn caller2() {
244+
callee();
245+
}
246+
"#,
247+
"callee FN_DEF FileId(1) [0; 14) [3; 9)",
248+
&[
249+
"caller1 FN_DEF FileId(1) [15; 45) [18; 25) : [[34; 40)]",
250+
"caller2 FN_DEF FileId(1) [46; 76) [49; 56) : [[65; 71)]",
251+
],
252+
&[],
253+
);
254+
}
255+
256+
#[test]
257+
fn test_call_hierarchy_in_different_files() {
258+
check_hierarchy(
259+
r#"
260+
//- /lib.rs
261+
mod foo;
262+
use foo::callee;
263+
264+
fn caller() {
265+
call<|>ee();
266+
}
267+
268+
//- /foo/mod.rs
269+
pub fn callee() {}
270+
"#,
271+
"callee FN_DEF FileId(2) [0; 18) [7; 13)",
272+
&["caller FN_DEF FileId(1) [26; 55) [29; 35) : [[44; 50)]"],
273+
&[],
274+
);
275+
}
276+
277+
#[test]
278+
fn test_call_hierarchy_outgoing() {
279+
check_hierarchy(
280+
r#"
281+
//- /lib.rs
282+
fn callee() {}
283+
fn call<|>er() {
284+
callee();
285+
callee();
286+
}
287+
"#,
288+
"caller FN_DEF FileId(1) [15; 58) [18; 24)",
289+
&[],
290+
&["callee FN_DEF FileId(1) [0; 14) [3; 9) : [[33; 39), [47; 53)]"],
291+
);
292+
}
293+
294+
#[test]
295+
fn test_call_hierarchy_outgoing_in_different_files() {
296+
check_hierarchy(
297+
r#"
298+
//- /lib.rs
299+
mod foo;
300+
use foo::callee;
301+
302+
fn call<|>er() {
303+
callee();
304+
}
305+
306+
//- /foo/mod.rs
307+
pub fn callee() {}
308+
"#,
309+
"caller FN_DEF FileId(1) [26; 55) [29; 35)",
310+
&[],
311+
&["callee FN_DEF FileId(2) [0; 18) [7; 13) : [[44; 50)]"],
312+
);
313+
}
314+
315+
#[test]
316+
fn test_call_hierarchy_incoming_outgoing() {
317+
check_hierarchy(
318+
r#"
319+
//- /lib.rs
320+
fn caller1() {
321+
call<|>er2();
322+
}
323+
324+
fn caller2() {
325+
caller3();
326+
}
327+
328+
fn caller3() {
329+
330+
}
331+
"#,
332+
"caller2 FN_DEF FileId(1) [32; 63) [35; 42)",
333+
&["caller1 FN_DEF FileId(1) [0; 31) [3; 10) : [[19; 26)]"],
334+
&["caller3 FN_DEF FileId(1) [64; 80) [67; 74) : [[51; 58)]"],
335+
);
336+
}
337+
}

crates/ra_ide/src/call_info.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option<Cal
8888
}
8989

9090
#[derive(Debug)]
91-
enum FnCallNode {
91+
pub(crate) enum FnCallNode {
9292
CallExpr(ast::CallExpr),
9393
MethodCallExpr(ast::MethodCallExpr),
9494
MacroCallExpr(ast::MacroCall),
@@ -108,7 +108,18 @@ impl FnCallNode {
108108
})
109109
}
110110

111-
fn name_ref(&self) -> Option<ast::NameRef> {
111+
pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option<FnCallNode> {
112+
match_ast! {
113+
match node {
114+
ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) },
115+
ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) },
116+
ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) },
117+
_ => { None },
118+
}
119+
}
120+
}
121+
122+
pub(crate) fn name_ref(&self) -> Option<ast::NameRef> {
112123
match self {
113124
FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? {
114125
ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?,

crates/ra_ide/src/display/navigation_target.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use super::short_label::ShortLabel;
1919
///
2020
/// Typically, a `NavigationTarget` corresponds to some element in the source
2121
/// code, like a function or a struct, but this is not strictly required.
22-
#[derive(Debug, Clone)]
22+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2323
pub struct NavigationTarget {
2424
file_id: FileId,
2525
name: SmolStr,

0 commit comments

Comments
 (0)