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>
This commit is contained in:
bors[bot] 2020-01-08 16:42:56 +00:00 committed by GitHub
commit 2e66bed782
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 502 additions and 15 deletions

1
Cargo.lock generated
View file

@ -1032,6 +1032,7 @@ dependencies = [
"either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"format-buf 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fst 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"insta 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -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"

View file

@ -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(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())
}
#[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;
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)]"],
);
}
}

View file

@ -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()?,

View file

@ -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,

View file

@ -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))

View file

@ -1,8 +1,8 @@
//! Advertizes the capabilities of the LSP Server.
use lsp_types::{
CodeActionProviderCapability, CodeLensOptions, CompletionOptions,
DocumentOnTypeFormattingOptions, FoldingRangeProviderCapability,
CallHierarchyServerCapability, CodeActionProviderCapability, CodeLensOptions,
CompletionOptions, DocumentOnTypeFormattingOptions, FoldingRangeProviderCapability,
ImplementationProviderCapability, RenameOptions, RenameProviderCapability, SaveOptions,
SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
@ -56,7 +56,7 @@ pub fn server_capabilities() -> ServerCapabilities {
color_provider: None,
execute_command_provider: None,
workspace: None,
call_hierarchy_provider: None,
call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
experimental: Default::default(),
}
}

View file

@ -490,6 +490,24 @@ impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<Vec<NavigationTarget>>)
}
}
pub fn to_call_hierarchy_item(
file_id: FileId,
range: TextRange,
world: &WorldSnapshot,
line_index: &LineIndex,
nav: NavigationTarget,
) -> Result<lsp_types::CallHierarchyItem> {
Ok(lsp_types::CallHierarchyItem {
name: nav.name().to_string(),
kind: nav.kind().conv(),
tags: None,
detail: nav.description().map(|it| it.to_string()),
uri: file_id.try_conv_with(&world)?,
range: nav.range().conv_with(&line_index),
selection_range: range.conv_with(&line_index),
})
}
pub fn to_location(
file_id: FileId,
range: TextRange,

View file

@ -499,6 +499,9 @@ fn on_request(
.on::<req::Formatting>(handlers::handle_formatting)?
.on::<req::DocumentHighlightRequest>(handlers::handle_document_highlight)?
.on::<req::InlayHints>(handlers::handle_inlay_hints)?
.on::<req::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare)?
.on::<req::CallHierarchyIncomingCalls>(handlers::handle_call_hierarchy_incoming)?
.on::<req::CallHierarchyOutgoingCalls>(handlers::handle_call_hierarchy_outgoing)?
.finish();
Ok(())
}

View file

