use super::*;
#[derive(Debug, Clone)]
struct FnDef {
path: Vec<SmolStr>,
binding: BindingId,
selection: TextRange,
full: TextRange,
}
impl FnDef {
fn name(&self) -> &str {
self.path.last().map_or("", SmolStr::as_str)
}
}
struct FileFunctions {
defs: Vec<(FnDef, FunctionExpr)>,
owners: HashMap<ScopeId, usize>,
}
impl FileFunctions {
fn def(&self, idx: usize) -> &FnDef {
&self.defs[idx].0
}
fn by_path(&self, path: &[SmolStr]) -> Option<usize> {
self.defs.iter().position(|(d, _)| d.path == path)
}
fn by_binding(&self, binding: BindingId) -> Option<usize> {
self.defs.iter().position(|(d, _)| d.binding == binding)
}
fn by_selection(&self, range: TextRange) -> Option<usize> {
self.defs.iter().position(|(d, _)| d.selection == range)
}
}
fn function_defs(root: &SyntaxNode, model: &SemanticModel) -> FileFunctions {
let mut by_target: HashMap<TextRange, (SyntaxNode, FunctionExpr)> = HashMap::new();
for node in root.descendants() {
let Some(assign) = AssignmentExpr::cast(node.clone()) else {
continue;
};
let Some(name_token) = assign.target_name_token() else {
continue;
};
let Some(NodeOrToken::Node(value)) = assign.value_element() else {
continue;
};
let Some(func) = FunctionExpr::cast(value) else {
continue;
};
by_target.insert(name_token.text_range(), (node, func));
}
let scope_by_range: HashMap<TextRange, ScopeId> = model
.scopes()
.iter()
.enumerate()
.filter(|(_, s)| s.kind == ScopeKind::Function)
.map(|(i, s)| (s.range, ScopeId::from_index(i)))
.collect();
let mut defs: Vec<(FnDef, FunctionExpr)> = Vec::new();
let mut owners: HashMap<ScopeId, usize> = HashMap::new();
for (i, b) in model.bindings().iter().enumerate() {
if !matches!(b.kind, BindingKind::Local | BindingKind::Implicit) {
continue;
}
let Some((assign, func)) = by_target.get(&b.def_range) else {
continue;
};
if let Some(scope) = scope_by_range.get(&func.syntax().text_range()) {
owners.entry(*scope).or_insert(defs.len());
}
defs.push((
FnDef {
path: vec![b.name.clone()],
binding: BindingId::from_index(i),
selection: b.def_range,
full: assign.text_range(),
},
func.clone(),
));
}
for idx in 0..defs.len() {
let mut chain: Vec<SmolStr> = Vec::new();
let mut current = Some(model.binding(defs[idx].0.binding).scope);
while let Some(scope_id) = current {
let scope = model.scope(scope_id);
if scope.kind == ScopeKind::Function
&& let Some(&owner) = owners.get(&scope_id)
{
chain.push(SmolStr::new(defs[owner].0.name()));
}
current = scope.parent;
}
chain.reverse();
chain.push(SmolStr::new(defs[idx].0.name()));
defs[idx].0.path = chain;
}
FileFunctions { defs, owners }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ItemData {
path: Vec<SmolStr>,
}
fn item_path(item: &CallHierarchyItem) -> Vec<SmolStr> {
item.data
.clone()
.and_then(|data| serde_json::from_value::<ItemData>(data).ok())
.filter(|d| !d.path.is_empty())
.map(|d| d.path)
.unwrap_or_else(|| vec![SmolStr::new(&item.name)])
}
fn nested_detail(path: &[SmolStr]) -> Option<String> {
let enclosing = path.split_last()?.1;
(!enclosing.is_empty()).then(|| {
enclosing
.iter()
.map(SmolStr::as_str)
.collect::<Vec<_>>()
.join("/")
})
}
fn fn_def_to_item(
def: &FnDef,
uri: &Uri,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> CallHierarchyItem {
CallHierarchyItem {
name: def.name().to_string(),
kind: LspSymbolKind::FUNCTION,
tags: None,
detail: nested_detail(&def.path),
uri: uri.clone(),
range: text_range_to_lsp_range(line_index, def.full, encoding),
selection_range: text_range_to_lsp_range(line_index, def.selection, encoding),
data: serde_json::to_value(ItemData {
path: def.path.clone(),
})
.ok(),
}
}
fn function_item(
snapshot: &Analysis,
path: &Path,
fn_path: &[SmolStr],
encoding: PositionEncoding,
) -> Option<CallHierarchyItem> {
let file = snapshot.lookup_file(path)?;
let uri = uri::from_path(path)?;
let root = snapshot.parsed_tree(file);
let model = snapshot.semantic_model(file);
let functions = function_defs(&root, model);
let def = functions.def(functions.by_path(fn_path)?);
let line_index = snapshot.line_index(file);
Some(fn_def_to_item(def, &uri, line_index, encoding))
}
pub(crate) fn prepare_call_hierarchy_via_db(
snapshot: &Analysis,
path: &Path,
uri: &Uri,
text: &str,
position: Position,
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyItem>> {
let line_index = LineIndex::new(text);
let offset = TextSize::new(
line_index
.position_to_byte(position, encoding)
.min(text.len()) as u32,
);
let root = parse(text).cst;
let model = SemanticModel::build(&root);
if let Some(items) = prepare_local(&root, &model, offset, uri, &line_index, encoding) {
return Some(items);
}
let token = pick_name_token(&root, offset)?;
if token.kind() != SyntaxKind::IDENT
|| matches!(
symbol_query_at(&root, offset),
Some(SymbolQuery::Namespaced { .. })
)
{
return None;
}
let name = SmolStr::new(token.text());
let fn_path = std::slice::from_ref(&name);
let items = salsa::Cancelled::catch(AssertUnwindSafe(|| {
snapshot
.workspace_def_sites(&name)
.into_iter()
.filter(|(def_path, _)| def_path != path)
.filter_map(|(def_path, _)| function_item(snapshot, &def_path, fn_path, encoding))
.collect::<Vec<_>>()
}))
.unwrap_or_default();
(!items.is_empty()).then_some(items)
}
fn prepare_local(
root: &SyntaxNode,
model: &SemanticModel,
offset: TextSize,
uri: &Uri,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyItem>> {
let token = pick_name_token(root, offset)?;
if token.kind() != SyntaxKind::IDENT {
return None;
}
let range = token.text_range();
let functions = function_defs(root, model);
if model.bindings().iter().any(|b| b.def_range == range) {
return Some(
functions
.by_selection(range)
.map(|idx| {
vec![fn_def_to_item(
functions.def(idx),
uri,
line_index,
encoding,
)]
})
.unwrap_or_default(),
);
}
let ident = model.idents().iter().find(|i| i.range == range)?;
let binding = model.resolve_local(ident)?;
Some(
functions
.by_binding(binding)
.map(|idx| {
vec![fn_def_to_item(
functions.def(idx),
uri,
line_index,
encoding,
)]
})
.unwrap_or_default(),
)
}
pub(crate) fn incoming_calls_via_db(
snapshot: &Analysis,
item: &CallHierarchyItem,
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyIncomingCall>> {
let path = uri::to_path(&item.uri)?;
let fn_path = item_path(item);
salsa::Cancelled::catch(AssertUnwindSafe(|| {
incoming_calls(snapshot, &path, &fn_path, encoding)
}))
.ok()
.flatten()
}
enum RefSet<'a> {
FileScope(&'a str),
Free(&'a str),
Local(&'a [SmolStr]),
}
fn incoming_calls(
snapshot: &Analysis,
def_path: &Path,
fn_path: &[SmolStr],
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyIncomingCall>> {
let mut groups: Vec<IncomingGroup> = Vec::new();
match fn_path {
[name] => {
let binding = snapshot.cross_file_binding(def_path, name);
for member in &binding.cohort {
collect_incoming(
snapshot,
member,
RefSet::FileScope(name),
&mut groups,
encoding,
);
}
for reader in &binding.readers {
collect_incoming(snapshot, reader, RefSet::Free(name), &mut groups, encoding);
}
}
_ => collect_incoming(
snapshot,
def_path,
RefSet::Local(fn_path),
&mut groups,
encoding,
),
}
Some(
groups
.into_iter()
.map(|g| CallHierarchyIncomingCall {
from: g.from,
from_ranges: g.from_ranges,
})
.collect(),
)
}
struct IncomingGroup {
uri: Uri,
selection: TextRange,
from: CallHierarchyItem,
from_ranges: Vec<Range>,
}
fn collect_incoming(
snapshot: &Analysis,
file_path: &Path,
refs: RefSet<'_>,
groups: &mut Vec<IncomingGroup>,
encoding: PositionEncoding,
) {
let Some(file) = snapshot.lookup_file(file_path) else {
return;
};
let Some(uri) = uri::from_path(file_path) else {
return;
};
let root = snapshot.parsed_tree(file);
let model = snapshot.semantic_model(file);
let line_index = snapshot.line_index(file);
let functions = function_defs(&root, model);
let ref_ranges: Vec<TextRange> = match refs {
RefSet::Free(name) => snapshot.read_ranges_in(file, name),
RefSet::FileScope(name) => file_scope_occurrences_in(model, name)
.map(|(_, reads)| reads)
.unwrap_or_default(),
RefSet::Local(path) => functions
.by_path(path)
.map(|idx| variable_occurrences(model, functions.def(idx).binding).1)
.unwrap_or_default(),
};
for range in ref_ranges {
if call_at_callee(&root, range).is_none() {
continue;
}
let Some(caller) = enclosing_function(model, &functions.owners, range) else {
continue; };
let caller = functions.def(caller);
let from_range = text_range_to_lsp_range(line_index, range, encoding);
match groups
.iter_mut()
.find(|g| g.uri == uri && g.selection == caller.selection)
{
Some(group) => group.from_ranges.push(from_range),
None => groups.push(IncomingGroup {
uri: uri.clone(),
selection: caller.selection,
from: fn_def_to_item(caller, &uri, line_index, encoding),
from_ranges: vec![from_range],
}),
}
}
}
pub(crate) fn outgoing_calls_via_db(
snapshot: &Analysis,
item: &CallHierarchyItem,
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyOutgoingCall>> {
let path = uri::to_path(&item.uri)?;
let fn_path = item_path(item);
salsa::Cancelled::catch(AssertUnwindSafe(|| {
outgoing_calls(snapshot, &path, &fn_path, encoding)
}))
.ok()
.flatten()
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CalleeTarget {
Local { path: Vec<SmolStr> },
CrossFile { file: PathBuf, name: SmolStr },
}
struct OutgoingGroup {
target: CalleeTarget,
from_ranges: Vec<TextRange>,
}
fn outgoing_calls(
snapshot: &Analysis,
path: &Path,
fn_path: &[SmolStr],
encoding: PositionEncoding,
) -> Option<Vec<CallHierarchyOutgoingCall>> {
let file = snapshot.lookup_file(path)?;
let uri = uri::from_path(path)?;
let root = snapshot.parsed_tree(file);
let model = snapshot.semantic_model(file);
let line_index = snapshot.line_index(file);
let functions = function_defs(&root, model);
let idx = functions.by_path(fn_path)?;
let func = functions.defs[idx].1.clone();
let mut groups: Vec<OutgoingGroup> = Vec::new();
for call_node in func.syntax().descendants() {
if call_node.kind() != SyntaxKind::CALL_EXPR {
continue;
}
let Some(call) = CallExpr::cast(call_node.clone()) else {
continue;
};
let Some(callee) = call.callee_token() else {
continue; };
if callee.kind() != SyntaxKind::IDENT {
continue;
}
if call_node
.parent()
.and_then(BinaryExpr::cast)
.and_then(|b| b.namespace_access())
.is_some()
{
continue;
}
if enclosing_function(model, &functions.owners, callee.text_range()) != Some(idx) {
continue;
}
let Some(target) = resolve_callee(snapshot, model, &functions, path, &callee, encoding)
else {
continue;
};
let range = callee.text_range();
match groups.iter_mut().find(|g| g.target == target) {
Some(group) => group.from_ranges.push(range),
None => groups.push(OutgoingGroup {
target,
from_ranges: vec![range],
}),
}
}
Some(
groups
.into_iter()
.filter_map(|g| {
let to = match &g.target {
CalleeTarget::Local { path } => {
let idx = functions.by_path(path)?;
fn_def_to_item(functions.def(idx), &uri, line_index, encoding)
}
CalleeTarget::CrossFile { file, name } => {
function_item(snapshot, file, std::slice::from_ref(name), encoding)?
}
};
Some(CallHierarchyOutgoingCall {
to,
from_ranges: g
.from_ranges
.iter()
.map(|r| text_range_to_lsp_range(line_index, *r, encoding))
.collect(),
})
})
.collect(),
)
}
fn resolve_callee(
snapshot: &Analysis,
model: &SemanticModel,
functions: &FileFunctions,
from_path: &Path,
callee: &SyntaxToken<RLanguage>,
encoding: PositionEncoding,
) -> Option<CalleeTarget> {
let range = callee.text_range();
if let Some(ident) = model.idents().iter().find(|i| i.range == range)
&& let Some(binding) = model.resolve_local(ident)
{
let idx = functions.by_binding(binding)?;
return Some(CalleeTarget::Local {
path: functions.def(idx).path.clone(),
});
}
let name = SmolStr::new(callee.text());
snapshot
.visible_def_files(from_path, &name)
.into_iter()
.find(|p| function_item(snapshot, p, std::slice::from_ref(&name), encoding).is_some())
.map(|file| CalleeTarget::CrossFile { file, name })
}
fn call_at_callee(root: &SyntaxNode, range: TextRange) -> Option<SyntaxNode> {
let token = match root.token_at_offset(range.start()) {
TokenAtOffset::None => return None,
TokenAtOffset::Single(t) => t,
TokenAtOffset::Between(left, right) => {
if left.text_range() == range {
left
} else {
right
}
}
};
let call = token.parent()?;
if call.kind() != SyntaxKind::CALL_EXPR {
return None;
}
if CallExpr::cast(call.clone())?.callee_token()?.text_range() != range {
return None;
}
if call
.parent()
.and_then(BinaryExpr::cast)
.and_then(|b| b.namespace_access())
.is_some()
{
return None;
}
Some(call)
}
fn enclosing_function(
model: &SemanticModel,
owners: &HashMap<ScopeId, usize>,
range: TextRange,
) -> Option<usize> {
let mut current = Some(model.innermost_scope_at(range.start()));
while let Some(scope_id) = current {
if let Some(&idx) = owners.get(&scope_id) {
return Some(idx);
}
current = model.scope(scope_id).parent;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn prepare_at(
snapshot: &Analysis,
path: &Path,
text: &str,
offset: usize,
) -> Vec<CallHierarchyItem> {
let uri = uri::from_path(path).unwrap();
prepare_call_hierarchy_via_db(
snapshot,
path,
&uri,
text,
pos_at(text, offset),
PositionEncoding::Utf16,
)
.unwrap_or_default()
}
fn item_named(snapshot: &Analysis, path: &Path, name: &str) -> CallHierarchyItem {
nested_item(snapshot, path, &[name])
}
fn nested_item(snapshot: &Analysis, path: &Path, fn_path: &[&str]) -> CallHierarchyItem {
let fn_path: Vec<SmolStr> = fn_path.iter().copied().map(SmolStr::new).collect();
function_item(snapshot, path, &fn_path, PositionEncoding::Utf16).expect("function item")
}
fn path_of(item: &CallHierarchyItem) -> Vec<String> {
item_path(item).iter().map(SmolStr::to_string).collect()
}
#[test]
fn prepare_on_a_definition_yields_its_item() {
let src = "foo <- function() 1\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(&snapshot, &ws_path("a.R"), src, src.find("foo").unwrap());
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "foo");
assert_eq!(items[0].kind, LspSymbolKind::FUNCTION);
}
#[test]
fn prepare_on_a_call_site_yields_the_callee_item() {
let src = "foo <- function() 1\nbar <- function() foo()\n";
let snapshot = rename_workspace(src, "");
let offset = src.find("foo()").unwrap();
let items = prepare_at(&snapshot, &ws_path("a.R"), src, offset);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "foo");
}
#[test]
fn prepare_on_a_nested_definition_yields_its_item() {
let src = "outer <- function() {\n inner <- function() 1\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(
&snapshot,
&ws_path("a.R"),
src,
src.find("inner <-").unwrap(),
);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "inner");
assert_eq!(items[0].kind, LspSymbolKind::FUNCTION);
assert_eq!(items[0].detail.as_deref(), Some("outer"));
assert_eq!(path_of(&items[0]), ["outer", "inner"]);
}
#[test]
fn prepare_on_a_nested_call_site_yields_the_nested_item() {
let src = "outer <- function() {\n inner <- function() 1\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(
&snapshot,
&ws_path("a.R"),
src,
src.find("inner()").unwrap(),
);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "inner");
assert_eq!(path_of(&items[0]), ["outer", "inner"]);
}
#[test]
fn prepare_declines_a_non_function_binding() {
let src = "x <- 1\nprint(x)\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(&snapshot, &ws_path("a.R"), src, src.find("x <-").unwrap());
assert!(items.is_empty());
}
#[test]
fn prepare_resolves_a_cross_file_callee() {
let a_src = "foo <- function() 1\n";
let b_src = "source(\"a.R\")\nbar <- function() foo()\n";
let snapshot = rename_workspace(a_src, b_src);
let items = prepare_at(
&snapshot,
&ws_path("b.R"),
b_src,
b_src.find("foo()").unwrap(),
);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "foo");
assert_eq!(items[0].uri, uri::from_path(&ws_path("a.R")).unwrap());
}
#[test]
fn prepare_declines_a_nested_non_function_local() {
let a_src = "outer <- function() {\n x <- 1\n print(x)\n}\n";
let b_src = "x <- function() 1\n";
let snapshot = rename_workspace(a_src, b_src);
let items = prepare_at(
&snapshot,
&ws_path("a.R"),
a_src,
a_src.find("x <- 1").unwrap(),
);
assert!(items.is_empty(), "a nested non-function is not an item");
}
#[test]
fn prepare_prefers_a_shadowing_nested_function_over_a_sibling_file() {
let a_src =
"source(\"b.R\")\nouter <- function() {\n helper <- function() 1\n helper()\n}\n";
let b_src = "helper <- function() 2\n";
let snapshot = rename_workspace(a_src, b_src);
let a = ws_path("a.R");
let items = prepare_at(&snapshot, &a, a_src, a_src.find("helper()").unwrap());
assert_eq!(items.len(), 1);
assert_eq!(items[0].uri, uri::from_path(&a).unwrap());
assert_eq!(path_of(&items[0]), ["outer", "helper"]);
}
#[test]
fn outgoing_collects_intra_file_calls() {
let src = "helper <- function() 1\nmain <- function() {\n helper()\n helper()\n}\n";
let snapshot = rename_workspace(src, "");
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "main"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.name, "helper");
assert_eq!(calls[0].from_ranges.len(), 2, "both call sites reported");
}
#[test]
fn outgoing_skips_namespaced_and_unresolved_calls() {
let src = "main <- function() {\n dplyr::filter(x)\n undefined_fn()\n}\n";
let snapshot = rename_workspace(src, "");
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "main"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert!(calls.is_empty(), "no top-level function callee resolves");
}
#[test]
fn outgoing_resolves_a_cross_file_callee() {
let a_src = "foo <- function() 1\n";
let b_src = "source(\"a.R\")\nbar <- function() foo()\n";
let snapshot = rename_workspace(a_src, b_src);
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("b.R"), "bar"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.name, "foo");
assert_eq!(calls[0].to.uri, uri::from_path(&ws_path("a.R")).unwrap());
}
#[test]
fn outgoing_reports_a_nested_function_as_a_callee() {
let src = "outer <- function() {\n inner <- function() 1\n inner()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let a = ws_path("a.R");
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &a, "outer"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.name, "inner");
assert_eq!(calls[0].to.detail.as_deref(), Some("outer"));
assert_eq!(calls[0].to.uri, uri::from_path(&a).unwrap());
assert_eq!(calls[0].from_ranges.len(), 2);
}
#[test]
fn outgoing_stops_at_a_nested_function_boundary() {
let src = "foo <- function() 1\nouter <- function() {\n inner <- function() foo()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let a = ws_path("a.R");
let outer = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &a, "outer"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(outer.len(), 1, "foo() belongs to inner, not outer");
assert_eq!(outer[0].to.name, "inner");
let inner = outgoing_calls_via_db(
&snapshot,
&nested_item(&snapshot, &a, &["outer", "inner"]),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(inner.len(), 1);
assert_eq!(inner[0].to.name, "foo");
}
#[test]
fn outgoing_prefers_a_shadowing_local_over_a_sibling_definition() {
let a_src =
"source(\"b.R\")\nouter <- function() {\n helper <- function() 1\n helper()\n}\n";
let b_src = "helper <- function() 2\n";
let snapshot = rename_workspace(a_src, b_src);
let a = ws_path("a.R");
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &a, "outer"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.uri, uri::from_path(&a).unwrap());
assert_eq!(path_of(&calls[0].to), ["outer", "helper"]);
}
#[test]
fn outgoing_skips_a_call_through_a_non_function_local() {
let a_src = "source(\"b.R\")\nouter <- function(helper) {\n helper()\n}\n";
let b_src = "helper <- function() 2\n";
let snapshot = rename_workspace(a_src, b_src);
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "outer"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert!(
calls.is_empty(),
"a parameter shadows the sibling definition"
);
}
#[test]
fn outgoing_includes_a_call_in_a_parameter_default() {
let src = "helper <- function() 1\nmain <- function(x = helper()) x\n";
let snapshot = rename_workspace(src, "");
let calls = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "main"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.name, "helper");
}
#[test]
fn prepare_round_trips_a_nested_item_into_outgoing() {
let src = "foo <- function() 1\nouter <- function() {\n inner <- function() foo()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(
&snapshot,
&ws_path("a.R"),
src,
src.find("inner <-").unwrap(),
);
assert_eq!(items.len(), 1);
let calls =
outgoing_calls_via_db(&snapshot, &items[0], PositionEncoding::Utf16).expect("outgoing");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].to.name, "foo");
}
#[test]
fn incoming_finds_callers_across_a_source_edge() {
let a_src = "foo <- function() 1\n";
let b_src = "source(\"a.R\")\nbar <- function() foo()\n";
let snapshot = rename_workspace(a_src, b_src);
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "bar");
assert_eq!(calls[0].from.uri, uri::from_path(&ws_path("b.R")).unwrap());
assert_eq!(calls[0].from_ranges.len(), 1);
}
#[test]
fn incoming_drops_script_level_calls() {
let src = "foo <- function() 1\nfoo()\n";
let snapshot = rename_workspace(src, "");
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert!(calls.is_empty(), "script-level call site is dropped in v1");
}
#[test]
fn incoming_attributes_a_call_to_the_innermost_named_function() {
let src = "foo <- function() 1\nouter <- function() {\n inner <- function() foo()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "inner");
assert_eq!(calls[0].from.detail.as_deref(), Some("outer"));
assert_eq!(path_of(&calls[0].from), ["outer", "inner"]);
}
#[test]
fn incoming_attributes_an_anonymous_function_call_to_the_enclosing_named_function() {
let src = "foo <- function() 1\nouter <- function() lapply(xs, function(x) foo(x))\n";
let snapshot = rename_workspace(src, "");
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "outer");
assert_eq!(path_of(&calls[0].from), ["outer"]);
}
#[test]
fn incoming_distinguishes_same_named_nested_functions() {
let src = "outer1 <- function() {\n inner <- function() 1\n inner()\n}\n\
outer2 <- function() {\n inner <- function() 2\n inner()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let a = ws_path("a.R");
let first = incoming_calls_via_db(
&snapshot,
&nested_item(&snapshot, &a, &["outer1", "inner"]),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(first.len(), 1);
assert_eq!(first[0].from.name, "outer1");
assert_eq!(first[0].from_ranges.len(), 1);
let second = incoming_calls_via_db(
&snapshot,
&nested_item(&snapshot, &a, &["outer2", "inner"]),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(second.len(), 1);
assert_eq!(second[0].from.name, "outer2");
assert_eq!(second[0].from_ranges.len(), 2);
}
#[test]
fn incoming_finds_callers_of_a_nested_function() {
let src = "outer <- function() {\n inner <- function() 1\n inner()\n inner()\n}\n";
let snapshot = rename_workspace(src, "");
let calls = incoming_calls_via_db(
&snapshot,
&nested_item(&snapshot, &ws_path("a.R"), &["outer", "inner"]),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "outer");
assert_eq!(calls[0].from_ranges.len(), 2);
}
#[test]
fn incoming_for_a_nested_function_is_intra_file() {
let a_src = "outer <- function() {\n inner <- function() 1\n inner()\n}\n";
let b_src = "inner <- function() 2\ncaller <- function() inner()\n";
let snapshot = rename_workspace(a_src, b_src);
let a = ws_path("a.R");
let calls = incoming_calls_via_db(
&snapshot,
&nested_item(&snapshot, &a, &["outer", "inner"]),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.uri, uri::from_path(&a).unwrap());
assert_eq!(calls[0].from.name, "outer");
}
#[test]
fn super_assigned_function_is_a_file_scope_item() {
let src =
"outer <- function() {\n helper <<- function() 1\n}\nmain <- function() helper()\n";
let snapshot = rename_workspace(src, "");
let a = ws_path("a.R");
let item = item_named(&snapshot, &a, "helper");
assert_eq!(path_of(&item), ["helper"]);
assert_eq!(item.detail, None);
let calls =
incoming_calls_via_db(&snapshot, &item, PositionEncoding::Utf16).expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "main");
let outgoing = outgoing_calls_via_db(
&snapshot,
&item_named(&snapshot, &a, "outer"),
PositionEncoding::Utf16,
)
.expect("outgoing");
assert!(
outgoing.is_empty(),
"outer defines helper, it does not call it"
);
}
#[test]
fn package_sibling_incoming_attributes_to_a_nested_caller() {
let a_src = "foo <- function() 1\n";
let b_src = "outer <- function() {\n inner <- function() foo()\n inner()\n}\n";
let (_dir, snapshot, a_path, b_path) = rename_package(a_src, b_src);
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &a_path, "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "inner");
assert_eq!(calls[0].from.detail.as_deref(), Some("outer"));
assert_eq!(calls[0].from.uri, uri::from_path(&b_path).unwrap());
assert_eq!(path_of(&calls[0].from), ["outer", "inner"]);
}
#[test]
fn an_item_without_data_falls_back_to_the_file_scope_name() {
let src = "foo <- function() 1\nbar <- function() foo()\n";
let snapshot = rename_workspace(src, "");
let mut item = item_named(&snapshot, &ws_path("a.R"), "foo");
item.data = None;
let calls =
incoming_calls_via_db(&snapshot, &item, PositionEncoding::Utf16).expect("incoming");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "bar");
}
#[test]
fn incoming_excludes_a_value_use() {
let src = "foo <- function() 1\nbar <- function() lapply(xs, foo)\n";
let snapshot = rename_workspace(src, "");
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert!(calls.is_empty(), "a value use is not a call");
}
#[test]
fn incoming_excludes_a_disjoint_same_name_def() {
let a_src = "foo <- function() 1\n";
let b_src = "foo <- function() 2\nbar <- function() foo()\n";
let snapshot = rename_workspace(a_src, b_src);
let calls = incoming_calls_via_db(
&snapshot,
&item_named(&snapshot, &ws_path("a.R"), "foo"),
PositionEncoding::Utf16,
)
.expect("incoming");
assert!(
calls.is_empty(),
"b.R's foo is a disjoint binding; a.R's foo has no callers"
);
}
}