use std::collections::HashMap;
use tower_lsp::lsp_types::{CallHierarchyItem, Range, SymbolKind, Url};
use crate::goto::{CachedBuild, LOW_LEVEL_CALL_NAMES, NodeInfo, bytes_to_pos};
use crate::references::byte_to_id;
use crate::solc_ast::DeclNode;
use crate::types::{AbsPath, NodeId, SolcFileId, SourceLoc};
pub fn verify_node_identity(
nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
node_id: NodeId,
expected_abs_path: &str,
expected_name_offset: usize,
expected_name: &str,
) -> bool {
let Some(file_nodes) = nodes.get(expected_abs_path) else {
return false;
};
let Some(info) = file_nodes.get(&node_id) else {
return false;
};
let name_offset_matches = info
.name_location
.as_deref()
.and_then(SourceLoc::parse)
.is_some_and(|loc| loc.offset == expected_name_offset);
if !name_offset_matches {
return false;
}
if let Some(nl) = info.name_location.as_deref()
&& let Some(loc) = SourceLoc::parse(nl)
{
let name_matches = std::path::Path::new(expected_abs_path)
.exists()
.then(|| std::fs::read(expected_abs_path).ok())
.flatten()
.is_some_and(|source_bytes| {
source_bytes
.get(loc.offset..loc.end())
.is_some_and(|slice| slice == expected_name.as_bytes())
});
return name_matches;
}
false
}
pub fn resolve_target_in_build(
build: &CachedBuild,
node_id: NodeId,
target_abs: &str,
target_name: &str,
target_name_offset: usize,
) -> Vec<NodeId> {
let mut ids = Vec::new();
if verify_node_identity(
&build.nodes,
node_id,
target_abs,
target_name_offset,
target_name,
) {
ids.push(node_id);
return ids;
}
if let Some(resolved_id) = byte_to_id(&build.nodes, target_abs, target_name_offset) {
if let Some(file_nodes) = build.nodes.get(target_abs)
&& let Some(info) = file_nodes.get(&resolved_id)
{
let nt = info.node_type.as_deref().unwrap_or("");
if matches!(
nt,
"FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
) {
ids.push(resolved_id);
}
}
}
ids
}
pub fn incoming_calls(
nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
target_ids: &[NodeId],
) -> Vec<(NodeId, String)> {
let mut results = Vec::new();
for file_nodes in nodes.values() {
let callables: Vec<(NodeId, &NodeInfo)> = file_nodes
.iter()
.filter(|(_id, info)| {
info.node_type
.as_deref()
.is_some_and(|nt| nt == "FunctionDefinition" || nt == "ModifierDefinition")
})
.map(|(id, info)| (*id, info))
.collect();
for (ref_id, ref_info) in file_nodes {
let Some(ref_decl) = ref_info.referenced_declaration else {
continue;
};
if !target_ids.contains(&ref_decl) {
continue;
}
if target_ids.contains(ref_id) {
continue;
}
let ref_src = match SourceLoc::parse(ref_info.src.as_str()) {
Some(s) => s,
None => continue,
};
let mut best_callable: Option<(NodeId, usize)> = None;
for &(callable_id, callable_info) in &callables {
let Some(callable_src) = SourceLoc::parse(callable_info.src.as_str()) else {
continue;
};
if callable_src.file_id != ref_src.file_id {
continue;
}
if callable_src.offset <= ref_src.offset && ref_src.end() <= callable_src.end() {
let span = callable_src.length;
if best_callable.is_none() || span < best_callable.unwrap().1 {
best_callable = Some((callable_id, span));
}
}
}
if let Some((caller_id, _)) = best_callable {
let call_src = ref_info
.member_location
.as_deref()
.unwrap_or(ref_info.src.as_str())
.to_string();
results.push((caller_id, call_src));
}
}
}
results.sort_by(|a, b| a.0.0.cmp(&b.0.0).then_with(|| a.1.cmp(&b.1)));
results.dedup();
results
}
pub fn outgoing_calls(
nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
caller_id: NodeId,
) -> Vec<(NodeId, String)> {
let caller_info = match find_node_info(nodes, caller_id) {
Some(info) => info,
None => return vec![],
};
let caller_src = match SourceLoc::parse(caller_info.src.as_str()) {
Some(s) => s,
None => return vec![],
};
let callable_ids: std::collections::HashSet<NodeId> = nodes
.values()
.flat_map(|file_nodes| {
file_nodes.iter().filter_map(|(id, info)| {
info.node_type.as_deref().and_then(|nt| {
if nt == "FunctionDefinition" || nt == "ModifierDefinition" {
Some(*id)
} else {
None
}
})
})
})
.collect();
let mut results = Vec::new();
for file_nodes in nodes.values() {
for ref_info in file_nodes.values() {
let Some(ref_decl) = ref_info.referenced_declaration else {
continue;
};
if !callable_ids.contains(&ref_decl) {
continue;
}
let Some(ref_src) = SourceLoc::parse(ref_info.src.as_str()) else {
continue;
};
if ref_src.file_id != caller_src.file_id {
continue;
}
if caller_src.offset <= ref_src.offset && ref_src.end() <= caller_src.end() {
if ref_decl == caller_id {
continue;
}
let call_src = ref_info
.member_location
.as_deref()
.unwrap_or(ref_info.src.as_str())
.to_string();
results.push((ref_decl, call_src));
}
}
}
results.sort_by(|a, b| a.0.0.cmp(&b.0.0).then_with(|| a.1.cmp(&b.1)));
results.dedup();
results
}
pub fn outgoing_low_level_calls(
build: &CachedBuild,
caller_id: NodeId,
) -> Vec<(CallHierarchyItem, Range)> {
let caller_info = match find_node_info(&build.nodes, caller_id) {
Some(info) => info,
None => return vec![],
};
let caller_src = match SourceLoc::parse(caller_info.src.as_str()) {
Some(s) => s,
None => return vec![],
};
let mut results: Vec<(CallHierarchyItem, Range)> = Vec::new();
for file_nodes in build.nodes.values() {
for ref_info in file_nodes.values() {
if ref_info.referenced_declaration.is_some() {
continue;
}
let Some(mn) = ref_info.member_name.as_deref() else {
continue;
};
if !LOW_LEVEL_CALL_NAMES.contains(&mn) {
continue;
}
let Some(ref_src) = SourceLoc::parse(ref_info.src.as_str()) else {
continue;
};
if ref_src.file_id != caller_src.file_id {
continue;
}
if !(caller_src.offset <= ref_src.offset && ref_src.end() <= caller_src.end()) {
continue;
}
let name_src = ref_info
.member_location
.as_deref()
.unwrap_or(ref_info.src.as_str());
if let Some((item, call_range)) = low_level_call_item_from_src(
mn,
ref_info.src.as_str(),
name_src,
&build.id_to_path_map,
) {
results.push((item, call_range));
}
}
}
for llc in &build.low_level_calls {
let Some(llc_src) = SourceLoc::parse(llc.src.as_str()) else {
continue;
};
if llc_src.file_id != caller_src.file_id {
continue;
}
if !(caller_src.offset <= llc_src.offset && llc_src.end() <= caller_src.end()) {
continue;
}
if let Some((item, call_range)) = low_level_call_item_from_src(
&llc.kind,
llc.src.as_str(),
llc.name_src.as_str(),
&build.id_to_path_map,
) {
results.push((item, call_range));
}
}
results
}
fn low_level_call_item_from_src(
kind: &str,
src: &str,
name_src: &str,
id_to_path_map: &HashMap<SolcFileId, String>,
) -> Option<(CallHierarchyItem, Range)> {
let loc = SourceLoc::parse(src)?;
let file_path_str = id_to_path_map.get(&loc.file_id_str())?;
let file_path = if std::path::Path::new(file_path_str).is_absolute() {
std::path::PathBuf::from(file_path_str)
} else {
std::env::current_dir().ok()?.join(file_path_str)
};
let source_bytes = std::fs::read(&file_path).ok()?;
let uri = Url::from_file_path(&file_path).ok()?;
let start = bytes_to_pos(&source_bytes, loc.offset)?;
let end = bytes_to_pos(&source_bytes, loc.end())?;
let range = Range { start, end };
let selection_range = SourceLoc::parse(name_src)
.and_then(|nl| {
let ns = bytes_to_pos(&source_bytes, nl.offset)?;
let ne = bytes_to_pos(&source_bytes, nl.end())?;
Some(Range { start: ns, end: ne })
})
.unwrap_or(range);
let name = source_bytes
.get(loc.offset..loc.end())
.and_then(|slice| std::str::from_utf8(slice).ok())
.map(|s| s.to_string())
.unwrap_or_else(|| format!(".{}()", kind));
let item = CallHierarchyItem {
name,
kind: SymbolKind::FUNCTION,
tags: None,
detail: Some(format!("low-level {}", kind)),
uri,
range,
selection_range,
data: Some(serde_json::json!({ "lowLevelCall": kind })),
};
Some((item, selection_range))
}
pub fn decl_to_hierarchy_item(
decl: &DeclNode,
node_id: NodeId,
node_id_to_source_path: &HashMap<NodeId, AbsPath>,
id_to_path_map: &HashMap<SolcFileId, String>,
nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
) -> Option<CallHierarchyItem> {
let abs_path = node_id_to_source_path.get(&node_id)?;
let file_path = find_file_path(abs_path.as_str(), id_to_path_map)?;
let source_bytes = std::fs::read(&file_path).ok()?;
let uri = Url::from_file_path(&file_path).ok()?;
let src_loc = SourceLoc::parse(decl.src())?;
let start = bytes_to_pos(&source_bytes, src_loc.offset)?;
let end = bytes_to_pos(&source_bytes, src_loc.end())?;
let range = Range { start, end };
let selection_range = find_node_info(nodes, node_id)
.and_then(|info| {
name_loc_range(
&info.name_location.as_ref().map(|s| s.to_string()),
&source_bytes,
)
})
.unwrap_or(range);
let symbol_kind = match decl {
DeclNode::FunctionDefinition(_) => SymbolKind::FUNCTION,
DeclNode::ModifierDefinition(_) => SymbolKind::FUNCTION,
DeclNode::ContractDefinition(c) => match c.contract_kind {
crate::solc_ast::ContractKind::Interface => SymbolKind::INTERFACE,
_ => SymbolKind::CLASS,
},
_ => SymbolKind::FUNCTION,
};
let data = Some(serde_json::json!({ "nodeId": node_id.0 }));
Some(CallHierarchyItem {
name: decl.name().to_string(),
kind: symbol_kind,
tags: None,
detail: decl.build_signature(),
uri,
range,
selection_range,
data,
})
}
pub fn node_info_to_hierarchy_item(
node_id: NodeId,
info: &NodeInfo,
id_to_path_map: &HashMap<SolcFileId, String>,
) -> Option<CallHierarchyItem> {
let src_loc = SourceLoc::parse(info.src.as_str())?;
let file_path_str = id_to_path_map.get(&src_loc.file_id_str())?;
let file_path = if std::path::Path::new(file_path_str).is_absolute() {
std::path::PathBuf::from(file_path_str)
} else {
std::env::current_dir().ok()?.join(file_path_str)
};
let source_bytes = std::fs::read(&file_path).ok()?;
let uri = Url::from_file_path(&file_path).ok()?;
let start = bytes_to_pos(&source_bytes, src_loc.offset)?;
let end = bytes_to_pos(&source_bytes, src_loc.end())?;
let range = Range { start, end };
let selection_range = info
.name_location
.as_deref()
.and_then(|nl| {
let nl_loc = SourceLoc::parse(nl)?;
let ns = bytes_to_pos(&source_bytes, nl_loc.offset)?;
let ne = bytes_to_pos(&source_bytes, nl_loc.end())?;
Some(Range { start: ns, end: ne })
})
.unwrap_or(range);
let node_type = info.node_type.as_deref().unwrap_or("");
let kind = match node_type {
"FunctionDefinition" | "ModifierDefinition" => SymbolKind::FUNCTION,
"ContractDefinition" => SymbolKind::CLASS,
_ => SymbolKind::FUNCTION,
};
let name = extract_name_from_source(&source_bytes, &selection_range)
.unwrap_or_else(|| node_type.to_string());
let data = Some(serde_json::json!({ "nodeId": node_id.0 }));
Some(CallHierarchyItem {
name,
kind,
tags: None,
detail: None,
uri,
range,
selection_range,
data,
})
}
pub fn call_src_to_range(
call_src: &str,
id_to_path_map: &HashMap<SolcFileId, String>,
) -> Option<Range> {
let loc = SourceLoc::parse(call_src)?;
let file_path_str = id_to_path_map.get(&loc.file_id_str())?;
let file_path = if std::path::Path::new(file_path_str).is_absolute() {
std::path::PathBuf::from(file_path_str)
} else {
std::env::current_dir().ok()?.join(file_path_str)
};
let source_bytes = std::fs::read(&file_path).ok()?;
let start = bytes_to_pos(&source_bytes, loc.offset)?;
let end = bytes_to_pos(&source_bytes, loc.end())?;
Some(Range { start, end })
}
pub fn find_node_info(
nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
node_id: NodeId,
) -> Option<&NodeInfo> {
for file_nodes in nodes.values() {
if let Some(info) = file_nodes.get(&node_id) {
return Some(info);
}
}
None
}
fn find_file_path(
abs_path: &str,
id_to_path_map: &HashMap<SolcFileId, String>,
) -> Option<std::path::PathBuf> {
let as_path = std::path::Path::new(abs_path);
if as_path.is_absolute() && as_path.exists() {
return Some(as_path.to_path_buf());
}
for file_path in id_to_path_map.values() {
if file_path == abs_path || file_path.ends_with(abs_path) {
let fp = std::path::Path::new(file_path);
if fp.is_absolute() {
return Some(fp.to_path_buf());
} else {
return std::env::current_dir().ok().map(|cwd| cwd.join(fp));
}
}
}
std::env::current_dir()
.ok()
.map(|cwd| cwd.join(abs_path))
.filter(|p| p.exists())
}
fn name_loc_range(name_location: &Option<String>, source_bytes: &[u8]) -> Option<Range> {
let loc_str = name_location.as_deref()?;
let loc = SourceLoc::parse(loc_str)?;
let start = bytes_to_pos(source_bytes, loc.offset)?;
let end = bytes_to_pos(source_bytes, loc.end())?;
Some(Range { start, end })
}
fn extract_name_from_source(source_bytes: &[u8], range: &Range) -> Option<String> {
let text = String::from_utf8_lossy(source_bytes);
let lines: Vec<&str> = text.lines().collect();
let line = lines.get(range.start.line as usize)?;
let start = range.start.character as usize;
let end = range.end.character as usize;
if range.start.line == range.end.line && start < line.len() && end <= line.len() {
Some(line[start..end].to_string())
} else {
None
}
}
pub fn resolve_callable_at_position(
build: &CachedBuild,
abs_path: &str,
byte_position: usize,
) -> Option<NodeId> {
let node_id = byte_to_id(&build.nodes, abs_path, byte_position)?;
let file_nodes = build.nodes.get(abs_path)?;
let info = file_nodes.get(&node_id)?;
let node_type = info.node_type.as_deref().unwrap_or("");
if matches!(
node_type,
"FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
) {
return Some(node_id);
}
if let Some(ref_id) = info.referenced_declaration {
if let Some(decl) = build.decl_index.get(&ref_id)
&& matches!(
decl,
DeclNode::FunctionDefinition(_)
| DeclNode::ModifierDefinition(_)
| DeclNode::ContractDefinition(_)
)
{
return Some(ref_id);
}
if let Some(ref_info) = find_node_info(&build.nodes, ref_id) {
let ref_type = ref_info.node_type.as_deref().unwrap_or("");
if matches!(
ref_type,
"FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
) {
return Some(ref_id);
}
}
}
let mut best_callable: Option<(NodeId, usize)> = None;
for (id, ni) in file_nodes {
let nt = ni.node_type.as_deref().unwrap_or("");
if !matches!(
nt,
"FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
) {
continue;
}
if let Some(src_loc) = SourceLoc::parse(ni.src.as_str())
&& src_loc.offset <= byte_position
&& byte_position < src_loc.end()
{
match best_callable {
None => best_callable = Some((*id, src_loc.length)),
Some((_, best_len)) if src_loc.length < best_len => {
best_callable = Some((*id, src_loc.length));
}
_ => {}
}
}
}
best_callable.map(|(id, _)| id)
}