@ -5,13 +5,16 @@ use std::{fmt::Write as _, io::Write as _};
use lsp_server::ErrorCode;
use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeAction, CodeActionResponse, CodeLens, Command, CompletionItem, Diagnostic,
DocumentFormattingParams, DocumentHighlight, DocumentSymbol, FoldingRange, FoldingRangeParams,
Hover, HoverContents, Location, MarkupContent, MarkupKind, Position, PrepareRenameResponse,
Range, RenameParams, SymbolInformation, TextDocumentIdentifier, TextEdit, WorkspaceEdit,
};
use ra_ide::{
AssistId, FileId, FilePosition, FileRange, Query, Runnable, RunnableKind, SearchScope,
AssistId, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind,
SearchScope,
};
use ra_prof::profile;
use ra_syntax::{AstNode, SyntaxKind, TextRange, TextUnit};
@ -21,7 +24,10 @@ use serde_json::to_value;
use crate::{
cargo_target_spec::{runnable_args, CargoTargetSpec},
conv::{to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, TryConvWithToVec},
conv::{
to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith,
TryConvWithToVec,
},
req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind},
world::WorldSnapshot,
LspError, Result,
@ -936,3 +942,91 @@ pub fn handle_inlay_hints(
})
.collect())
}
pub fn handle_call_hierarchy_prepare(
world: WorldSnapshot,
params: CallHierarchyPrepareParams,
) -> Result<Option<Vec<CallHierarchyItem>>> {
let _p = profile("handle_call_hierarchy_prepare");
let position = params.text_document_position_params.try_conv_with(&world)?;
let file_id = position.file_id;
let nav_info = match world.analysis().call_hierarchy(position)? {
None => return Ok(None),
Some(it) => it,
};
let line_index = world.analysis().file_line_index(file_id)?;
let RangeInfo { range, info: navs } = nav_info;
let res = navs
.into_iter()
.filter(|it| it.kind() == SyntaxKind::FN_DEF)
.filter_map(|it| to_call_hierarchy_item(file_id, range, &world, &line_index, it).ok())
.collect();
Ok(Some(res))
}
pub fn handle_call_hierarchy_incoming(
world: WorldSnapshot,
params: CallHierarchyIncomingCallsParams,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
let _p = profile("handle_call_hierarchy_incoming");
let item = params.item;
let doc = TextDocumentIdentifier::new(item.uri);
let frange: FileRange = (&doc, item.range).try_conv_with(&world)?;
let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
let call_items = match world.analysis().incoming_calls(fpos)? {
None => return Ok(None),
Some(it) => it,
};
let mut res = vec![];
for call_item in call_items.into_iter() {
let file_id = call_item.target.file_id();
let line_index = world.analysis().file_line_index(file_id)?;
let range = call_item.target.range();
let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?;
res.push(CallHierarchyIncomingCall {
from: item,
from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(),
});
}
Ok(Some(res))
}
pub fn handle_call_hierarchy_outgoing(
world: WorldSnapshot,
params: CallHierarchyOutgoingCallsParams,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
let _p = profile("handle_call_hierarchy_outgoing");
let item = params.item;
let doc = TextDocumentIdentifier::new(item.uri);
let frange: FileRange = (&doc, item.range).try_conv_with(&world)?;
let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
let call_items = match world.analysis().outgoing_calls(fpos)? {
None => return Ok(None),
Some(it) => it,
};
let mut res = vec![];
for call_item in call_items.into_iter() {
let file_id = call_item.target.file_id();
let line_index = world.analysis().file_line_index(file_id)?;
let range = call_item.target.range();
let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?;
res.push(CallHierarchyOutgoingCall {
to: item,
from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(),
});
}
Ok(Some(res))
}

View file

@ -6,13 +6,15 @@ use serde::{Deserialize, Serialize};
pub use lsp_types::{
notification::*, request::*, ApplyWorkspaceEditParams, CodeActionParams, CodeLens,
CodeLensParams, CompletionParams, CompletionResponse, DidChangeConfigurationParams,
DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
DocumentOnTypeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
FileSystemWatcher, Hover, InitializeResult, MessageType, ProgressParams, ProgressParamsValue,
ProgressToken, PublishDiagnosticsParams, ReferenceParams, Registration, RegistrationParams,
SelectionRange, SelectionRangeParams, ShowMessageParams, SignatureHelp, TextDocumentEdit,
TextDocumentPositionParams, TextEdit, WorkspaceEdit, WorkspaceSymbolParams,
CodeLensParams, CompletionParams, CompletionResponse, DiagnosticTag,
DidChangeConfigurationParams, DidChangeWatchedFilesParams,
DidChangeWatchedFilesRegistrationOptions, DocumentOnTypeFormattingParams, DocumentSymbolParams,
DocumentSymbolResponse, FileSystemWatcher, Hover, InitializeResult, MessageType,
PartialResultParams, ProgressParams, ProgressParamsValue, ProgressToken,
PublishDiagnosticsParams, ReferenceParams, Registration, RegistrationParams, SelectionRange,
SelectionRangeParams, ServerCapabilities, ShowMessageParams, SignatureHelp, SymbolKind,
TextDocumentEdit, TextDocumentPositionParams, TextEdit, WorkDoneProgressParams, WorkspaceEdit,
WorkspaceSymbolParams,
};
pub enum AnalyzerStatus {}