solidity_language_server/
call_hierarchy.rs1use std::collections::HashMap;
27use tower_lsp::lsp_types::{CallHierarchyItem, Range, SymbolKind, Url};
28
29use crate::goto::{CachedBuild, NodeInfo, bytes_to_pos};
30use crate::references::byte_to_id;
31use crate::solc_ast::DeclNode;
32use crate::types::{AbsPath, NodeId, SolcFileId, SourceLoc};
33
34pub fn verify_node_identity(
55 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
56 node_id: NodeId,
57 expected_abs_path: &str,
58 expected_name_offset: usize,
59 expected_name: &str,
60) -> bool {
61 let Some(file_nodes) = nodes.get(expected_abs_path) else {
62 return false;
63 };
64 let Some(info) = file_nodes.get(&node_id) else {
65 return false;
66 };
67 let name_offset_matches = info
69 .name_location
70 .as_deref()
71 .and_then(SourceLoc::parse)
72 .is_some_and(|loc| loc.offset == expected_name_offset);
73 if !name_offset_matches {
74 return false;
75 }
76 if let Some(nl) = info.name_location.as_deref() {
85 if let Some(loc) = SourceLoc::parse(nl) {
86 let name_matches = std::path::Path::new(expected_abs_path)
90 .exists()
91 .then(|| std::fs::read(expected_abs_path).ok())
92 .flatten()
93 .is_some_and(|source_bytes| {
94 source_bytes
95 .get(loc.offset..loc.end())
96 .is_some_and(|slice| slice == expected_name.as_bytes())
97 });
98 return name_matches;
99 }
100 }
101 false
102}
103
104pub fn resolve_target_in_build(
120 build: &CachedBuild,
121 node_id: NodeId,
122 target_abs: &str,
123 target_name: &str,
124 target_name_offset: usize,
125) -> Vec<NodeId> {
126 let mut ids = Vec::new();
127
128 if verify_node_identity(
130 &build.nodes,
131 node_id,
132 target_abs,
133 target_name_offset,
134 target_name,
135 ) {
136 ids.push(node_id);
137 return ids;
138 }
139
140 if let Some(resolved_id) = byte_to_id(&build.nodes, target_abs, target_name_offset) {
143 if let Some(file_nodes) = build.nodes.get(target_abs) {
145 if let Some(info) = file_nodes.get(&resolved_id) {
146 let nt = info.node_type.as_deref().unwrap_or("");
147 if matches!(
148 nt,
149 "FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
150 ) {
151 ids.push(resolved_id);
152 }
153 }
154 }
155 }
156
157 ids
158}
159
160pub fn incoming_calls(
172 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
173 target_ids: &[NodeId],
174) -> Vec<(NodeId, String)> {
175 let mut results = Vec::new();
176
177 for (_abs_path, file_nodes) in nodes {
178 let callables: Vec<(NodeId, &NodeInfo)> = file_nodes
180 .iter()
181 .filter(|(_id, info)| {
182 info.node_type
183 .as_deref()
184 .is_some_and(|nt| nt == "FunctionDefinition" || nt == "ModifierDefinition")
185 })
186 .map(|(id, info)| (*id, info))
187 .collect();
188
189 for (ref_id, ref_info) in file_nodes {
190 let Some(ref_decl) = ref_info.referenced_declaration else {
191 continue;
192 };
193 if !target_ids.contains(&ref_decl) {
194 continue;
195 }
196 if target_ids.contains(ref_id) {
198 continue;
199 }
200
201 let ref_src = match SourceLoc::parse(ref_info.src.as_str()) {
203 Some(s) => s,
204 None => continue,
205 };
206
207 let mut best_callable: Option<(NodeId, usize)> = None;
208 for &(callable_id, callable_info) in &callables {
209 let Some(callable_src) = SourceLoc::parse(callable_info.src.as_str()) else {
210 continue;
211 };
212 if callable_src.file_id != ref_src.file_id {
213 continue;
214 }
215 if callable_src.offset <= ref_src.offset && ref_src.end() <= callable_src.end() {
216 let span = callable_src.length;
217 if best_callable.is_none() || span < best_callable.unwrap().1 {
218 best_callable = Some((callable_id, span));
219 }
220 }
221 }
222
223 if let Some((caller_id, _)) = best_callable {
224 let call_src = ref_info
228 .member_location
229 .as_deref()
230 .unwrap_or(ref_info.src.as_str())
231 .to_string();
232 results.push((caller_id, call_src));
233 }
234 }
235 }
236
237 results.sort_by(|a, b| a.0.0.cmp(&b.0.0).then_with(|| a.1.cmp(&b.1)));
238 results.dedup();
239 results
240}
241
242pub fn outgoing_calls(
248 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
249 caller_id: NodeId,
250) -> Vec<(NodeId, String)> {
251 let caller_info = match find_node_info(nodes, caller_id) {
252 Some(info) => info,
253 None => return vec![],
254 };
255 let caller_src = match SourceLoc::parse(caller_info.src.as_str()) {
256 Some(s) => s,
257 None => return vec![],
258 };
259
260 let callable_ids: std::collections::HashSet<NodeId> = nodes
262 .values()
263 .flat_map(|file_nodes| {
264 file_nodes.iter().filter_map(|(id, info)| {
265 info.node_type.as_deref().and_then(|nt| {
266 if nt == "FunctionDefinition" || nt == "ModifierDefinition" {
267 Some(*id)
268 } else {
269 None
270 }
271 })
272 })
273 })
274 .collect();
275
276 let mut results = Vec::new();
277
278 for (_abs_path, file_nodes) in nodes {
279 for (_ref_id, ref_info) in file_nodes {
280 let Some(ref_decl) = ref_info.referenced_declaration else {
281 continue;
282 };
283 if !callable_ids.contains(&ref_decl) {
284 continue;
285 }
286 let Some(ref_src) = SourceLoc::parse(ref_info.src.as_str()) else {
287 continue;
288 };
289 if ref_src.file_id != caller_src.file_id {
290 continue;
291 }
292 if caller_src.offset <= ref_src.offset && ref_src.end() <= caller_src.end() {
293 if ref_decl == caller_id {
294 continue;
295 }
296 let call_src = ref_info
300 .member_location
301 .as_deref()
302 .unwrap_or(ref_info.src.as_str())
303 .to_string();
304 results.push((ref_decl, call_src));
305 }
306 }
307 }
308
309 results.sort_by(|a, b| a.0.0.cmp(&b.0.0).then_with(|| a.1.cmp(&b.1)));
310 results.dedup();
311 results
312}
313
314pub fn decl_to_hierarchy_item(
326 decl: &DeclNode,
327 node_id: NodeId,
328 node_id_to_source_path: &HashMap<NodeId, AbsPath>,
329 id_to_path_map: &HashMap<SolcFileId, String>,
330 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
331) -> Option<CallHierarchyItem> {
332 let abs_path = node_id_to_source_path.get(&node_id)?;
333
334 let file_path = find_file_path(abs_path.as_str(), id_to_path_map)?;
335 let source_bytes = std::fs::read(&file_path).ok()?;
336 let uri = Url::from_file_path(&file_path).ok()?;
337
338 let src_loc = SourceLoc::parse(decl.src())?;
339 let start = bytes_to_pos(&source_bytes, src_loc.offset)?;
340 let end = bytes_to_pos(&source_bytes, src_loc.end())?;
341 let range = Range { start, end };
342
343 let selection_range = find_node_info(nodes, node_id)
344 .and_then(|info| {
345 name_loc_range(
346 &info.name_location.as_ref().map(|s| s.to_string()),
347 &source_bytes,
348 )
349 })
350 .unwrap_or(range);
351
352 let symbol_kind = match decl {
353 DeclNode::FunctionDefinition(_) => SymbolKind::FUNCTION,
354 DeclNode::ModifierDefinition(_) => SymbolKind::FUNCTION,
355 DeclNode::ContractDefinition(c) => match c.contract_kind {
356 crate::solc_ast::ContractKind::Interface => SymbolKind::INTERFACE,
357 _ => SymbolKind::CLASS,
358 },
359 _ => SymbolKind::FUNCTION,
360 };
361
362 let data = Some(serde_json::json!({ "nodeId": node_id.0 }));
363
364 Some(CallHierarchyItem {
365 name: decl.name().to_string(),
366 kind: symbol_kind,
367 tags: None,
368 detail: decl.build_signature(),
369 uri,
370 range,
371 selection_range,
372 data,
373 })
374}
375
376pub fn node_info_to_hierarchy_item(
381 node_id: NodeId,
382 info: &NodeInfo,
383 id_to_path_map: &HashMap<SolcFileId, String>,
384) -> Option<CallHierarchyItem> {
385 let src_loc = SourceLoc::parse(info.src.as_str())?;
386 let file_path_str = id_to_path_map.get(&src_loc.file_id_str())?;
387
388 let file_path = if std::path::Path::new(file_path_str).is_absolute() {
389 std::path::PathBuf::from(file_path_str)
390 } else {
391 std::env::current_dir().ok()?.join(file_path_str)
392 };
393
394 let source_bytes = std::fs::read(&file_path).ok()?;
395 let uri = Url::from_file_path(&file_path).ok()?;
396
397 let start = bytes_to_pos(&source_bytes, src_loc.offset)?;
398 let end = bytes_to_pos(&source_bytes, src_loc.end())?;
399 let range = Range { start, end };
400
401 let selection_range = info
402 .name_location
403 .as_deref()
404 .and_then(|nl| {
405 let nl_loc = SourceLoc::parse(nl)?;
406 let ns = bytes_to_pos(&source_bytes, nl_loc.offset)?;
407 let ne = bytes_to_pos(&source_bytes, nl_loc.end())?;
408 Some(Range { start: ns, end: ne })
409 })
410 .unwrap_or(range);
411
412 let node_type = info.node_type.as_deref().unwrap_or("");
413 let kind = match node_type {
414 "FunctionDefinition" | "ModifierDefinition" => SymbolKind::FUNCTION,
415 "ContractDefinition" => SymbolKind::CLASS,
416 _ => SymbolKind::FUNCTION,
417 };
418
419 let name = extract_name_from_source(&source_bytes, &selection_range)
420 .unwrap_or_else(|| node_type.to_string());
421
422 let data = Some(serde_json::json!({ "nodeId": node_id.0 }));
423
424 Some(CallHierarchyItem {
425 name,
426 kind,
427 tags: None,
428 detail: None,
429 uri,
430 range,
431 selection_range,
432 data,
433 })
434}
435
436pub fn call_src_to_range(
438 call_src: &str,
439 id_to_path_map: &HashMap<SolcFileId, String>,
440) -> Option<Range> {
441 let loc = SourceLoc::parse(call_src)?;
442 let file_path_str = id_to_path_map.get(&loc.file_id_str())?;
443
444 let file_path = if std::path::Path::new(file_path_str).is_absolute() {
445 std::path::PathBuf::from(file_path_str)
446 } else {
447 std::env::current_dir().ok()?.join(file_path_str)
448 };
449
450 let source_bytes = std::fs::read(&file_path).ok()?;
451 let start = bytes_to_pos(&source_bytes, loc.offset)?;
452 let end = bytes_to_pos(&source_bytes, loc.end())?;
453 Some(Range { start, end })
454}
455
456pub fn find_node_info<'a>(
460 nodes: &'a HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
461 node_id: NodeId,
462) -> Option<&'a NodeInfo> {
463 for file_nodes in nodes.values() {
464 if let Some(info) = file_nodes.get(&node_id) {
465 return Some(info);
466 }
467 }
468 None
469}
470
471fn find_file_path(
473 abs_path: &str,
474 id_to_path_map: &HashMap<SolcFileId, String>,
475) -> Option<std::path::PathBuf> {
476 let as_path = std::path::Path::new(abs_path);
477 if as_path.is_absolute() && as_path.exists() {
478 return Some(as_path.to_path_buf());
479 }
480
481 for file_path in id_to_path_map.values() {
482 if file_path == abs_path || file_path.ends_with(abs_path) {
483 let fp = std::path::Path::new(file_path);
484 if fp.is_absolute() {
485 return Some(fp.to_path_buf());
486 } else {
487 return std::env::current_dir().ok().map(|cwd| cwd.join(fp));
488 }
489 }
490 }
491
492 std::env::current_dir()
493 .ok()
494 .map(|cwd| cwd.join(abs_path))
495 .filter(|p| p.exists())
496}
497
498fn name_loc_range(name_location: &Option<String>, source_bytes: &[u8]) -> Option<Range> {
500 let loc_str = name_location.as_deref()?;
501 let loc = SourceLoc::parse(loc_str)?;
502 let start = bytes_to_pos(source_bytes, loc.offset)?;
503 let end = bytes_to_pos(source_bytes, loc.end())?;
504 Some(Range { start, end })
505}
506
507fn extract_name_from_source(source_bytes: &[u8], range: &Range) -> Option<String> {
509 let text = String::from_utf8_lossy(source_bytes);
510 let lines: Vec<&str> = text.lines().collect();
511 let line = lines.get(range.start.line as usize)?;
512 let start = range.start.character as usize;
513 let end = range.end.character as usize;
514 if range.start.line == range.end.line && start < line.len() && end <= line.len() {
515 Some(line[start..end].to_string())
516 } else {
517 None
518 }
519}
520
521pub fn resolve_callable_at_position(
529 build: &CachedBuild,
530 abs_path: &str,
531 byte_position: usize,
532) -> Option<NodeId> {
533 let node_id = byte_to_id(&build.nodes, abs_path, byte_position)?;
534 let file_nodes = build.nodes.get(abs_path)?;
535 let info = file_nodes.get(&node_id)?;
536 let node_type = info.node_type.as_deref().unwrap_or("");
537
538 if matches!(
540 node_type,
541 "FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
542 ) {
543 return Some(node_id);
544 }
545
546 if let Some(ref_id) = info.referenced_declaration {
548 if let Some(decl) = build.decl_index.get(&ref_id) {
549 if matches!(
550 decl,
551 DeclNode::FunctionDefinition(_)
552 | DeclNode::ModifierDefinition(_)
553 | DeclNode::ContractDefinition(_)
554 ) {
555 return Some(ref_id);
556 }
557 }
558 if let Some(ref_info) = find_node_info(&build.nodes, ref_id) {
559 let ref_type = ref_info.node_type.as_deref().unwrap_or("");
560 if matches!(
561 ref_type,
562 "FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
563 ) {
564 return Some(ref_id);
565 }
566 }
567 }
568
569 let mut best_callable: Option<(NodeId, usize)> = None;
571 for (id, ni) in file_nodes {
572 let nt = ni.node_type.as_deref().unwrap_or("");
573 if !matches!(
574 nt,
575 "FunctionDefinition" | "ModifierDefinition" | "ContractDefinition"
576 ) {
577 continue;
578 }
579 if let Some(src_loc) = SourceLoc::parse(ni.src.as_str()) {
580 if src_loc.offset <= byte_position && byte_position < src_loc.end() {
581 match best_callable {
582 None => best_callable = Some((*id, src_loc.length)),
583 Some((_, best_len)) if src_loc.length < best_len => {
584 best_callable = Some((*id, src_loc.length));
585 }
586 _ => {}
587 }
588 }
589 }
590 }
591
592 best_callable.map(|(id, _)| id)
593}