1use crate::builtin;
4use crate::cg::{
5 CallGraph, CallGraphGeneratorContext, CallGraphGeneratorInput, NodeInfo, NodeType,
6};
7use crate::parser::get_node_text;
8use std::collections::VecDeque;
9use streaming_iterator::StreamingIterator;
10use tree_sitter::{Node as TsNode, Query, QueryCursor};
11use tracing::{trace, error};
12
13#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
14pub enum TypeError {
15 #[error("Query error: {0}")]
16 QueryError(String),
17
18 #[error("Missing child: {0}")]
19 MissingChild(String),
20
21 #[error("Failed to resolve type for expression '{expr}': {reason}")]
22 TypeResolutionFailed { expr: String, reason: String },
23
24 #[error("Failed to resolve target '{name}': {reason}")]
25 TargetResolutionFailed { name: String, reason: String },
26
27 #[error("Unsupported node kind: {0}")]
28 UnsupportedNodeKind(String),
29
30 #[error("Ambiguous implementation for interface {interface_name}.{method_name}, found implementations: {implementations:?}")]
31 AmbiguousInterfaceImplementation {
32 interface_name: String,
33 method_name: String,
34 implementations: Vec<String>,
35 },
36
37 #[error("Internal error: {0}")]
38 Internal(String),
39}
40
41impl From<tree_sitter::QueryError> for TypeError {
42 fn from(error: tree_sitter::QueryError) -> Self {
43 TypeError::QueryError(error.to_string())
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
53pub enum ResolvedTarget {
54 Function {
56 contract_name: Option<String>, function_name: String,
58 node_type: NodeType, },
60 InterfaceMethod {
62 interface_name: String,
63 method_name: String,
64 implementation: Option<Box<ResolvedTarget>>, },
67 BuiltIn { object_type: String, name: String },
69 NotCallable { reason: String },
71 External { address_expr: String },
73 TypeCast { type_name: String },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
80pub struct ResolvedCallStep {
81 pub call_expr_span: (usize, usize),
83 pub function_span: (usize, usize),
85 pub target: ResolvedTarget,
87 pub arguments: Vec<String>,
89 pub result_type: Option<String>,
93 pub object_type: Option<String>,
96 pub object_instance_text: Option<String>,
100 pub originating_span_start: usize,
103 pub base_object_identifier_for_builtin: Option<String>,
106}
107
108pub(crate) fn analyze_chained_call<'a>(
128 start_node: TsNode<'a>,
129 caller_node_id: usize,
130 caller_contract_name_opt: &'a Option<String>,
131 ctx: &CallGraphGeneratorContext,
132 graph: &'a CallGraph, source: &'a str,
134 solidity_lang: &'a tree_sitter::Language,
135 input: &'a CallGraphGeneratorInput,
136 original_start_node_for_err_reporting: Option<TsNode<'a>>, originating_span_start: usize, ) -> std::result::Result<Vec<ResolvedCallStep>, TypeError> {
139 let original_start_node_for_err_reporting =
141 original_start_node_for_err_reporting.unwrap_or(start_node); let mut steps = Vec::new();
144
145 trace!(
146 node_kind = start_node.kind(),
147 node_text = get_node_text(&start_node, source).trim(),
148 "Starting chained analysis"
149 );
150
151 match start_node.kind() {
152 "identifier" | "primitive_type" | "string_literal" | "number_literal"
154 | "boolean_literal" | "hex_literal" | "address_literal" => {
155 let _resolved_type = resolve_expression_type_v2(
159 start_node,
160 caller_node_id,
161 caller_contract_name_opt,
162 ctx,
163 graph,
164 source,
165 solidity_lang,
166 input,
167 )?;
168 trace!(
169 node_kind = start_node.kind(),
170 ?_resolved_type,
171 "Base case node processed"
172 );
173 }
176
177 "new_expression" => {
178 let type_name_node = start_node.named_child(0).ok_or_else(|| {
180 TypeError::MissingChild(format!(
181 "new_expression missing type_name (named_child(0) failed for node: {})",
182 get_node_text(&start_node, source)
183 ))
184 })?;
185 let contract_name = get_node_text(&type_name_node, source).to_string(); let target = ResolvedTarget::Function {
189 contract_name: Some(contract_name.clone()),
190 function_name: contract_name.clone(), node_type: NodeType::Constructor,
192 };
193
194 let arguments = if let Some(parent) = start_node.parent() {
200 if parent.kind() == "call_expression" {
201 extract_arguments_v2(parent, source)
202 } else {
203 vec![] }
205 } else {
206 vec![]
207 };
208
209 let step = ResolvedCallStep {
210 call_expr_span: (start_node.start_byte(), start_node.end_byte()), function_span: (
212 type_name_node.start_byte(), type_name_node.end_byte(), ), target,
216 arguments,
217 result_type: Some(contract_name.clone()), object_type: None, object_instance_text: None, originating_span_start, base_object_identifier_for_builtin: None, };
223 steps.push(step);
224 trace!(
225 result_type = ?steps.last().and_then(|s| s.result_type.as_ref()),
226 "New expression step added"
227 );
228 }
229
230 "call_expression" => {
232 let function_node = start_node .child_by_field_name("function")
234 .ok_or_else(|| {
235 TypeError::MissingChild("call_expression missing function".to_string())
236 })?;
237 let argument_nodes = find_argument_expression_nodes(start_node); trace!(
241 "Handling call_expression. Function node kind: '{}', text: '{}', Num args: {}",
242 function_node.kind(),
243 get_node_text(&function_node, source).trim(),
244 argument_nodes.len()
245 );
246
247 let mut argument_steps = Vec::new();
249 let mut argument_texts = Vec::with_capacity(argument_nodes.len());
250 for arg_node in argument_nodes {
251 trace!(
252 " Analyzing argument node kind: '{}', text: '{}'",
253 arg_node.kind(),
254 get_node_text(&arg_node, source).trim()
255 );
256 let inner_steps = analyze_chained_call(
258 arg_node,
259 caller_node_id,
260 caller_contract_name_opt,
261 ctx,
262 graph,
263 source,
264 solidity_lang,
265 input,
266 Some(original_start_node_for_err_reporting),
267 originating_span_start, )?;
269
270 argument_steps.extend(inner_steps);
271 argument_texts.push(get_node_text(&arg_node, source).to_string());
273 }
274 trace!(
275 " Finished analyzing arguments. Collected {} inner steps.",
276 argument_texts.len()
277 );
278
279 match function_node.kind() {
284 "expression"
286 if function_node
287 .child(0)
288 .map_or(false, |n| n.kind() == "identifier") =>
289 {
290 let id_node = function_node.child(0).unwrap();
291 let name = get_node_text(&id_node, source).to_string();
292
293 if name.chars().next().map_or(false, |c| c.is_uppercase())
295 && (ctx.all_contracts.contains_key(&name)
296 || ctx.all_interfaces.contains_key(&name)
297 || ctx.all_libraries.contains_key(&name))
298 {
300 trace!(
301 " Outer call is Type Cast/Constructor '{}'. No outer step generated.", name
302 );
303 } else {
317 trace!(
319 " Outer call is Simple Function Call '{}'",
320 name
321 );
322 let target =
323 resolve_simple_call_v2(&name, caller_contract_name_opt, graph, ctx)?;
324 let result_type = resolve_call_return_type(
325 &target,
326 ctx,
327 graph,
328 source,
329 solidity_lang,
330 input,
331 )?;
332
333 let outer_step = ResolvedCallStep {
334 call_expr_span: (start_node.start_byte(), start_node.end_byte()),
335 function_span: (id_node.start_byte(), id_node.end_byte()),
336 target,
337 arguments: argument_texts, result_type: result_type.clone(),
339 object_type: None, object_instance_text: None, originating_span_start, base_object_identifier_for_builtin: None, };
344 steps = argument_steps; steps.push(outer_step); trace!(
348 " Simple call step added. Result type: {:?}. Total steps for this branch: {}",
349 result_type, steps.len()
350 );
351 }
352 }
353
354 "expression"
356 if function_node
357 .child(0)
358 .map_or(false, |n| n.kind() == "member_expression") =>
359 {
360 let member_expr_node = function_node.child(0).unwrap();
361 let object_node =
362 member_expr_node
363 .child_by_field_name("object")
364 .ok_or_else(|| {
365 TypeError::MissingChild(
366 "member_expression missing object".to_string(),
367 )
368 })?;
369 let property_node = member_expr_node
370 .child_by_field_name("property")
371 .ok_or_else(|| {
372 TypeError::MissingChild(
373 "member_expression missing property".to_string(),
374 )
375 })?;
376
377 if property_node.kind() != "identifier" {
378 return Err(TypeError::UnsupportedNodeKind(format!(
379 "Member expression property kind '{}' not supported",
380 property_node.kind()
381 )));
382 }
383 let property_name = get_node_text(&property_node, source).to_string();
384 trace!(
385 " Outer call is Member Call '.{}'",
386 property_name
387 );
388 let object_steps = analyze_chained_call(
390 object_node,
392 caller_node_id,
393 caller_contract_name_opt,
394 ctx,
395 graph,
396 source,
397 solidity_lang,
398 input,
399 Some(original_start_node_for_err_reporting),
400 originating_span_start, )?;
402 let base_identifier_name = if object_node.kind() == "identifier" {
404 Some(get_node_text(&object_node, source).to_string())
405 } else {
406 None
409 };
410
411 let outer_object_type = if let Some(last_object_step) = object_steps.last() {
413 last_object_step.result_type.clone()
414 } else {
415 resolve_expression_type_v2(
417 object_node,
418 caller_node_id,
419 caller_contract_name_opt,
420 ctx,
421 graph,
422 source,
423 solidity_lang,
424 input,
425 )?
426 };
427 trace!(
428 " Resolved outer_object_type for member call: {:?}",
429 outer_object_type
430 );
431
432 if let Some(ref obj_type) = outer_object_type {
433 trace!(
434 " Object type for '.{}' call resolved to: '{}'",
435 property_name, obj_type
436 );
437 trace!(" Calling resolve_member_or_library_call_v2 with obj_type='{}', property='{}'", obj_type, property_name);
438 let target = resolve_member_or_library_call_v2(
439 obj_type,
440 &property_name,
441 caller_contract_name_opt,
442 graph,
443 ctx,
444 source,
445 (member_expr_node.start_byte(), member_expr_node.end_byte()),
446 )?;
447 let result_type = resolve_call_return_type(
448 &target,
449 ctx,
450 graph,
451 source,
452 solidity_lang,
453 input,
454 )?;
455
456 let current_object_instance_text =
457 Some(get_node_text(&object_node, source).to_string());
458
459 let outer_step = ResolvedCallStep {
460 call_expr_span: (start_node.start_byte(), start_node.end_byte()),
461 function_span: (property_node.start_byte(), property_node.end_byte()),
462 target: target.clone(), arguments: argument_texts, result_type: result_type.clone(),
465 object_type: outer_object_type.clone(),
466 object_instance_text: current_object_instance_text, originating_span_start, base_object_identifier_for_builtin: if matches!(
469 target,
470 ResolvedTarget::BuiltIn { .. }
471 ) {
472 base_identifier_name } else {
474 None
475 },
476 };
477 steps = object_steps; steps.extend(argument_steps); steps.push(outer_step); trace!(
482 " Member call step added. Result type: {:?}. Total steps for this branch: {}",
483 result_type, steps.len()
484 );
485 } else {
486 trace!(" Failed to resolve object type for member call '.{}'", property_name);
487 return Err(TypeError::TypeResolutionFailed {
488 expr: get_node_text(&object_node, source).to_string(),
489 reason: "Could not determine type for member call.".to_string(),
490 });
491 }
492 }
493
494 "expression"
499 if function_node
500 .child(0)
501 .map_or(false, |n| n.kind() == "new_expression") =>
502 {
503 let _object_steps: Vec<ResolvedCallStep> = Vec::new(); let _outer_object_type: Option<String>; let new_expression_node = function_node.child(0).unwrap();
508 let mut new_constructor_steps = analyze_chained_call(
509 new_expression_node, caller_node_id,
511 caller_contract_name_opt,
512 ctx,
513 graph,
514 source,
515 solidity_lang,
516 input,
517 Some(original_start_node_for_err_reporting),
518 originating_span_start,
519 )?;
520
521 if let Some(constructor_step) = new_constructor_steps.get_mut(0) {
524 constructor_step.arguments = argument_texts.clone();
526 }
527
528 steps = argument_steps; steps.extend(new_constructor_steps); trace!(
532 " 'new' expression wrapped in call processed. Total steps for this branch: {}",
533 steps.len()
534 );
535 }
536 _ => {
537 return Err(TypeError::UnsupportedNodeKind(format!(
538 "Unsupported function node kind in call_expression: '{}'",
539 function_node.kind()
540 )));
541 }
542 }
543 }
544
545 "member_expression" => {
546 let object_node = start_node .child_by_field_name("object")
550 .ok_or_else(|| {
551 TypeError::MissingChild("member_expression missing object".to_string())
552 })?;
553 let property_node = start_node .child_by_field_name("property")
555 .ok_or_else(|| {
556 TypeError::MissingChild("member_expression missing property".to_string())
557 })?;
558
559 if property_node.kind() != "identifier" {
560 return Err(TypeError::UnsupportedNodeKind(format!(
561 "Member expression property kind '{}' not supported",
562 property_node.kind()
563 )));
564 }
565 let property_name = get_node_text(&property_node, source).to_string();
566
567 trace!(
568 "Handling member_expression '.{}' (not a call)",
569 property_name
570 );
571
572 let object_steps = analyze_chained_call(
574 object_node,
575 caller_node_id,
576 caller_contract_name_opt,
577 ctx,
578 graph,
579 source,
580 solidity_lang,
581 input,
582 Some(original_start_node_for_err_reporting), originating_span_start, )?;
585 steps.extend(object_steps); trace!(
591 " Member access processed. {} inner steps added.",
592 steps.len()
593 );
594 }
595
596 "expression" => {
597 if let Some(child_node) = start_node.child(0) {
599 trace!(
601 "Delegating 'expression' analysis to child '{}'.",
602 child_node.kind()
603 );
604 return analyze_chained_call(
607 child_node,
609 caller_node_id,
610 caller_contract_name_opt,
611 ctx,
612 graph,
613 source,
614 solidity_lang,
615 input,
616 Some(original_start_node_for_err_reporting),
617 originating_span_start, );
619 } else {
620 trace!("'expression' node has no children.");
621 }
623 }
624 "binary_expression" => {
625 let left_node = start_node.child_by_field_name("left").ok_or_else(|| {
627 TypeError::MissingChild("binary_expression missing left operand".to_string())
628 })?;
629 let right_node = start_node.child_by_field_name("right").ok_or_else(|| {
630 TypeError::MissingChild("binary_expression missing right operand".to_string())
631 })?;
632
633 trace!(
634 " Binary expression: Analyzing left operand kind: '{}'",
635 left_node.kind()
636 );
637 let left_steps = analyze_chained_call(
638 left_node,
639 caller_node_id,
640 caller_contract_name_opt,
641 ctx,
642 graph,
643 source,
644 solidity_lang,
645 input,
646 Some(original_start_node_for_err_reporting), originating_span_start, )?;
649 steps.extend(left_steps); trace!(
652 " Binary expression: Analyzing right operand kind: '{}'",
653 right_node.kind()
654 );
655 let right_steps = analyze_chained_call(
656 right_node,
657 caller_node_id,
658 caller_contract_name_opt,
659 ctx,
660 graph,
661 source,
662 solidity_lang,
663 input,
664 Some(original_start_node_for_err_reporting), originating_span_start, )?;
667 steps.extend(right_steps); trace!(
670 " Binary expression processed. Total steps for this branch now: {}",
671 steps.len()
672 );
673 }
675 _ => {
677 let _resolved_type = resolve_expression_type_v2(
681 start_node, caller_node_id,
684 caller_contract_name_opt,
685 ctx,
686 graph,
687 source,
688 solidity_lang,
689 input,
690 )?;
691 if !matches!(
693 start_node.kind(),
694 "type_cast_expression"
695 | "binary_expression"
696 | "unary_expression"
697 | "parenthesized_expression"
698 ) {
699 trace!(
700 "Potentially unhandled start node kind '{}', resolved type: {:?}. No steps generated.",
701 start_node.kind(), _resolved_type
703 );
704 } else {
705 trace!(
706 "Handled base node kind '{}', resolved type: {:?}. No steps generated.",
707 start_node.kind(), _resolved_type
709 );
710 }
711 }
712 }
713 trace!(
719 "Analysis finished. Total steps generated: {}",
720 steps.len()
721 );
722 Ok(steps)
723}
724
725fn find_argument_expression_nodes<'a>(call_expr_node: TsNode<'a>) -> Vec<TsNode<'a>> {
729 let mut arg_nodes = Vec::new();
730 let mut cursor = call_expr_node.walk();
731 for child in call_expr_node.children(&mut cursor) {
732 if child.kind() == "call_argument" {
734 if let Some(expr_node) = child.child(0) {
735 arg_nodes.push(expr_node);
737 }
738 }
739 else if child.kind() == "arguments" {
741 let mut arg_cursor = child.walk();
742 for arg_child in child.children(&mut arg_cursor) {
743 if arg_child.kind() == "call_argument" {
744 if let Some(expr_node) = arg_child.child(0) {
745 arg_nodes.push(expr_node);
746 }
747 }
748 }
749 }
750 }
751 arg_nodes
752}
753
754fn resolve_expression_type_v2<'a>(
757 expr_node: TsNode<'a>,
758 caller_node_id: usize,
759 caller_contract_name_opt: &'a Option<String>,
760 ctx: &CallGraphGeneratorContext,
761 graph: &'a CallGraph,
762 source: &'a str,
763 solidity_lang: &'a tree_sitter::Language,
764 input: &'a CallGraphGeneratorInput, ) -> std::result::Result<Option<String>, TypeError> {
766 let expr_text = get_node_text(&expr_node, source).trim().to_string();
768 trace!(
769 "[Resolve Type V2] Resolving type for node kind: '{}', text: '{}'",
770 expr_node.kind(),
771 expr_text
772 );
773
774 match expr_node.kind() {
775 "identifier" => {
777 let name = expr_text;
778 if name == "Math" {
779 trace!(
780 "[Resolve Type V2] Encountered identifier 'Math'. CallerScope='{:?}'",
781 caller_contract_name_opt
782 );
783 }
784 if let Some(contract_name) = caller_contract_name_opt {
786 if let Some(type_name) = ctx
787 .state_var_types
788 .get(&(contract_name.clone(), name.clone()))
789 {
790 trace!(
791 "[Resolve Type V2] Identifier '{}' resolved to state var type '{}'",
792 name, type_name
793 );
794 return Ok(Some(type_name.clone()));
795 }
796 }
797 if let Some(caller_node_graph_info) = graph.nodes.get(caller_node_id) {
802 let current_func_name = &caller_node_graph_info.name;
803 let current_contract_name = caller_node_graph_info
804 .contract_name
805 .as_deref()
806 .unwrap_or("Global");
807
808 trace!("[Resolve Type V2 - LocalVar] Attempting to resolve identifier '{}' as local variable in function '{}.{}' (Caller Node ID: {}, Expr Node Span: {:?})", name, current_contract_name, current_func_name, caller_node_id, (expr_node.start_byte(), expr_node.end_byte()));
809
810 if let Some((_, caller_node_info_for_span, _)) =
811 ctx .definition_nodes_info
813 .iter()
814 .find(|(id, _, _)| *id == caller_node_id)
815 {
816 if let Some(_definition_ts_node) =
817 input.tree.root_node().descendant_for_byte_range(
818 caller_node_info_for_span.span.0,
819 caller_node_info_for_span.span.1,
820 )
821 {
822 let local_var_query_str = r#"
824 (variable_declaration_statement
825 (variable_declaration
826 type: (_) @local_var_type
827 name: (identifier) @local_var_name
828 )
829 ) @local_var_decl_stmt
830 "#;
831 let local_var_query =
832 match Query::new(&input.solidity_lang, local_var_query_str) {
833 Ok(q) => q,
834 Err(e) => {
835 return Err(TypeError::QueryError(format!(
836 "Failed to create local var query: {}",
837 e
838 )))
839 }
840 };
841
842 let mut enclosing_true_function_ts_node: Option<TsNode> = None;
844 let mut temp_ancestor_node_opt = Some(expr_node);
845 while let Some(temp_ancestor_node) = temp_ancestor_node_opt {
846 match temp_ancestor_node.kind() {
847 "function_definition"
848 | "modifier_definition"
849 | "constructor_definition" => {
850 enclosing_true_function_ts_node = Some(temp_ancestor_node);
851 break;
852 }
853 _ => temp_ancestor_node_opt = temp_ancestor_node.parent(),
854 }
855 }
856
857 if let Some(true_function_boundary_node) = enclosing_true_function_ts_node {
858 trace!("[Resolve Type V2 - LocalVar Upward] Boundary for search: True function kind='{}', span=({},{})", true_function_boundary_node.kind(), true_function_boundary_node.start_byte(), true_function_boundary_node.end_byte());
859
860 let mut current_scope_node_opt = expr_node.parent();
861 trace!("[Resolve Type V2 - LocalVar Upward] Starting upward search for identifier '{}' (usage at byte {}), from parent kind: {:?}", name, expr_node.start_byte(), current_scope_node_opt.map(|n| n.kind()));
862
863 while let Some(current_scope_node) = current_scope_node_opt {
864 trace!("[Resolve Type V2 - LocalVar Upward] Searching in scope: kind='{}', id={}, span=({},{})", current_scope_node.kind(), current_scope_node.id(), current_scope_node.start_byte(), current_scope_node.end_byte());
865
866 let mut cursor = QueryCursor::new();
867 let source_bytes = input.source.as_bytes();
868 let mut matches = cursor.matches(
869 &local_var_query,
870 current_scope_node,
871 |n: TsNode| std::iter::once(&source_bytes[n.byte_range()]),
872 );
873
874 while let Some(match_) = matches.next() {
875 let mut var_name_opt: Option<String> = None;
876 let mut type_node_opt: Option<TsNode> = None;
877 let mut decl_stmt_node_opt: Option<TsNode> = None;
878
879 for capture in match_.captures {
880 let capture_name = &local_var_query.capture_names()
881 [capture.index as usize];
882 match *capture_name {
883 "local_var_name" => {
884 var_name_opt = Some(
885 get_node_text(&capture.node, &input.source)
886 .to_string(),
887 )
888 }
889 "local_var_type" => type_node_opt = Some(capture.node),
890 "local_var_decl_stmt" => {
891 decl_stmt_node_opt = Some(capture.node)
892 }
893 _ => {}
894 }
895 }
896
897 if let (
898 Some(var_name_found),
899 Some(type_node_found),
900 Some(decl_stmt_node),
901 ) = (var_name_opt, type_node_opt, decl_stmt_node_opt)
902 {
903 if var_name_found == name
906 && decl_stmt_node.end_byte() <= expr_node.start_byte()
907 {
908 let type_name_found =
909 get_node_text(&type_node_found, &input.source)
910 .to_string();
911 trace!("[Resolve Type V2 - LocalVar Upward] MATCH! In scope '{}' (ID: {}), found decl for '{}' (type '{}') ending at byte {}. Usage at byte {}.", current_scope_node.kind(), current_scope_node.id(), var_name_found, type_name_found, decl_stmt_node.end_byte(), expr_node.start_byte());
912 return Ok(Some(type_name_found));
913 } else if var_name_found == name {
914 trace!("[Resolve Type V2 - LocalVar Upward] Found decl for '{}' in scope '{}' (ID: {}), but usage (byte {}) is not strictly after declaration end (byte {}). Decl span: ({},{}).", name, current_scope_node.kind(), current_scope_node.id(), expr_node.start_byte(), decl_stmt_node.end_byte(), decl_stmt_node.start_byte(), decl_stmt_node.end_byte());
917 }
918 }
921 }
922
923 if current_scope_node.id() == true_function_boundary_node.id() {
924 trace!("[Resolve Type V2 - LocalVar Upward] Reached true function boundary ('{}'). Stopping upward search for '{}'.", true_function_boundary_node.kind(), name);
925 break; }
928
929 current_scope_node_opt = current_scope_node.parent();
930 if current_scope_node_opt.is_none() {
931 trace!("[Resolve Type V2 - LocalVar Upward] Reached tree root. Stopping upward search for '{}'.", name);
932 break;
933 }
934 } } else {
936 trace!("[Resolve Type V2 - LocalVar Upward] Could not determine true function boundary for identifier '{}'. Local variable search might be incomplete.", name);
937 }
938 trace!("[Resolve Type V2 - LocalVar Upward] Finished upward search for '{}'. Not found as local variable in accessible scope.", name);
939 } else {
941 trace!("[Resolve Type V2 - LocalVar] Could not find definition TsNode for Caller Node ID: {}", caller_node_id);
943 }
944 } else {
945 trace!("[Resolve Type V2 - LocalVar] Could not find NodeInfo for Caller Node ID: {} in definition_nodes_info", caller_node_id);
947 }
948 } else {
949 trace!("[Resolve Type V2 - LocalVar] Could not find graph node info for Caller Node ID: {} (e.g. for function name/contract context)", caller_node_id);
951 }
952 let is_contract = ctx.all_contracts.contains_key(&name);
954 let is_library = ctx.all_libraries.contains_key(&name);
955 let is_interface = ctx.all_interfaces.contains_key(&name);
956 trace!("[Resolve Type V2] Identifier '{}': is_contract={}, is_library={}, is_interface={}", name, is_contract, is_library, is_interface); if is_contract || is_library || is_interface {
958 trace!("[Resolve Type V2] Identifier '{}' resolved to type (contract/library/interface) '{}'", name, name);
959 return Ok(Some(name));
960 }
961 if name == "this" {
963 if let Some(contract_name) = caller_contract_name_opt {
964 trace!(
965 "[Resolve Type V2] Identifier 'this' resolved to contract type '{}'",
966 contract_name
967 );
968 return Ok(Some(contract_name.clone()));
969 } else {
970 trace!("[Resolve Type V2] Identifier 'this' used outside contract scope.");
971 return Ok(None); }
973 }
974 match resolve_simple_call_v2(&name, caller_contract_name_opt, graph, ctx) {
981 Ok(ResolvedTarget::Function {
982 contract_name,
983 function_name,
984 ..
985 }) => {
986 trace!("[Resolve Type V2] Identifier '{}' resolved to function '{:?}.{}'. Getting return type.", name, contract_name, function_name);
987 let func_key = (contract_name.clone(), function_name.clone());
988 if let Some(node_id) = graph.node_lookup.get(&func_key) {
989 let def_node_info_opt = ctx
990 .definition_nodes_info
991 .iter()
992 .find(|(id, _, _)| *id == *node_id)
993 .map(|(_, info, _)| info.clone());
994 if let Some(def_node_info) = def_node_info_opt {
995 let return_type = get_function_return_type_v2(&def_node_info, input)?;
997 trace!(
998 "[Resolve Type V2] Function return type: {:?}",
999 return_type
1000 );
1001 return Ok(return_type);
1002 } else {
1003 trace!("[Resolve Type V2] Could not find definition NodeInfo for function ID {}", node_id);
1004 }
1005 } else {
1006 trace!("[Resolve Type V2] Could not find node ID in graph lookup for function key {:?}", func_key);
1007 }
1008 }
1009 Ok(_) | Err(_) => {
1010 trace!(
1013 "[Resolve Type V2] Identifier '{}' did not resolve to a function in scope.",
1014 name
1015 );
1016 }
1017 }
1018
1019 trace!(
1020 "[Resolve Type V2] Identifier '{}' type not resolved (state/type/local/function?).",
1021 name
1022 );
1023 Ok(None) }
1025 "primitive_type" => Ok(Some(expr_text)),
1026 "type_cast_expression" => {
1027 let type_node = expr_node.child(0).ok_or_else(|| {
1029 TypeError::MissingChild("type_cast_expression missing type node".to_string())
1030 })?;
1031 let type_name = get_node_text(&type_node, source).to_string();
1032 trace!(
1033 "[Resolve Type V2] Type cast expression resolves to type '{}'",
1034 type_name
1035 );
1036 Ok(Some(type_name))
1037 }
1038 "string_literal" => Ok(Some("string".to_string())),
1039 "number_literal" => Ok(Some("uint256".to_string())),
1040 "boolean_literal" => Ok(Some("bool".to_string())),
1041 "hex_literal" => Ok(Some("bytes".to_string())),
1042 "address_literal" => Ok(Some("address".to_string())),
1043
1044 "member_expression" => resolve_property_type_from_node(
1046 expr_node,
1047 caller_node_id,
1048 caller_contract_name_opt,
1049 ctx,
1050 graph,
1051 source,
1052 solidity_lang,
1053 input,
1054 ),
1055 "call_expression" => {
1056 trace!(
1059 "[Resolve Type V2] Call expression: Analyzing call to determine return type."
1060 );
1061 let steps = analyze_chained_call(
1065 expr_node, caller_node_id,
1067 caller_contract_name_opt,
1068 ctx,
1069 graph,
1070 source,
1071 solidity_lang,
1072 input,
1073 None, expr_node.start_byte(), )?;
1076
1077 if let Some(last_step) = steps.last() {
1078 trace!(
1079 "[Resolve Type V2] Call expression resolved. Last step result type: {:?}",
1080 last_step.result_type
1081 );
1082 Ok(last_step.result_type.clone())
1083 } else {
1084 trace!("[Resolve Type V2] Call expression analysis yielded no steps. Cannot determine type.");
1085 if let Some(func_node) = expr_node.child_by_field_name("function") {
1086 if func_node.kind() == "expression"
1087 && func_node
1088 .child(0)
1089 .map_or(false, |n| n.kind() == "identifier")
1090 {
1091 let id_node = func_node.child(0).unwrap();
1092 let name = get_node_text(&id_node, source).to_string();
1093 if ctx.all_interfaces.contains_key(&name) {
1094 trace!("[Resolve Type V2] Re-identified as interface type cast to '{}' (no steps generated)", name);
1095 return Ok(Some(name)); }
1097 }
1098 }
1099 Ok(None) }
1101 }
1102 "new_expression" => {
1103 let type_name_node = expr_node.named_child(0).ok_or_else(|| {
1105 TypeError::MissingChild(format!(
1106 "new_expression missing type_name (named_child(0) failed for expr_node: {})",
1107 get_node_text(&expr_node, source)
1108 ))
1109 })?;
1110 let contract_name = get_node_text(&type_name_node, source).to_string(); trace!(
1112 "[Resolve Type V2] New expression resolves to type '{}'",
1113 contract_name
1114 );
1115 Ok(Some(contract_name))
1116 }
1117 "expression" => {
1118 if let Some(child_node) = expr_node.child(0) {
1120 trace!(
1121 "[Resolve Type V2] Delegating 'expression' type resolution to child kind: '{}'",
1122 child_node.kind()
1123 );
1124 resolve_expression_type_v2(
1126 child_node,
1127 caller_node_id,
1128 caller_contract_name_opt,
1129 ctx,
1130 graph,
1131 source,
1132 solidity_lang,
1133 input,
1134 )
1135 } else {
1136 trace!("[Resolve Type V2] 'expression' node has no children.");
1137 Ok(None) }
1139 }
1140 "array_access" => {
1142 let base_node = expr_node.child_by_field_name("base").ok_or_else(|| {
1143 TypeError::MissingChild(format!(
1144 "array_access missing base for node: {}",
1145 get_node_text(&expr_node, source)
1146 ))
1147 })?;
1148 trace!(
1149 "[Resolve Type V2] Array access: base kind '{}', text '{}'",
1150 base_node.kind(),
1151 get_node_text(&base_node, source)
1152 );
1153
1154 let base_type_opt = resolve_expression_type_v2(
1155 base_node,
1156 caller_node_id,
1157 caller_contract_name_opt,
1158 ctx,
1159 graph,
1160 source,
1161 solidity_lang,
1162 input,
1163 )?;
1164
1165 if let Some(ref base_type_of_base_expr_str) = base_type_opt {
1166 trace!(
1168 "[Resolve Type V2] Base type of array_access resolved to: '{}'",
1169 base_type_of_base_expr_str
1170 );
1171
1172 if base_node.kind() == "identifier" {
1174 if let Some(contract_name) = caller_contract_name_opt {
1175 let mapping_var_name = get_node_text(&base_node, source);
1176 if let Some(mapping_info) = ctx
1177 .contract_mappings
1178 .get(&(contract_name.clone(), mapping_var_name.to_string()))
1179 {
1180 trace!(
1181 "[Resolve Type V2] Array access on known mapping '{}.{}'. Resolved value type from MappingInfo: '{}'",
1182 contract_name, mapping_var_name, mapping_info.value_type
1183 );
1184 return Ok(Some(mapping_info.value_type.clone()));
1185 }
1186 }
1187 }
1188
1189 if base_type_of_base_expr_str.starts_with("mapping(") {
1191 match parse_mapping_final_value_type(base_type_of_base_expr_str) {
1192 Ok(final_value_type) => {
1193 trace!(
1194 "[Resolve Type V2] Parsed mapping type string '{}'. Final value type: '{}'",
1195 base_type_of_base_expr_str, final_value_type
1196 );
1197 return Ok(Some(final_value_type));
1198 }
1199 Err(e) => {
1200 trace!(
1201 "[Resolve Type V2] Error parsing mapping type string '{}': {}. Falling back.",
1202 base_type_of_base_expr_str, e
1203 );
1204 }
1207 }
1208 }
1209
1210 if base_type_of_base_expr_str.ends_with("[]") {
1212 let element_type =
1213 base_type_of_base_expr_str[0..base_type_of_base_expr_str.len() - 2].trim();
1214 if !element_type.is_empty() {
1215 trace!(
1216 "[Resolve Type V2] Array element type: '{}'",
1217 element_type
1218 );
1219 return Ok(Some(element_type.to_string()));
1220 }
1221 trace!(
1222 "[Resolve Type V2] Could not parse array type: {}",
1223 base_type_of_base_expr_str );
1225 Ok(None)
1226 } else if base_type_of_base_expr_str == "bytes" {
1227 trace!("[Resolve Type V2] Bytes indexed, returning 'bytes1'");
1230 return Ok(Some("bytes1".to_string()));
1231 }
1232 else {
1234 trace!(
1235 "[Resolve Type V2] Base type '{}' is not a mapping, array, or bytes. Cannot determine indexed type.",
1236 base_type_of_base_expr_str );
1238 Ok(None)
1239 }
1240 } else {
1241 trace!("[Resolve Type V2] Could not resolve base type of array_access.");
1242 Ok(None)
1243 }
1244 }
1245 "binary_expression" => {
1246 let operator_node = expr_node.child_by_field_name("operator").ok_or_else(|| {
1249 TypeError::MissingChild("binary_expression missing operator".to_string())
1250 })?;
1251 let operator_text = get_node_text(&operator_node, source);
1252
1253 let result_type = match &*operator_text {
1254 ">" | "<" | ">=" | "<=" | "==" | "!=" => Some("bool".to_string()),
1256 "&&" | "||" => Some("bool".to_string()),
1257 "+" | "-" | "*" | "/" | "%" | "**" | "&" | "|" | "^" | "<<" | ">>" => {
1258 Some("uint256".to_string())
1260 }
1261 _ => {
1262 trace!(
1263 "[Resolve Type V2] Unhandled binary operator: '{}'",
1264 operator_text
1265 );
1266 None }
1268 };
1269 return Ok(result_type); }
1271 "unary_expression" => {
1272 let operator = expr_node
1274 .child_by_field_name("operator")
1275 .map(|n| get_node_text(&n, source));
1276 let operand_node = expr_node.child_by_field_name("argument").ok_or_else(|| {
1277 TypeError::MissingChild("unary_expression missing argument".to_string())
1278 })?;
1279 match operator.as_deref() {
1280 Some("!") => Ok(Some("bool".to_string())),
1281 Some("-") | Some("+") | Some("++") | Some("--") => Ok(Some("uint256".to_string())), _ => {
1283 trace!(
1284 "[Resolve Type V2] Unhandled unary operator: {:?}",
1285 operator
1286 );
1287 resolve_expression_type_v2(
1289 operand_node,
1290 caller_node_id,
1291 caller_contract_name_opt,
1292 ctx,
1293 graph,
1294 source,
1295 solidity_lang,
1296 input,
1297 )
1298 }
1299 }
1300 }
1301 _ => {
1302 trace!(
1303 "[Resolve Type V2] Unhandled expression kind: {}",
1304 expr_node.kind()
1305 );
1306 Ok(None)
1307 }
1308 }
1309}
1310
1311fn resolve_property_type<'a>(
1313 object_type_name: &str,
1314 property_name: &str,
1315 caller_contract_name_opt: &'a Option<String>, graph: &'a CallGraph,
1317 ctx: &CallGraphGeneratorContext,
1318 _source: &'a str, _solidity_lang: &'a tree_sitter::Language, input: &'a CallGraphGeneratorInput, ) -> std::result::Result<Option<String>, TypeError> {
1322 trace!(
1324 "[Resolve Property Type] Resolving type for property '{}' on object type '{}'",
1325 property_name, object_type_name
1326 );
1327
1328 if !ctx.all_interfaces.contains_key(object_type_name) {
1331 if let Some(type_name) = ctx
1332 .state_var_types
1333 .get(&(object_type_name.to_string(), property_name.to_string()))
1334 {
1335 trace!(
1336 "[Resolve Property Type] Property resolved to state var type '{}'",
1337 type_name
1338 );
1339 return Ok(Some(type_name.clone()));
1340 }
1341 }
1342
1343 let function_key = (
1346 Some(object_type_name.to_string()),
1347 property_name.to_string(),
1348 );
1349 if let Some(target_node_id) = graph.node_lookup.get(&function_key) {
1350 trace!("[Resolve Property Type] Property resolved to function member. Node ID: {}. Getting return type.", target_node_id);
1351 let target_def_node_opt = ctx
1354 .definition_nodes_info
1355 .iter()
1356 .find(|(id, _, _)| *id == *target_node_id)
1357 .map(|(_, n, _)| n.clone());
1358 if let Some(target_def_node) = target_def_node_opt {
1359 let return_type = get_function_return_type_v2(&target_def_node, input)?; trace!(
1361 "[Resolve Property Type] Return type from get_function_return_type_v2: {:?}",
1362 return_type
1363 );
1364 return Ok(return_type);
1365 } else {
1366 trace!("[Resolve Property Type] Could not find definition node for target function ID {}", target_node_id);
1367 return Ok(None); }
1369 }
1370
1371 if let Some(library_target) = find_using_for_target(
1374 object_type_name,
1375 property_name,
1376 caller_contract_name_opt,
1377 graph,
1378 ctx,
1379 )? {
1380 if let ResolvedTarget::Function {
1381 contract_name: Some(lib_name),
1382 function_name,
1383 ..
1384 } = library_target
1385 {
1386 let lib_func_key = (Some(lib_name.clone()), function_name.clone());
1387 if let Some(target_node_id) = graph.node_lookup.get(&lib_func_key) {
1388 trace!("[Resolve Property Type] Property resolved to 'using for' library function '{}.{}'. Node ID: {}. Getting return type.", lib_name, function_name, target_node_id);
1389 let target_def_node_opt = ctx
1390 .definition_nodes_info
1391 .iter()
1392 .find(|(id, _, _)| *id == *target_node_id)
1393 .map(|(_, n, _)| n.clone());
1394 if let Some(target_def_node) = target_def_node_opt {
1395 let return_type = get_function_return_type_v2(&target_def_node, input)?;
1396 trace!("[Resolve Property Type] Return type from get_function_return_type_v2: {:?}", return_type);
1397 return Ok(return_type);
1398 } else {
1399 trace!("[Resolve Property Type] Could not find definition node for target library function ID {}", target_node_id);
1400 return Ok(None);
1401 }
1402 }
1403 }
1404 }
1405
1406 if property_name == "length"
1409 && (object_type_name.ends_with("[]")
1410 || object_type_name == "bytes"
1411 || object_type_name == "string")
1412 {
1413 return Ok(Some("uint256".to_string()));
1414 }
1415 if property_name == "balance" && object_type_name == "address" {
1416 return Ok(Some("uint256".to_string()));
1417 }
1418
1419 trace!(
1420 "[Resolve Property Type] Property '{}' type not resolved within type '{}'.",
1421 property_name, object_type_name
1422 );
1423 Ok(None)
1424}
1425
1426fn resolve_property_type_from_node<'a>(
1428 member_expr_node: TsNode<'a>,
1429 caller_node_id: usize,
1430 caller_contract_name_opt: &'a Option<String>,
1431 ctx: &CallGraphGeneratorContext,
1432 graph: &'a CallGraph,
1433 source: &'a str,
1434 solidity_lang: &'a tree_sitter::Language,
1435 input: &'a CallGraphGeneratorInput, ) -> std::result::Result<Option<String>, TypeError> {
1437 let object_node = member_expr_node
1439 .child_by_field_name("object")
1440 .ok_or_else(|| TypeError::MissingChild("member_expression missing object".to_string()))?;
1441 let property_node = member_expr_node
1442 .child_by_field_name("property")
1443 .ok_or_else(|| TypeError::MissingChild("member_expression missing property".to_string()))?;
1444
1445 if property_node.kind() != "identifier" {
1446 trace!("[Resolve Type V2] Member expr: Property is not an identifier.");
1447 return Ok(None);
1448 }
1449 let property_name = get_node_text(&property_node, source).to_string();
1450
1451 let object_type_name_opt = resolve_expression_type_v2(
1453 object_node,
1454 caller_node_id,
1455 caller_contract_name_opt,
1456 ctx,
1457 graph,
1458 source,
1459 solidity_lang,
1460 input,
1461 )?;
1462
1463 if let Some(object_type_name) = object_type_name_opt {
1464 resolve_property_type(
1466 &object_type_name,
1467 &property_name,
1468 caller_contract_name_opt,
1469 graph,
1470 ctx,
1471 source,
1472 solidity_lang,
1473 input,
1474 )
1475 } else {
1476 trace!("[Resolve Type V2] Member expr: Could not resolve object type.");
1477 Ok(None) }
1479}
1480
1481fn resolve_member_or_library_call_v2<'a>(
1484 object_type_name: &str,
1485 property_name: &str,
1486 caller_contract_name_opt: &'a Option<String>,
1487 graph: &'a CallGraph,
1488 ctx: &CallGraphGeneratorContext,
1489 source: &'a str, call_span_bytes: (usize, usize), ) -> std::result::Result<ResolvedTarget, TypeError> {
1492 let call_span_text = &source[call_span_bytes.0..call_span_bytes.1];
1494 trace!(
1495 "[Resolve Member/Lib V2] Resolving: Type='{}', Property='{}', CallerScope='{:?}', Span='{}'",
1496 object_type_name, property_name, caller_contract_name_opt, call_span_text
1497 );
1498
1499 if ctx.all_interfaces.contains_key(object_type_name) {
1501 trace!(
1502 "[Resolve Member/Lib V2] Object type '{}' is an interface. Looking for implementations.",
1503 object_type_name
1504 );
1505 let interface_name = object_type_name; let method_name = property_name; let mut implementing_contracts = Vec::new();
1510 for (contract, implemented_interfaces) in &ctx.contract_implements {
1511 if implemented_interfaces.contains(&interface_name.to_string()) {
1512 implementing_contracts.push(contract.clone());
1513 }
1514 }
1515 trace!(
1516 "[Resolve Member/Lib V2] Directly implementing contracts found: {:?}",
1517 implementing_contracts
1518 );
1519
1520 let mut potential_direct_targets = Vec::new();
1521 for contract_name_impl in &implementing_contracts {
1522 let target_key = (Some(contract_name_impl.clone()), method_name.to_string());
1523 if let Some(node_id) = graph.node_lookup.get(&target_key).copied() {
1524 if let Some(node) = graph.nodes.get(node_id) {
1525 potential_direct_targets.push(ResolvedTarget::Function {
1526 contract_name: Some(contract_name_impl.clone()),
1527 function_name: method_name.to_string(),
1528 node_type: node.node_type.clone(),
1529 });
1530 }
1531 }
1532 }
1533
1534 let mut natspec_binding_provided_concrete_impl: Option<ResolvedTarget> = None;
1536 let mut natspec_key_found_but_binding_non_concrete = false;
1537
1538 if let (Some(manifest_ref), Some(registry_ref)) =
1539 (ctx.manifest.as_ref(), ctx.binding_registry.as_ref())
1540 {
1541 let resolver =
1542 crate::interface_resolver::InterfaceResolver::new(manifest_ref, registry_ref);
1543 let interface_file_path_hint: Option<std::path::PathBuf> = manifest_ref
1544 .entries
1545 .iter()
1546 .find(|entry| {
1547 entry.item_kind == crate::natspec::extract::SourceItemKind::Interface
1548 && entry.item_name.as_deref() == Some(interface_name)
1549 })
1550 .map(|entry| entry.file_path.clone());
1551
1552 trace!(
1553 "[Resolve Member/Lib V2] Interface file hint for binding lookup: {:?}",
1554 interface_file_path_hint
1555 );
1556
1557 match resolver.resolve_for_item_name_kind(
1558 interface_name,
1559 crate::natspec::extract::SourceItemKind::Interface,
1560 interface_file_path_hint.as_deref(),
1561 ) {
1562 Ok(Some(binding_config)) => {
1563 trace!("[Resolve Member/Lib V2] Natspec binding key '{}' found for interface '{}'.", binding_config.key, interface_name);
1564 if let Some(concrete_contract_name) = &binding_config.contract_name {
1565 trace!("[Resolve Member/Lib V2] Binding config specifies concrete contract: '{}'", concrete_contract_name);
1566 let bound_target_key = (
1567 Some(concrete_contract_name.clone()),
1568 method_name.to_string(),
1569 );
1570 if let Some(node_id) = graph.node_lookup.get(&bound_target_key) {
1571 if let Some(node_info) = graph.nodes.get(*node_id) {
1572 match node_info.node_type {
1573 NodeType::Function | NodeType::Modifier | NodeType::Constructor => {
1574 let bound_implementation = ResolvedTarget::Function {
1575 contract_name: Some(concrete_contract_name.clone()),
1576 function_name: method_name.to_string(),
1577 node_type: node_info.node_type.clone(),
1578 };
1579 trace!("[Resolve Member/Lib V2] Successfully resolved to bound callable implementation: {:?}", bound_implementation);
1580 natspec_binding_provided_concrete_impl = Some(ResolvedTarget::InterfaceMethod {
1581 interface_name: interface_name.to_string(),
1582 method_name: method_name.to_string(),
1583 implementation: Some(Box::new(bound_implementation)),
1584 });
1585 }
1586 NodeType::StorageVariable => {
1587 if node_info.visibility == crate::cg::Visibility::Public {
1588 let getter_implementation = ResolvedTarget::Function {
1589 contract_name: Some(concrete_contract_name.clone()),
1590 function_name: method_name.to_string(), node_type: NodeType::Function, };
1593 trace!("[Resolve Member/Lib V2] Successfully resolved to getter for public storage variable: {:?}", getter_implementation);
1594 natspec_binding_provided_concrete_impl = Some(ResolvedTarget::InterfaceMethod {
1595 interface_name: interface_name.to_string(),
1596 method_name: method_name.to_string(),
1597 implementation: Some(Box::new(getter_implementation)),
1598 });
1599 } else {
1600 trace!("[Resolve Member/Lib V2] Bound contract '{}' member '{}' is a non-public StorageVariable. Binding not used.", concrete_contract_name, method_name);
1601 }
1602 }
1603 _ => trace!("[Resolve Member/Lib V2] Bound contract '{}' member '{}' is not callable or a public storage variable (Type: {:?}). Binding not used.", concrete_contract_name, method_name, node_info.node_type),
1604 }
1605 } else {
1606 trace!("[Resolve Member/Lib V2] Bound contract '{}' method '{}' (Node ID {}) not found in graph.nodes. Binding not used.", concrete_contract_name, method_name, node_id);
1607 }
1608 } else { trace!("[Resolve Member/Lib V2] Method or Variable '{}' not found in bound contract '{}' via graph.node_lookup. Binding not used.", method_name, concrete_contract_name);
1610 }
1611 } else {
1612 trace!("[Resolve Member/Lib V2] Binding for interface '{}' (key '{}') has no concrete_contract_name.", interface_name, binding_config.key);
1613 natspec_key_found_but_binding_non_concrete = true;
1614 }
1615 }
1616 Ok(None) => trace!("[Resolve Member/Lib V2] No Natspec binding key found for interface type '{}', or key not in registry.", interface_name),
1617 Err(e) => trace!("Warning: Error resolving Natspec binding for interface type '{}': {}. Proceeding as if no binding found.", interface_name, e),
1618 }
1619 } else {
1620 trace!("[Resolve Member/Lib V2] Manifest or BindingRegistry not available for Natspec-driven interface binding resolution.");
1621 }
1622
1623 if let Some(concrete_impl) = natspec_binding_provided_concrete_impl {
1625 return Ok(concrete_impl);
1626 }
1627
1628 if potential_direct_targets.len() == 1 {
1630 let direct_implementation = potential_direct_targets.remove(0);
1631 trace!("[Resolve Member/Lib V2] Resolved to single direct implementation (Natspec binding was not concrete or not found): {:?}", direct_implementation);
1632 return Ok(ResolvedTarget::InterfaceMethod {
1633 interface_name: interface_name.to_string(),
1634 method_name: method_name.to_string(),
1635 implementation: Some(Box::new(direct_implementation)),
1636 });
1637 }
1638
1639 if potential_direct_targets.len() > 1 {
1640 if natspec_key_found_but_binding_non_concrete {
1641 trace!("[Resolve Member/Lib V2] Ambiguous direct implementations for '{}.{}', but Natspec binding key was found (though non-concrete). Falling back to abstract.", interface_name, method_name);
1642 return Ok(ResolvedTarget::InterfaceMethod {
1643 interface_name: interface_name.to_string(),
1644 method_name: method_name.to_string(),
1645 implementation: None,
1646 });
1647 } else {
1648 trace!("[Resolve Member/Lib V2] Ambiguous implementation for '{}.{}' ({} direct targets) and no overriding/clarifying Natspec binding. Implementations: {:?}", interface_name, method_name, potential_direct_targets.len(), implementing_contracts);
1650 return Err(TypeError::AmbiguousInterfaceImplementation {
1651 interface_name: interface_name.to_string(),
1652 method_name: method_name.to_string(),
1653 implementations: implementing_contracts,
1654 });
1655 }
1656 }
1657
1658 trace!("[Resolve Member/Lib V2] No specific implementation found for '{}.{}'. Assuming external or abstract.", interface_name, method_name);
1660 return Ok(ResolvedTarget::InterfaceMethod {
1661 interface_name: interface_name.to_string(),
1662 method_name: method_name.to_string(),
1663 implementation: None,
1664 });
1665 }
1666
1667 let direct_lookup_key = (
1669 Some(object_type_name.to_string()),
1670 property_name.to_string(),
1671 );
1672 let lookup_result = graph.node_lookup.get(&direct_lookup_key);
1673 trace!(
1674 "[Resolve Member/Lib V2] Attempting direct lookup with key: {:?}. Found: {}",
1675 direct_lookup_key,
1676 lookup_result.is_some()
1677 );
1678 if let Some(node_id) = lookup_result {
1679 if let Some(node) = graph.nodes.get(*node_id) {
1681 match node.node_type {
1683 NodeType::Function | NodeType::Modifier | NodeType::Constructor => {
1684 trace!(
1685 "[Resolve Member/Lib V2] Found direct member: Node ID {}, Type: {:?}",
1686 node_id, node.node_type
1687 );
1688 return Ok(ResolvedTarget::Function {
1689 contract_name: node.contract_name.clone(),
1690 function_name: node.name.clone(),
1691 node_type: node.node_type.clone(),
1692 });
1693 }
1694 _ => {
1695 trace!("[Resolve Member/Lib V2] Found direct member but it's not callable (Type: {:?}). Node ID {}", node.node_type, node_id);
1696 return Ok(ResolvedTarget::NotCallable {
1698 reason: format!(
1699 "Member '{}' is a {:?}, not a function.",
1700 property_name, node.node_type
1701 ),
1702 });
1703 }
1704 }
1705 }
1706 } else {
1707 trace!(
1708 "[Resolve Member/Lib V2] Direct lookup failed for key: {:?}",
1709 direct_lookup_key
1710 );
1711 }
1712
1713 if let Some(library_target) = find_using_for_target(
1715 object_type_name,
1716 property_name,
1717 caller_contract_name_opt,
1718 graph,
1719 ctx,
1720 )? {
1721 trace!(
1722 "[Resolve Member/Lib V2] Found via 'using for': {:?}",
1723 library_target
1724 );
1725 return Ok(library_target);
1727 }
1728
1729 if let Some(builtin) = builtin::get_builtin(property_name, object_type_name) {
1731 trace!(
1732 "[Resolve Member/Lib V2] Resolved to built-in member '{}' for type '{}'",
1733 builtin.name, builtin.object_type
1734 );
1735 return Ok(ResolvedTarget::BuiltIn {
1736 object_type: object_type_name.to_string(), name: builtin.name.to_string(),
1738 });
1739 }
1740
1741 trace!(
1743 "[Resolve Member/Lib V2] Target '{}.{}' could not be resolved.",
1744 object_type_name, property_name
1745 );
1746 Ok(ResolvedTarget::NotCallable {
1747 reason: format!(
1748 "Member or method '{}.{}' not found.",
1749 object_type_name, property_name
1750 ),
1751 })
1752}
1753
1754fn find_using_for_target<'a>(
1756 object_type_name: &str,
1757 property_name: &str,
1758 caller_contract_name_opt: &'a Option<String>,
1759 graph: &'a CallGraph,
1760 ctx: &CallGraphGeneratorContext,
1761) -> std::result::Result<Option<ResolvedTarget>, TypeError> {
1762 let mut potential_libraries = Vec::new();
1764 let mut types_to_check = vec![object_type_name.to_string()];
1765
1766 if object_type_name == "uint" {
1767 types_to_check.push("uint256".to_string());
1768 } else if object_type_name == "uint256" {
1769 types_to_check.push("uint".to_string());
1770 }
1771
1772 if let Some(caller_contract_name) = caller_contract_name_opt {
1774 for type_name in &types_to_check {
1775 let specific_type_key = (
1778 Some(caller_contract_name.clone()),
1779 type_name.clone(), );
1781 trace!(
1782 "[Find UsingFor] Checking specific key: {:?}",
1783 specific_type_key
1784 ); if let Some(libs) = ctx.using_for_directives.get(&specific_type_key) {
1786 trace!(
1787 "[Find UsingFor] Found specific directive for contract '{}', type '{}': {:?}",
1788 caller_contract_name,
1789 type_name,
1790 libs );
1792 potential_libraries.extend(libs.iter().cloned());
1793 }
1794 }
1795 let wildcard_key = (Some(caller_contract_name.clone()), "*".to_string());
1797 trace!("[Find UsingFor] Checking wildcard key: {:?}", wildcard_key); if let Some(libs) = ctx.using_for_directives.get(&wildcard_key) {
1799 trace!(
1800 "[Find UsingFor] Found wildcard directive for contract '{}': {:?}",
1801 caller_contract_name, libs
1802 );
1803 potential_libraries.extend(libs.iter().cloned());
1804 }
1805 } else {
1806 trace!(
1807 "[Find UsingFor] No caller contract name provided. Skipping contract-specific directives."
1808 );
1809 }
1810 trace!(
1813 "[Find UsingFor] All potential libraries before dedup: {:?}",
1814 potential_libraries
1815 );
1816
1817 potential_libraries.sort_unstable();
1819 potential_libraries.dedup();
1820
1821 trace!(
1822 "[Find UsingFor] Potential libraries after dedup: {:?}", potential_libraries
1824 );
1825
1826 if !potential_libraries.is_empty() {
1827 trace!(
1828 "[Find UsingFor] Checking libraries for '{}.{}': {:?}",
1829 object_type_name,
1830 property_name,
1831 potential_libraries );
1833 for library_name in potential_libraries {
1834 let library_method_key = (Some(library_name.clone()), property_name.to_string());
1835 trace!(
1836 "[Find UsingFor] Checking library key: {:?}",
1837 library_method_key
1838 ); if let Some(id) = graph.node_lookup.get(&library_method_key) {
1840 if let Some(node) = graph.nodes.get(*id) {
1841 trace!(
1843 "[Find UsingFor] Found match in library '{}': Node ID {}",
1844 library_name, id
1845 );
1846 return Ok(Some(ResolvedTarget::Function {
1849 contract_name: Some(library_name.clone()), function_name: property_name.to_string(),
1851 node_type: node.node_type.clone(), }));
1853 }
1854 } else {
1855 trace!(
1856 "[Find UsingFor] Lookup failed for key: {:?}",
1857 library_method_key
1858 ); }
1860 }
1861 }
1862 trace!("[Find UsingFor] No match found via 'using for'."); Ok(None) }
1865
1866fn parse_mapping_final_value_type(type_str: &str) -> Result<String, String> {
1871 let trimmed_type_str = type_str.trim();
1872 if trimmed_type_str.starts_with("mapping(") && trimmed_type_str.ends_with(')') {
1873 let mut balance = 0;
1876 let mut arrow_index_opt: Option<usize> = None;
1877
1878 let content_start = "mapping(".len();
1880 let content_end = trimmed_type_str.len() - ")".len();
1881
1882 if content_start >= content_end {
1883 return Err(format!("Invalid mapping string (too short): {}", type_str));
1884 }
1885
1886 for (i, char_code) in trimmed_type_str
1887 .char_indices()
1888 .skip(content_start)
1889 .take(content_end - content_start)
1890 {
1891 if char_code == '(' {
1892 balance += 1;
1893 } else if char_code == ')' {
1894 balance -= 1;
1895 if balance < 0 {
1896 return Err(format!(
1898 "Malformed mapping string (unbalanced parentheses): {}",
1899 type_str
1900 ));
1901 }
1902 } else if char_code == '=' && balance == 0 {
1903 if trimmed_type_str.chars().nth(i + 1) == Some('>') {
1905 arrow_index_opt = Some(i);
1906 break;
1907 }
1908 }
1909 }
1910
1911 if let Some(arrow_idx) = arrow_index_opt {
1912 if arrow_idx + 2 >= content_end {
1914 return Err(format!(
1915 "Malformed mapping string (no value type after '=>'): {}",
1916 type_str
1917 ));
1918 }
1919 let value_type_part = trimmed_type_str[arrow_idx + 2..content_end].trim();
1920 parse_mapping_final_value_type(value_type_part)
1922 } else {
1923 Err(format!(
1924 "Malformed mapping string (could not find top-level '=>'): {}",
1925 type_str
1926 ))
1927 }
1928 } else {
1929 Ok(trimmed_type_str.to_string())
1931 }
1932}
1933
1934fn resolve_simple_call_v2<'a>(
1936 name: &str,
1937 caller_contract_name_opt: &'a Option<String>,
1938 graph: &'a CallGraph,
1939 ctx: &CallGraphGeneratorContext,
1940) -> std::result::Result<ResolvedTarget, TypeError> {
1941 trace!(
1943 "[Resolve Simple V2] Resolving simple call '{}', CallerScope='{:?}'",
1944 name, caller_contract_name_opt
1945 );
1946
1947 if let Some(contract_name) = caller_contract_name_opt {
1949 let key = (Some(contract_name.clone()), name.to_string());
1950 if let Some(id) = graph.node_lookup.get(&key) {
1951 if let Some(node) = graph.nodes.get(*id) {
1952 trace!(
1953 "[Resolve Simple V2] Found in contract scope: Node ID {}, Type: {:?}",
1954 id, node.node_type
1955 );
1956 return Ok(ResolvedTarget::Function {
1957 contract_name: node.contract_name.clone(),
1958 function_name: node.name.clone(),
1959 node_type: node.node_type.clone(),
1960 });
1961 }
1962 }
1963 if let Some(inherited_names) = ctx.contract_inherits.get(contract_name) {
1965 trace!(
1966 "[Resolve Simple V2] Checking inheritance for '{}': {:?}",
1967 contract_name, inherited_names
1968 );
1969 for base_name in inherited_names {
1970 let base_key = (Some(base_name.clone()), name.to_string());
1971 if let Some(id) = graph.node_lookup.get(&base_key) {
1972 if let Some(node) = graph.nodes.get(*id) {
1973 trace!("[Resolve Simple V2] Found in base contract '{}': Node ID {}, Type: {:?}", base_name, id, node.node_type);
1974 return Ok(ResolvedTarget::Function {
1977 contract_name: node.contract_name.clone(), function_name: node.name.clone(),
1979 node_type: node.node_type.clone(),
1980 });
1981 }
1982 }
1983 }
1984 }
1985 }
1986
1987 let free_function_key = (None, name.to_string());
1989 if let Some(id) = graph.node_lookup.get(&free_function_key) {
1990 if let Some(node) = graph.nodes.get(*id) {
1991 trace!(
1992 "[Resolve Simple V2] Found free function: Node ID {}, Type: {:?}",
1993 id, node.node_type
1994 );
1995 return Ok(ResolvedTarget::Function {
1996 contract_name: None,
1997 function_name: node.name.clone(),
1998 node_type: node.node_type.clone(),
1999 });
2000 }
2001 }
2002
2003 if ctx.all_contracts.contains_key(name) {
2005 let constructor_key = (Some(name.to_string()), name.to_string());
2006 if graph.node_lookup.contains_key(&constructor_key) {
2007 trace!(
2008 "[Resolve Simple V2] Found constructor for contract '{}'",
2009 name
2010 );
2011 return Ok(ResolvedTarget::Function {
2012 contract_name: Some(name.to_string()),
2013 function_name: name.to_string(),
2014 node_type: NodeType::Constructor,
2015 });
2016 }
2017 }
2018
2019 trace!(
2021 "[Resolve Simple V2] Simple call target '{}' not found.",
2022 name
2023 );
2024 Err(TypeError::TargetResolutionFailed {
2025 name: name.to_string(),
2026 reason: "Function or constructor not found in current scope or globally.".to_string(),
2027 })
2028}
2029
2030fn extract_arguments_v2<'a>(node: TsNode<'a>, source: &'a str) -> Vec<String> {
2032 let mut argument_texts: Vec<String> = Vec::new();
2033 let mut cursor = node.walk();
2034 for child_node in node.children(&mut cursor) {
2035 let mut search_queue = VecDeque::new();
2037 search_queue.push_back(child_node);
2038
2039 while let Some(current) = search_queue.pop_front() {
2040 if current.kind() == "call_argument" {
2041 if let Some(expression_node) = current.child(0) {
2042 argument_texts.push(get_node_text(&expression_node, source).to_string());
2043 }
2044 } else {
2045 let mut inner_cursor = current.walk();
2047 for inner_child in current.children(&mut inner_cursor) {
2048 if inner_child.kind() != node.kind() {
2050 search_queue.push_back(inner_child);
2052 }
2053 }
2054 }
2055 }
2056 }
2057 argument_texts
2058}
2059
2060fn get_function_return_type_v2(
2063 definition_node_info: &NodeInfo,
2064 input: &CallGraphGeneratorInput, ) -> std::result::Result<Option<String>, TypeError> {
2066 let query_str = r#"
2071 (function_definition
2072 return_type: (return_type_definition
2073 (parameter type: (type_name) @return_type_name_node)
2074 )
2075 )
2076
2077 ; Interface function definitions might have a slightly different structure
2078 ; but the return type part should be similar.
2079 (interface_declaration (contract_body (function_definition
2080 return_type: (return_type_definition
2081 (parameter type: (type_name) @return_type_name_node))
2082 )))
2083 "#;
2084 let query = Query::new(&input.solidity_lang, query_str)?;
2086 let mut cursor = QueryCursor::new();
2087 let source_bytes = input.source.as_bytes();
2088
2089 let definition_ts_node = input
2091 .tree
2092 .root_node()
2093 .descendant_for_byte_range(definition_node_info.span.0, definition_node_info.span.1)
2094 .ok_or_else(|| {
2095 TypeError::Internal(format!(
2097 "Failed to find definition TsNode for span {:?}",
2098 definition_node_info.span
2099 ))
2100 })?;
2101
2102 let mut matches = cursor.matches(
2103 &query,
2104 definition_ts_node, |node: TsNode| std::iter::once(&source_bytes[node.byte_range()]),
2106 );
2107 while let Some(match_) = matches.next() {
2109 for capture in match_.captures {
2111 if query.capture_names()[capture.index as usize] == "return_type_name_node" {
2113 let type_name_node = capture.node;
2114 let type_name_text = get_node_text(&type_name_node, &input.source).to_string();
2115
2116 if !type_name_text.is_empty() {
2117 trace!(
2118 "[Get Return Type V2] Found single return type name: '{}'",
2119 type_name_text
2120 );
2121 return Ok(Some(type_name_text)); } else {
2124 trace!("[Get Return Type V2] Found empty return type name node.");
2125 }
2127 }
2128 }
2129 }
2130
2131 trace!("[Get Return Type V2] No return type name node captured or parsed.");
2132 Ok(None) }
2134
2135fn resolve_call_return_type<'a>(
2137 target: &ResolvedTarget,
2138 ctx: &CallGraphGeneratorContext,
2139 graph: &'a CallGraph,
2140 source: &'a str,
2141 solidity_lang: &'a tree_sitter::Language,
2142 input: &'a CallGraphGeneratorInput, ) -> std::result::Result<Option<String>, TypeError> {
2144 match target {
2146 ResolvedTarget::Function {
2147 contract_name,
2148 function_name,
2149 node_type: _,
2150 } => {
2151 let lookup_key = (contract_name.clone(), function_name.clone());
2153 if let Some(node_id) = graph.node_lookup.get(&lookup_key) {
2154 let graph_node = &graph.nodes[*node_id]; match graph_node.node_type {
2157 NodeType::Function | NodeType::Modifier | NodeType::Constructor => {
2158 if let Some(declared_type) = &graph_node.declared_return_type {
2160 trace!("[Resolve Call Return Type] Using declared_return_type '{}' for Node ID {}", declared_type, *node_id);
2161 return Ok(Some(declared_type.clone()));
2162 }
2163 let def_node_info_opt = ctx
2165 .definition_nodes_info
2166 .iter()
2167 .find(|(id, _, _)| *id == *node_id)
2168 .map(|(_, info, _)| info.clone());
2169 if let Some(def_node_info) = def_node_info_opt {
2170 trace!("[Resolve Call Return Type] Falling back to get_function_return_type_v2 for Node ID {}", *node_id);
2171 return get_function_return_type_v2(&def_node_info, input);
2172 }
2173 trace!("[Resolve Call Return Type] No declared_return_type and no NodeInfo for Node ID {}", *node_id);
2174 return Ok(None);
2175 }
2176 NodeType::StorageVariable => {
2177 if graph_node.visibility == crate::cg::Visibility::Public {
2178 let actual_contract_name =
2179 graph_node.contract_name.as_ref().ok_or_else(|| {
2180 TypeError::Internal(
2181 "StorageVariable node missing contract name".to_string(),
2182 )
2183 })?;
2184 let var_name = &graph_node.name;
2185 trace!("[Resolve Call Return Type] ResolvedTarget::Function points to public StorageVariable '{}.{}'. Determining getter return type.", actual_contract_name, var_name);
2186
2187 if let Some(mapping_info) = ctx
2189 .contract_mappings
2190 .get(&(actual_contract_name.clone(), var_name.clone()))
2191 {
2192 trace!("[Resolve Call Return Type] Found mapping info. Value type: '{}'", mapping_info.value_type);
2193 return Ok(Some(mapping_info.value_type.clone()));
2194 }
2195 if let Some(type_str) = ctx
2197 .state_var_types
2198 .get(&(actual_contract_name.clone(), var_name.clone()))
2199 {
2200 trace!("[Resolve Call Return Type] Found state_var_types info. Type: '{}'", type_str);
2201 return Ok(Some(type_str.clone()));
2202 }
2203 trace!("[Resolve Call Return Type] Public StorageVariable {}.{} type info not found in contract_mappings or state_var_types.", actual_contract_name, var_name);
2204 return Err(TypeError::Internal(format!(
2205 "Public StorageVariable {}.{} type info not found.",
2206 actual_contract_name, var_name
2207 )));
2208 } else {
2209 trace!("[Resolve Call Return Type] ResolvedTarget::Function points to non-public StorageVariable '{}.{}'. No getter.", graph_node.contract_name.as_deref().unwrap_or("?"), graph_node.name);
2210 return Ok(None);
2211 }
2212 }
2213 _ => {
2214 trace!("[Resolve Call Return Type] Warning: Node for '{:?}' (ID {}) is of unexpected type {:?}.", lookup_key, *node_id, graph_node.node_type);
2215 return Ok(None);
2216 }
2217 }
2218 } else {
2219 trace!("[Resolve Call Return Type] Warning: Node for '{:?}' not found in graph.node_lookup.", lookup_key);
2220 return Ok(None);
2221 }
2222 }
2223 ResolvedTarget::InterfaceMethod {
2224 interface_name,
2225 method_name,
2226 implementation,
2227 } => {
2228 if let Some(impl_target) = implementation {
2230 resolve_call_return_type(impl_target, ctx, graph, source, solidity_lang, input)
2231 } else {
2232 let _iface_key = (Some(interface_name.clone()), interface_name.clone()); let method_key = (Some(interface_name.clone()), method_name.clone()); if let Some(method_node_id) = graph.node_lookup.get(&method_key) {
2236 let def_node_opt = ctx
2237 .definition_nodes_info
2238 .iter()
2239 .find(|(id, _, _)| *id == *method_node_id)
2240 .map(|(_, n, _)| n.clone());
2241 if let Some(def_node) = def_node_opt {
2242 get_function_return_type_v2(&def_node, input)
2243 } else {
2244 Ok(None) }
2246 } else {
2247 Ok(None) }
2249 }
2250 }
2251 ResolvedTarget::BuiltIn { object_type, name } => {
2252 if let Some(return_type_str) = builtin::get_builtin_return_type(name, object_type) {
2254 if return_type_str == "void" {
2255 Ok(None) } else {
2257 Ok(Some(return_type_str.to_string()))
2258 }
2259 } else {
2260 trace!(
2261 "[Resolve Return Type] Warning: Could not find return type for known built-in {}.{}",
2262 object_type, name
2263 );
2264 Ok(None) }
2266 }
2267 ResolvedTarget::NotCallable { .. } => Ok(None), ResolvedTarget::External { .. } => Ok(None), ResolvedTarget::TypeCast { type_name } => {
2270 Ok(Some(type_name.clone()))
2272 }
2273 }
2274}
2275
2276#[cfg(test)]
2277mod tests {
2278 use super::*;
2279 use crate::cg::{
2280 CallGraph, CallGraphGeneratorContext, CallGraphGeneratorInput, CallGraphGeneratorPipeline,
2281 }; use crate::steps::CallsHandling;
2283 use crate::steps::ContractHandling;
2284 use anyhow::{Context, Result};
2285 use crate::parser::get_solidity_language;
2286 use std::collections::HashMap;
2287 use tree_sitter::{Parser, Tree};
2289
2290 use crate::interface_resolver::{BindingConfig, BindingRegistry}; use crate::manifest::{Manifest, ManifestEntry};
2293 use crate::natspec::extract::SourceItemKind as NatspecSourceItemKind;
2294 use crate::natspec::{TextIndex as NatspecTextIndex, TextRange as NatspecTextRange};
2295
2296 fn setup_test_environment(
2298 source: &str,
2299 ) -> Result<(
2300 CallGraphGeneratorContext,
2301 CallGraph,
2302 Tree,
2303 tree_sitter::Language,
2304 CallGraphGeneratorInput,
2305 )> {
2306 let solidity_lang = get_solidity_language();
2307 let mut parser = Parser::new();
2308 parser
2309 .set_language(&solidity_lang)
2310 .context("Failed to set language")?;
2311 let tree = parser
2312 .parse(source, None)
2313 .context("Failed to parse source code")?;
2314
2315 let mut ctx = CallGraphGeneratorContext::default();
2316 let mut graph = CallGraph::new();
2317
2318 let input = CallGraphGeneratorInput {
2321 source: source.to_string(),
2322 tree: tree.clone(),
2323 solidity_lang: solidity_lang.clone(),
2324 };
2325 let input_clone = input.clone();
2326 let mut pipeline = CallGraphGeneratorPipeline::new();
2327 pipeline.add_step(Box::new(ContractHandling::default()));
2328 pipeline.add_step(Box::new(CallsHandling::default()));
2329 let config: HashMap<String, String> = HashMap::new(); pipeline
2331 .run(input, &mut ctx, &mut graph, &config)
2332 .context("Pipeline execution failed")?;
2333 Ok((
2336 ctx.clone(),
2337 graph.clone(),
2338 tree.clone(),
2339 solidity_lang.clone(),
2340 input_clone, ))
2342 }
2343
2344 fn setup_test_environment_customized(
2346 source: &str,
2347 custom_manifest: Option<Manifest>,
2348 custom_binding_registry: Option<BindingRegistry>,
2349 ) -> Result<(
2352 CallGraphGeneratorContext,
2353 CallGraph,
2354 Tree,
2355 tree_sitter::Language,
2356 CallGraphGeneratorInput,
2357 )> {
2358 let solidity_lang = get_solidity_language();
2359 let mut parser = Parser::new();
2360 parser
2361 .set_language(&solidity_lang)
2362 .context("Failed to set language")?;
2363 let tree = parser
2364 .parse(source, None)
2365 .context("Failed to parse source code")?;
2366
2367 let mut ctx = CallGraphGeneratorContext::default();
2368 let mut graph = CallGraph::new();
2369
2370 if let Some(manifest) = custom_manifest {
2372 ctx.manifest = Some(manifest);
2373 }
2374 if let Some(registry) = custom_binding_registry {
2375 ctx.binding_registry = Some(registry);
2376 }
2377
2378 let input = CallGraphGeneratorInput {
2382 source: source.to_string(),
2383 tree: tree.clone(),
2384 solidity_lang: solidity_lang.clone(),
2385 };
2386 let input_clone = input.clone();
2387 let mut pipeline = CallGraphGeneratorPipeline::new();
2388 pipeline.add_step(Box::new(ContractHandling::default())); let config: HashMap<String, String> = HashMap::new();
2393 pipeline
2394 .run(input, &mut ctx, &mut graph, &config)
2395 .context("Minimal pipeline execution failed for custom setup")?;
2396
2397 Ok((
2404 ctx.clone(),
2405 graph.clone(),
2406 tree.clone(),
2407 solidity_lang.clone(),
2408 input_clone,
2409 ))
2410 }
2411 fn find_node_by_kind_and_text<'a>(
2413 tree: &'a Tree,
2414 source: &'a str,
2415 kind: &str,
2416 text: &str,
2417 ) -> Option<TsNode<'a>> {
2418 let mut cursor = tree.walk();
2419 loop {
2420 let node = cursor.node();
2421 if node.kind() == kind && get_node_text(&node, source) == text {
2422 return Some(node);
2423 }
2424 if !cursor.goto_first_child() {
2425 while !cursor.goto_next_sibling() {
2426 if !cursor.goto_parent() {
2427 return None; }
2429 }
2430 }
2431 }
2432 }
2433
2434 fn find_function_definition_node_by_name<'a>(
2436 tree: &'a Tree,
2437 source: &'a str,
2438 lang: &'a tree_sitter::Language,
2439 function_name: &str,
2440 ) -> Result<TsNode<'a>> {
2441 let query_str = r#"
2443 (function_definition
2444 name: (identifier) @function_name
2445 )
2446 "#;
2447 let query = Query::new(lang, query_str).context("Failed to create function name query")?;
2448 let mut cursor = QueryCursor::new();
2449 let source_bytes = source.as_bytes();
2450
2451 let mut matches = cursor.matches(&query, tree.root_node(), |node: TsNode| {
2452 std::iter::once(&source_bytes[node.byte_range()])
2453 });
2454
2455 while let Some(match_) = matches.next() {
2457 for capture in match_.captures {
2458 if query.capture_names()[capture.index as usize] == "function_name" {
2460 let name_node = capture.node;
2461 if get_node_text(&name_node, source) == function_name {
2462 return name_node.parent().ok_or_else(|| {
2464 anyhow::anyhow!(
2465 "Failed to get parent of function name identifier '{}'",
2467 function_name
2468 )
2469 });
2470 }
2471 }
2472 }
2473 }
2474
2475 Err(anyhow::anyhow!(
2476 "Function definition node for '{}' not found",
2478 function_name
2479 ))
2480 }
2481
2482 fn find_nth_node_of_kind<'a>(tree: &'a Tree, kind: &str, n: usize) -> Option<TsNode<'a>> {
2484 let mut cursor = tree.walk();
2485 let mut count = 0;
2486 loop {
2487 let node = cursor.node();
2488 if node.kind() == kind {
2489 if count == n {
2490 return Some(node);
2491 }
2492 count += 1;
2493 }
2494 if !cursor.goto_first_child() {
2495 while !cursor.goto_next_sibling() {
2496 if !cursor.goto_parent() {
2497 return None; }
2499 }
2500 }
2501 }
2502 }
2503
2504 fn find_nth_descendant_node_of_kind<'a>(
2506 start_node: &TsNode<'a>, source: &'a str,
2508 lang: &'a tree_sitter::Language,
2509 kind: &str,
2510 n: usize,
2511 ) -> Result<Option<TsNode<'a>>> {
2512 let query_str = format!("({}) @target_kind", kind);
2514 let query = Query::new(lang, &query_str)
2515 .with_context(|| format!("Failed to create query for kind '{}'", kind))?;
2516 let mut cursor = QueryCursor::new();
2517 let source_bytes = source.as_bytes();
2518
2519 let mut captures = cursor.captures(
2521 &query,
2522 start_node.clone(), |node: TsNode| std::iter::once(&source_bytes[node.byte_range()]),
2524 );
2525
2526 let mut count = 0;
2527 while let Some((match_, capture_index)) = captures.next() {
2529 if *capture_index == 0 {
2532 let node = match_.captures[*capture_index].node; trace!(
2535 "[find_nth_descendant] Found node: Kind='{}', Span=({},{}), Count={}",
2537 node.kind(),
2538 node.start_byte(),
2539 node.end_byte(),
2540 count
2541 );
2542 if count == n {
2543 trace!("[find_nth_descendant] Returning node at index {}", n); return Ok(Some(node)); }
2546 count += 1;
2547 }
2548 }
2549 trace!(
2550 "[find_nth_descendant] Node of kind '{}' at index {} not found.",
2551 kind, n
2552 ); Ok(None) }
2555
2556 #[test]
2557 fn test_resolve_simple_identifier_type() -> Result<()> {
2558 let source = r#"
2560 contract Test {
2561 uint256 stateVar;
2562 bool flag;
2563 function testFunc(uint256 local) public {
2564 stateVar = 1; // Access state var
2565 flag = true; // Access state var
2566 uint256 x = local; // Access local var
2567 }
2568 }
2569 "#;
2570 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2571 let _func_def_node = find_nth_node_of_kind(&tree, "function_definition", 0)
2573 .expect("missing function definition");
2574 let caller_node_id = graph
2575 .node_lookup
2576 .get(&(Some("Test".to_string()), "testFunc".to_string()))
2577 .copied()
2578 .unwrap();
2579
2580 let state_var_node =
2582 find_node_by_kind_and_text(&tree, source, "identifier", "stateVar").unwrap();
2583 let flag_node = find_node_by_kind_and_text(&tree, source, "identifier", "flag").unwrap();
2584 let type1 = resolve_expression_type_v2(
2588 state_var_node,
2589 caller_node_id,
2590 &Some("Test".to_string()),
2591 &ctx,
2592 &graph,
2593 source,
2594 &lang,
2595 &input,
2596 )?;
2597 assert_eq!(type1, Some("uint256".to_string()));
2598
2599 let type2 = resolve_expression_type_v2(
2600 flag_node,
2601 caller_node_id,
2602 &Some("Test".to_string()),
2603 &ctx,
2604 &graph,
2605 source,
2606 &lang,
2607 &input,
2608 )?;
2609 assert_eq!(type2, Some("bool".to_string()));
2610
2611 Ok(())
2616 }
2617
2618 #[test]
2619 fn test_resolve_member_access_type() -> Result<()> {
2620 let source = r#"
2622 contract C { uint public x; }
2623 contract Test {
2624 C c_instance;
2625 function getX() public view returns (uint) {
2626 return c_instance.x; // Member access
2627 }
2628 }
2629 "#;
2630 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2631 let _func_node = find_nth_node_of_kind(&tree, "function_definition", 0)
2632 .expect("missing function definition");
2633 let _member_expr_node = find_nth_node_of_kind(&tree, "member_expression", 0)
2635 .expect("missing member expression"); let caller_node_id = graph
2637 .node_lookup
2638 .get(&(Some("Test".to_string()), "getX".to_string()))
2639 .copied()
2640 .unwrap();
2641
2642 let member_expr_node = find_nth_node_of_kind(&tree, "member_expression", 0)
2644 .expect("missing member expression"); let resolved_type = resolve_expression_type_v2(
2647 member_expr_node,
2648 caller_node_id,
2649 &Some("Test".to_string()),
2650 &ctx,
2651 &graph,
2652 source,
2653 &lang,
2654 &input,
2655 )?;
2656 assert_eq!(resolved_type, Some("uint".to_string()));
2657
2658 Ok(())
2659 }
2660
2661 #[test]
2662 fn test_analyze_simple_call() -> Result<()> {
2663 let source = r#"
2665 contract Test {
2666 function target() internal pure returns (uint) { return 1; }
2667 function caller() public pure {
2668 target(); // Simple call
2669 }
2670 }
2671 "#;
2672 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2673 let _caller_def_node =
2675 find_function_definition_node_by_name(&tree, source, &lang, "caller")
2676 .expect("missing function definition");
2677 let caller_node_id = graph
2678 .node_lookup
2679 .get(&(Some("Test".to_string()), "caller".to_string()))
2680 .copied()
2681 .unwrap();
2682
2683 let call_expr_node = find_nth_node_of_kind(&tree, "call_expression", 0)
2685 .expect("missing call expression"); let steps = analyze_chained_call(
2688 call_expr_node,
2689 caller_node_id,
2690 &Some("Test".to_string()),
2691 &ctx,
2692 &graph,
2693 source,
2694 &lang,
2695 &input,
2696 None, call_expr_node.start_byte(), )?;
2699
2700 assert_eq!(steps.len(), 1);
2701 let step = &steps[0];
2702 assert_eq!(step.object_type, None);
2703 assert_eq!(step.result_type, Some("uint".to_string())); assert!(step.arguments.is_empty());
2705 match &step.target {
2706 ResolvedTarget::Function {
2707 contract_name,
2708 function_name,
2709 node_type,
2710 } => {
2711 assert_eq!(contract_name.as_deref(), Some("Test"));
2712 assert_eq!(function_name, "target");
2713 assert_eq!(*node_type, NodeType::Function);
2714 }
2715 _ => panic!("Expected ResolvedTarget::Function, got {:?}", step.target),
2716 }
2717
2718 Ok(())
2719 }
2720
2721 #[test]
2722 fn test_analyze_member_call() -> Result<()> {
2723 let source = r#"
2725 contract C {
2726 function method() public pure returns (bool) { return true; }
2727 }
2728 contract Test {
2729 C c_instance;
2730 function caller() public {
2731 c_instance.method(); // Member call
2732 }
2733 }
2734 "#;
2735 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2736 let caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")
2738 .expect("missing function definition");
2739 let caller_node_id = graph
2740 .node_lookup
2741 .get(&(Some("Test".to_string()), "caller".to_string()))
2742 .copied()
2743 .unwrap();
2744
2745 let call_expr_node_opt = find_nth_descendant_node_of_kind(
2748 &caller_def_node, source,
2751 &lang,
2752 "call_expression", 0, )?; let call_expr_node = call_expr_node_opt.ok_or_else(|| { TypeError::Internal(format!(
2758 "Could not find the call_expression node for c_instance.method() within caller function node: {:?}",
2759 caller_def_node.byte_range()
2760 ))
2761 })?; let steps = analyze_chained_call(
2764 call_expr_node,
2765 caller_node_id,
2766 &Some("Test".to_string()),
2767 &ctx,
2768 &graph,
2769 source,
2770 &lang,
2771 &input,
2772 None,
2773 call_expr_node.start_byte(), )?;
2775
2776 assert_eq!(steps.len(), 1);
2777 let step = &steps[0];
2778 assert_eq!(step.object_type, Some("C".to_string())); assert_eq!(step.result_type, Some("bool".to_string())); assert!(step.arguments.is_empty());
2781 match &step.target {
2782 ResolvedTarget::Function {
2783 contract_name,
2784 function_name,
2785 node_type,
2786 } => {
2787 assert_eq!(contract_name.as_deref(), Some("C"));
2788 assert_eq!(function_name, "method");
2789 assert_eq!(*node_type, NodeType::Function);
2790 }
2791 _ => panic!("Expected ResolvedTarget::Function, got {:?}", step.target),
2792 }
2793
2794 Ok(())
2795 }
2796
2797 #[test]
2798 fn test_analyze_using_for_call() -> Result<()> {
2799 let source = r#"
2801 library SafeMath {
2802 function add(uint a, uint b) internal pure returns (uint) { return a+b; }
2803 }
2804 contract Test {
2805 using SafeMath for uint;
2806 uint value;
2807 function caller(uint y) public {
2808 value = value.add(y); // Using for call
2809 }
2810 }
2811 "#;
2812 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2813 let _caller_def_node =
2814 find_function_definition_node_by_name(&tree, source, &lang, "caller")
2815 .expect("missing function definition");
2816 let caller_node_id = graph
2817 .node_lookup
2818 .get(&(Some("Test".to_string()), "caller".to_string()))
2819 .copied()
2820 .unwrap();
2821
2822 let call_expr_node = find_nth_node_of_kind(&tree, "call_expression", 0).unwrap();
2824
2825 let steps = analyze_chained_call(
2826 call_expr_node,
2827 caller_node_id,
2828 &Some("Test".to_string()),
2829 &ctx,
2830 &graph,
2831 source,
2832 &lang,
2833 &input,
2834 None,
2835 call_expr_node.start_byte(), )?;
2837
2838 assert_eq!(steps.len(), 1);
2839 let step = &steps[0];
2840 assert_eq!(step.object_type, Some("uint".to_string()));
2842 assert_eq!(step.result_type, Some("uint".to_string())); assert_eq!(step.arguments, vec!["y".to_string()]);
2844 match &step.target {
2845 ResolvedTarget::Function {
2846 contract_name,
2847 function_name,
2848 node_type,
2849 } => {
2850 assert_eq!(contract_name.as_deref(), Some("SafeMath")); assert_eq!(function_name, "add");
2852 assert_eq!(*node_type, NodeType::Function);
2853 }
2854 _ => panic!(
2855 "Expected ResolvedTarget::Function (Library), got {:?}",
2856 step.target
2857 ),
2858 }
2859
2860 Ok(())
2861 }
2862
2863 #[test]
2864 fn test_analyze_interface_call_single_impl() -> Result<()> {
2865 let source = r#"
2867 interface ICounter { function increment() external; }
2868 contract Counter is ICounter { function increment() external override {} }
2869 contract Test {
2870 ICounter c;
2871 function caller() public {
2872 c.increment(); // Interface call
2873 }
2874 }
2875 "#;
2876 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2877 let _caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")
2878 .expect("missing function definition");
2879 let caller_node_id = graph
2880 .node_lookup
2881 .get(&(Some("Test".to_string()), "caller".to_string()))
2882 .copied()
2883 .unwrap();
2884
2885 let call_expr_node = find_nth_node_of_kind(&tree, "call_expression", 0).unwrap();
2887
2888 let steps = analyze_chained_call(
2889 call_expr_node,
2890 caller_node_id,
2891 &Some("Test".to_string()),
2892 &ctx,
2893 &graph,
2894 source,
2895 &lang,
2896 &input,
2897 None,
2898 call_expr_node.start_byte(), )?;
2900
2901 assert_eq!(steps.len(), 1);
2902 let step = &steps[0];
2903 assert_eq!(step.object_type, Some("ICounter".to_string())); assert_eq!(step.result_type, None); assert!(step.arguments.is_empty());
2906 match &step.target {
2907 ResolvedTarget::InterfaceMethod {
2908 interface_name,
2909 method_name,
2910 implementation,
2911 } => {
2912 assert_eq!(interface_name, "ICounter");
2913 assert_eq!(method_name, "increment");
2914 assert!(implementation.is_some());
2915 match implementation.as_deref() {
2917 Some(ResolvedTarget::Function {
2918 contract_name,
2919 function_name,
2920 node_type,
2921 }) => {
2922 assert_eq!(contract_name.as_deref(), Some("Counter")); assert_eq!(function_name, "increment");
2924 assert_eq!(*node_type, NodeType::Function);
2925 }
2926 _ => panic!(
2927 "Expected implementation to be ResolvedTarget::Function, got {:?}",
2928 implementation
2929 ),
2930 }
2931 }
2932 _ => panic!(
2933 "Expected ResolvedTarget::InterfaceMethod, got {:?}",
2934 step.target
2935 ),
2936 }
2937
2938 Ok(())
2939 }
2940
2941 #[test]
2942 fn test_analyze_chained_call_using_for() -> Result<()> {
2943 let source = r#"
2945 library SafeMath {
2946 function add(uint a, uint b) internal pure returns (uint) { return a+b; }
2947 function sub(uint a, uint b) internal pure returns (uint) { return a-b; }
2948 }
2949 contract Test {
2950 using SafeMath for uint;
2951 uint value;
2952 function caller(uint x, uint y) public {
2953 value = value.add(x).sub(y); // Chained using for call
2954 }
2955 }
2956 "#;
2957 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
2958
2959 let _caller_def_node =
2960 find_function_definition_node_by_name(&tree, source, &lang, "caller")
2961 .expect("missing function definition");
2962 let caller_node_id = graph
2963 .node_lookup
2964 .get(&(Some("Test".to_string()), "caller".to_string()))
2965 .copied()
2966 .unwrap();
2967
2968 let caller_def_node_for_search =
2971 find_function_definition_node_by_name(&tree, source, &lang, "caller")
2972 .expect("missing function definition");
2973 let assignment_node = find_nth_descendant_node_of_kind(
2974 &caller_def_node_for_search, source,
2976 &lang,
2977 "assignment_expression", 0, )
2980 .expect("Failed to query for assignment node")
2981 .expect("Could not find the assignment_expression node within caller");
2982
2983 let outer_call_expr_node = assignment_node
2985 .child_by_field_name("right")
2986 .expect("Assignment node missing 'right' child")
2987 .child(0)
2988 .expect("Assignment 'right' expression missing child(0)");
2989
2990 assert_eq!(
2991 outer_call_expr_node.kind(),
2992 "call_expression",
2993 "Navigated node is not a call_expression"
2994 );
2995 trace!(
2996 "DEBUG [Test]: Analyzing node kind='{}', text='{}'",
2997 outer_call_expr_node.kind(),
2998 get_node_text(&outer_call_expr_node, source)
2999 );
3000
3001 let steps = analyze_chained_call(
3002 outer_call_expr_node, caller_node_id,
3004 &Some("Test".to_string()),
3005 &ctx,
3006 &graph,
3007 source,
3008 &lang,
3009 &input,
3010 None,
3011 outer_call_expr_node.start_byte(), )?;
3013
3014 assert_eq!(steps.len(), 2);
3016
3017 let step1 = &steps[0];
3019 assert_eq!(step1.object_type, Some("uint".to_string())); assert_eq!(step1.result_type, Some("uint".to_string())); assert_eq!(step1.arguments, vec!["x".to_string()]);
3022 match &step1.target {
3023 ResolvedTarget::Function {
3024 contract_name,
3025 function_name,
3026 ..
3027 } => {
3028 assert_eq!(contract_name.as_deref(), Some("SafeMath"));
3029 assert_eq!(function_name, "add");
3030 }
3031 _ => panic!("Step 1: Expected Library Function, got {:?}", step1.target),
3032 }
3033
3034 let step2 = &steps[1];
3036 assert_eq!(step2.object_type, Some("uint".to_string())); assert_eq!(step2.result_type, Some("uint".to_string())); assert_eq!(step2.arguments, vec!["y".to_string()]);
3039 match &step2.target {
3040 ResolvedTarget::Function {
3041 contract_name,
3042 function_name,
3043 ..
3044 } => {
3045 assert_eq!(contract_name.as_deref(), Some("SafeMath"));
3046 assert_eq!(function_name, "sub");
3047 }
3048 _ => panic!("Step 2: Expected Library Function, got {:?}", step2.target),
3049 }
3050
3051 Ok(())
3052 }
3053
3054 #[test]
3055 fn test_analyze_chained_call_interface_factory() -> Result<()> {
3056 let source = r#"
3058 interface IAction { function perform() external returns (bool); }
3059 contract Action is IAction { function perform() external override returns (bool) { return true; } }
3060 interface IFactory { function create() external returns (IAction); }
3061 contract Factory is IFactory { function create() external override returns (IAction) { return new Action(); } }
3062 contract Test {
3063 IFactory factory;
3064 function caller() public returns (bool) {
3065 // factory.create().perform()
3066 return factory.create().perform();
3067 }
3068 }
3069 "#;
3070 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3071
3072 let _caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")
3073 .expect("missing function definition");
3074 let caller_node_id = graph
3075 .node_lookup
3076 .get(&(Some("Test".to_string()), "caller".to_string()))
3077 .copied()
3078 .unwrap();
3079
3080 let call_expr_node = find_nth_node_of_kind(&tree, "call_expression", 1).unwrap(); let steps = analyze_chained_call(
3084 call_expr_node,
3085 caller_node_id,
3086 &Some("Test".to_string()),
3087 &ctx,
3088 &graph,
3089 source,
3090 &lang,
3091 &input,
3092 None,
3093 call_expr_node.start_byte(), )?;
3095
3096 assert_eq!(steps.len(), 2);
3098
3099 let step1 = &steps[0];
3101 assert_eq!(step1.object_type, Some("IFactory".to_string())); assert_eq!(step1.result_type, Some("IAction".to_string())); assert!(step1.arguments.is_empty());
3104 match &step1.target {
3105 ResolvedTarget::InterfaceMethod {
3106 interface_name,
3107 method_name,
3108 implementation,
3109 } => {
3110 assert_eq!(interface_name, "IFactory");
3111 assert_eq!(method_name, "create");
3112 assert!(implementation.is_some()); match implementation.as_deref() {
3114 Some(ResolvedTarget::Function {
3115 contract_name,
3116 function_name,
3117 ..
3118 }) => {
3119 assert_eq!(contract_name.as_deref(), Some("Factory"));
3120 assert_eq!(function_name, "create");
3121 }
3122 _ => panic!(
3123 "Step 1: Expected implementation Function, got {:?}",
3124 implementation
3125 ),
3126 }
3127 }
3128 _ => panic!("Step 1: Expected InterfaceMethod, got {:?}", step1.target),
3129 }
3130
3131 let step2 = &steps[1];
3133 assert_eq!(step2.object_type, Some("IAction".to_string())); assert_eq!(step2.result_type, Some("bool".to_string())); assert!(step2.arguments.is_empty());
3136 match &step2.target {
3137 ResolvedTarget::InterfaceMethod {
3138 interface_name,
3139 method_name,
3140 implementation,
3141 } => {
3142 assert_eq!(interface_name, "IAction");
3143 assert_eq!(method_name, "perform");
3144 assert!(implementation.is_some()); match implementation.as_deref() {
3146 Some(ResolvedTarget::Function {
3147 contract_name,
3148 function_name,
3149 ..
3150 }) => {
3151 assert_eq!(contract_name.as_deref(), Some("Action"));
3152 assert_eq!(function_name, "perform");
3153 }
3154 _ => panic!(
3155 "Step 2: Expected implementation Function, got {:?}",
3156 implementation
3157 ),
3158 }
3159 }
3160 _ => panic!("Step 2: Expected InterfaceMethod, got {:?}", step2.target),
3161 }
3162
3163 Ok(())
3164 }
3165
3166 #[test]
3167 fn test_analyze_chained_call_inline_factory() -> Result<()> {
3168 let source = r#"
3171 interface IAction { function perform() external returns (bool); }
3172 contract Action is IAction { function perform() external override returns (bool) { return true; } }
3173 interface IFactory { function create() external returns (IAction); }
3174 contract Factory is IFactory { function create() external override returns (IAction) { return new Action(); } }
3175 contract Test {
3176 // No factory state variable
3177 function caller() public returns (bool) {
3178 // Instantiate factory inline, then call create, then perform
3179 return new Factory().create().perform();
3180 }
3181 }
3182 "#;
3183 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3184
3185 let _caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")
3186 .expect("missing function definition");
3187 let caller_node_id = graph
3188 .node_lookup
3189 .get(&(Some("Test".to_string()), "caller".to_string()))
3190 .copied()
3191 .unwrap();
3192
3193 let outer_call_expr_node =
3196 find_nth_node_of_kind(&tree, "call_expression", 1) .expect("Could not find the outer call_expression node for .perform()");
3198
3199 trace!(
3201 "[Test Inline Factory] Analyzing node kind='{}', text='{}'",
3202 outer_call_expr_node.kind(),
3203 get_node_text(&outer_call_expr_node, source)
3204 );
3205 assert!(get_node_text(&outer_call_expr_node, source).contains(".perform()"));
3206
3207 let steps = analyze_chained_call(
3208 outer_call_expr_node,
3209 caller_node_id,
3210 &Some("Test".to_string()),
3211 &ctx,
3212 &graph,
3213 source,
3214 &lang,
3215 &input,
3216 None,
3217 outer_call_expr_node.start_byte(), )?;
3219
3220 assert_eq!(steps.len(), 3, "Expected 3 steps for inline factory chain");
3222
3223 let step1 = &steps[0];
3225 assert_eq!(
3226 step1.object_type, None,
3227 "Step 1 (new): object_type should be None"
3228 );
3229 assert_eq!(
3230 step1.result_type,
3231 Some("Factory".to_string()),
3232 "Step 1 (new): result_type should be Factory"
3233 );
3234 assert!(
3235 step1.arguments.is_empty(),
3236 "Step 1 (new): arguments should be empty"
3237 );
3238 match &step1.target {
3239 ResolvedTarget::Function {
3240 contract_name,
3241 function_name,
3242 node_type,
3243 } => {
3244 assert_eq!(
3245 contract_name.as_deref(),
3246 Some("Factory"),
3247 "Step 1 (new): target contract"
3248 );
3249 assert_eq!(
3250 function_name, "Factory",
3251 "Step 1 (new): target function name (constructor)"
3252 );
3253 assert_eq!(
3254 *node_type,
3255 NodeType::Constructor,
3256 "Step 1 (new): target node type"
3257 );
3258 }
3259 _ => panic!(
3260 "Step 1 (new): Expected Function (Constructor), got {:?}",
3261 step1.target
3262 ),
3263 }
3264
3265 let step2 = &steps[1];
3267 assert_eq!(
3268 step2.object_type,
3269 Some("Factory".to_string()),
3270 "Step 2 (create): object_type should be Factory"
3271 );
3272 assert_eq!(
3273 step2.result_type,
3274 Some("IAction".to_string()),
3275 "Step 2 (create): result_type should be IAction"
3276 );
3277 assert!(
3278 step2.arguments.is_empty(),
3279 "Step 2 (create): arguments should be empty"
3280 );
3281 match &step2.target {
3282 ResolvedTarget::InterfaceMethod {
3284 interface_name,
3285 method_name,
3286 implementation,
3287 } => {
3288 assert_eq!(
3289 interface_name, "IFactory",
3290 "Step 2 (create): target interface name"
3291 );
3292 assert_eq!(method_name, "create", "Step 2 (create): target method name");
3293 assert!(
3294 implementation.is_some(),
3295 "Step 2 (create): implementation should be resolved"
3296 );
3297 match implementation.as_deref() {
3298 Some(ResolvedTarget::Function {
3299 contract_name,
3300 function_name,
3301 node_type,
3302 }) => {
3303 assert_eq!(
3304 contract_name.as_deref(),
3305 Some("Factory"),
3306 "Step 2 (create): impl contract"
3307 );
3308 assert_eq!(
3309 function_name, "create",
3310 "Step 2 (create): impl function name"
3311 );
3312 assert_eq!(
3313 *node_type,
3314 NodeType::Function,
3315 "Step 2 (create): impl node type"
3316 );
3317 }
3318 _ => panic!(
3319 "Step 2 (create): Expected implementation Function, got {:?}",
3320 implementation
3321 ),
3322 }
3323 }
3324 ResolvedTarget::Function {
3325 contract_name,
3326 function_name,
3327 node_type,
3328 } => {
3329 assert_eq!(
3331 contract_name.as_deref(),
3332 Some("Factory"),
3333 "Step 2 (create): target contract (direct)"
3334 );
3335 assert_eq!(
3336 function_name, "create",
3337 "Step 2 (create): target function name (direct)"
3338 );
3339 assert_eq!(
3340 *node_type,
3341 NodeType::Function,
3342 "Step 2 (create): target node type (direct)"
3343 );
3344 }
3345 _ => panic!(
3346 "Step 2 (create): Expected InterfaceMethod or Function, got {:?}",
3347 step2.target
3348 ),
3349 }
3350
3351 let step3 = &steps[2];
3353 assert_eq!(
3354 step3.object_type,
3355 Some("IAction".to_string()),
3356 "Step 3 (perform): object_type should be IAction"
3357 );
3358 assert_eq!(
3359 step3.result_type,
3360 Some("bool".to_string()),
3361 "Step 3 (perform): result_type should be bool"
3362 );
3363 assert!(
3364 step3.arguments.is_empty(),
3365 "Step 3 (perform): arguments should be empty"
3366 );
3367 match &step3.target {
3368 ResolvedTarget::InterfaceMethod {
3369 interface_name,
3370 method_name,
3371 implementation,
3372 } => {
3373 assert_eq!(
3374 interface_name, "IAction",
3375 "Step 3 (perform): target interface name"
3376 );
3377 assert_eq!(
3378 method_name, "perform",
3379 "Step 3 (perform): target method name"
3380 );
3381 assert!(
3382 implementation.is_some(),
3383 "Step 3 (perform): implementation should be resolved"
3384 );
3385 match implementation.as_deref() {
3386 Some(ResolvedTarget::Function {
3387 contract_name,
3388 function_name,
3389 node_type,
3390 }) => {
3391 assert_eq!(
3392 contract_name.as_deref(),
3393 Some("Action"),
3394 "Step 3 (perform): impl contract"
3395 );
3396 assert_eq!(
3397 function_name, "perform",
3398 "Step 3 (perform): impl function name"
3399 );
3400 assert_eq!(
3401 *node_type,
3402 NodeType::Function,
3403 "Step 3 (perform): impl node type"
3404 );
3405 }
3406 _ => panic!(
3407 "Step 3 (perform): Expected implementation Function, got {:?}",
3408 implementation
3409 ),
3410 }
3411 }
3412 _ => panic!(
3413 "Step 3 (perform): Expected InterfaceMethod, got {:?}",
3414 step3.target
3415 ),
3416 }
3417
3418 Ok(())
3419 }
3420
3421 #[test]
3422 fn test_analyze_constructor_call() -> Result<()> {
3423 let source = r#"
3425 contract Target { constructor(uint x) {} }
3426 contract Test {
3427 function caller() public {
3428 new Target(123); // Constructor call
3429 }
3430 }
3431 "#;
3432 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3433
3434 let caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")?; let caller_node_id = graph
3437 .node_lookup
3438 .get(&(Some("Test".to_string()), "caller".to_string()))
3439 .copied()
3440 .ok_or_else(|| anyhow::anyhow!("Caller node ID not found in graph"))?; let caller_def_node_for_search =
3445 find_function_definition_node_by_name(&tree, source, &lang, "caller")?;
3446 let call_expr_node = find_nth_descendant_node_of_kind(
3447 &caller_def_node_for_search, source,
3449 &lang,
3450 "call_expression", 0, )? .ok_or_else(|| { TypeError::Internal(format!(
3456 "Could not find the call_expression node for new Target(123) within caller function node: {:?}",
3457 caller_def_node.byte_range()
3458 ))
3459 })?; let steps = analyze_chained_call(
3466 call_expr_node, caller_node_id,
3468 &Some("Test".to_string()),
3469 &ctx,
3470 &graph,
3471 source,
3472 &lang,
3473 &input,
3474 None,
3475 call_expr_node.start_byte(), )?;
3477
3478 assert_eq!(steps.len(), 1);
3479 let step = &steps[0];
3480 assert_eq!(step.object_type, None);
3481 assert_eq!(step.result_type, Some("Target".to_string())); assert_eq!(step.arguments, vec!["123".to_string()]);
3483 match &step.target {
3484 ResolvedTarget::Function {
3485 contract_name,
3486 function_name,
3487 node_type,
3488 } => {
3489 assert_eq!(contract_name.as_deref(), Some("Target"));
3490 assert_eq!(function_name, "Target"); assert_eq!(*node_type, NodeType::Constructor);
3492 }
3493 _ => panic!(
3494 "Expected ResolvedTarget::Function (Constructor), got {:?}",
3495 step.target
3496 ),
3497 }
3498
3499 Ok(())
3500 }
3501
3502 #[test]
3503 fn test_analyze_type_cast_call() -> Result<()> {
3504 let source = r#"
3506 interface IAction { function perform() external; }
3507 contract Test {
3508 address actionAddr;
3509 function caller() public {
3510 IAction(actionAddr).perform(); // Type cast then call
3511 }
3512 }
3513 "#;
3514 let (ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3515 let _caller_def_node = find_function_definition_node_by_name(&tree, source, &lang, "caller")
3516 .expect("missing function definition");
3517 let caller_node_id = graph
3518 .node_lookup
3519 .get(&(Some("Test".to_string()), "caller".to_string()))
3520 .copied()
3521 .unwrap();
3522
3523 let call_expr_node = find_nth_node_of_kind(&tree, "call_expression", 0).unwrap(); let steps = analyze_chained_call(
3527 call_expr_node,
3528 caller_node_id,
3529 &Some("Test".to_string()),
3530 &ctx,
3531 &graph,
3532 source,
3533 &lang,
3534 &input,
3535 None,
3536 call_expr_node.start_byte(), )?;
3538
3539 assert_eq!(steps.len(), 1);
3542
3543 let step1 = &steps[0];
3545 assert_eq!(step1.object_type, Some("IAction".to_string())); assert_eq!(step1.result_type, None); assert!(step1.arguments.is_empty());
3548 match &step1.target {
3549 ResolvedTarget::InterfaceMethod {
3550 interface_name,
3551 method_name,
3552 implementation,
3553 } => {
3554 assert_eq!(interface_name, "IAction");
3555 assert_eq!(method_name, "perform");
3556 assert!(implementation.is_none());
3558 }
3559 _ => panic!("Step 1: Expected InterfaceMethod, got {:?}", step1.target),
3560 }
3561
3562 Ok(())
3563 }
3564
3565 fn default_natspec_text_range() -> NatspecTextRange {
3566 NatspecTextRange {
3567 start: NatspecTextIndex {
3568 utf8: 0,
3569 line: 0,
3570 column: 0,
3571 },
3572 end: NatspecTextIndex {
3573 utf8: 0,
3574 line: 0,
3575 column: 0,
3576 },
3577 }
3578 }
3579
3580 #[test]
3581 fn test_analyze_interface_call_fallback_to_binding_multiple_implementers() -> Result<()> {
3582 let source = r#"
3583 interface IMyService { function doThing() external returns (uint); }
3584 contract MyServiceImplA is IMyService { function doThing() external override returns (uint) { return 1; } }
3585 contract MyServiceImplB is IMyService { function doThing() external override returns (uint) { return 2; } }
3586
3587 contract Caller {
3588 IMyService serviceInstance;
3589 function callIt() public returns (uint) {
3590 return serviceInstance.doThing();
3591 }
3592 }
3593 "#;
3594 let (mut ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3595
3596 let manifest_entry = ManifestEntry {
3598 file_path: std::path::PathBuf::from("test.sol"),
3599 text: "/// @custom:binds-to MyService_BoundTo_A".to_string(),
3600 raw_comment_span: default_natspec_text_range(),
3601 item_kind: NatspecSourceItemKind::Interface,
3602 item_name: Some("IMyService".to_string()),
3603 item_span: default_natspec_text_range(), is_natspec: true,
3605 };
3606 let manifest = Manifest {
3607 entries: vec![manifest_entry],
3608 };
3609 ctx.manifest = Some(manifest);
3610
3611 let binding_config = BindingConfig {
3613 key: "MyService_BoundTo_A".to_string(),
3614 contract_name: Some("MyServiceImplA".to_string()),
3615 address: None,
3616 chain_id: None,
3617 notes: None,
3618 };
3619 let mut bindings_map = HashMap::new();
3620 bindings_map.insert("MyService_BoundTo_A".to_string(), binding_config);
3621 let binding_registry = BindingRegistry {
3622 bindings: bindings_map,
3623 };
3624 ctx.binding_registry = Some(binding_registry);
3625
3626 let caller_func_def_node =
3628 find_function_definition_node_by_name(&tree, source, &lang, "callIt")
3629 .expect("Could not find function definition node for callIt");
3630
3631 let caller_node_id = graph
3633 .node_lookup
3634 .get(&(Some("Caller".to_string()), "callIt".to_string()))
3635 .copied()
3636 .ok_or_else(|| anyhow::anyhow!("Node ID for Caller.callIt not found in graph"))?;
3637
3638 let call_expr_node = find_nth_descendant_node_of_kind(
3640 &caller_func_def_node,
3641 source,
3642 &lang,
3643 "call_expression",
3644 0, )?
3646 .ok_or_else(|| {
3647 TypeError::Internal(
3648 "call_expression node for serviceInstance.doThing() not found".to_string(),
3649 )
3650 })?;
3651
3652 let steps = analyze_chained_call(
3653 call_expr_node,
3654 caller_node_id,
3655 &Some("Caller".to_string()),
3656 &ctx,
3657 &graph,
3658 source,
3659 &lang,
3660 &input,
3661 None,
3662 call_expr_node.start_byte(),
3663 )?;
3664
3665 assert_eq!(steps.len(), 1, "Expected one step for the call");
3666 let step = &steps[0];
3667
3668 assert_eq!(step.object_type, Some("IMyService".to_string()));
3669 assert_eq!(step.result_type, Some("uint".to_string())); match &step.target {
3672 ResolvedTarget::InterfaceMethod {
3673 interface_name,
3674 method_name,
3675 implementation,
3676 } => {
3677 assert_eq!(interface_name, "IMyService");
3678 assert_eq!(method_name, "doThing");
3679 assert!(
3680 implementation.is_some(),
3681 "Implementation should be resolved via binding"
3682 );
3683
3684 match implementation.as_deref() {
3685 Some(ResolvedTarget::Function {
3686 contract_name,
3687 function_name,
3688 node_type,
3689 }) => {
3690 assert_eq!(contract_name.as_deref(), Some("MyServiceImplA"));
3691 assert_eq!(function_name, "doThing");
3692 assert_eq!(*node_type, NodeType::Function);
3693 }
3694 _ => panic!(
3695 "Expected implementation to be ResolvedTarget::Function, got {:?}",
3696 implementation
3697 ),
3698 }
3699 }
3700 _ => panic!(
3701 "Expected ResolvedTarget::InterfaceMethod, got {:?}",
3702 step.target
3703 ),
3704 }
3705 Ok(())
3706 }
3707
3708 #[test]
3709 fn test_analyze_interface_call_fallback_to_binding_zero_implementers() -> Result<()> {
3710 let source = r#"
3711 interface IExternalService { function performRemoteAction() external returns (bool); }
3712 // No direct implementers of IExternalService in this source
3713
3714 // This contract will be specified in bindings
3715 contract BoundRemoteImpl {
3716 function performRemoteAction() public pure returns (bool) { return true; }
3717 }
3718
3719 contract UserContract {
3720 IExternalService remoteService;
3721 function executeRemote() public returns (bool) {
3722 return remoteService.performRemoteAction();
3723 }
3724 }
3725 "#;
3726 let (mut ctx, graph, tree, lang, input) = setup_test_environment(source)?;
3727
3728 let manifest_entry = ManifestEntry {
3730 file_path: std::path::PathBuf::from("interfaces.sol"), text: "/// @custom:binds-to RemoteServiceKey".to_string(),
3732 raw_comment_span: default_natspec_text_range(),
3733 item_kind: NatspecSourceItemKind::Interface,
3734 item_name: Some("IExternalService".to_string()),
3735 item_span: default_natspec_text_range(),
3736 is_natspec: true,
3737 };
3738 let manifest = Manifest {
3739 entries: vec![manifest_entry],
3740 };
3741 ctx.manifest = Some(manifest);
3742
3743 let binding_config = BindingConfig {
3745 key: "RemoteServiceKey".to_string(),
3746 contract_name: Some("BoundRemoteImpl".to_string()),
3747 address: None,
3748 chain_id: None,
3749 notes: None,
3750 };
3751 let mut bindings_map = HashMap::new();
3752 bindings_map.insert("RemoteServiceKey".to_string(), binding_config);
3753 let binding_registry = BindingRegistry {
3754 bindings: bindings_map,
3755 };
3756 ctx.binding_registry = Some(binding_registry);
3757
3758 let caller_func_def_node =
3759 find_function_definition_node_by_name(&tree, source, &lang, "executeRemote")?;
3760 let caller_node_id = graph
3761 .node_lookup
3762 .get(&(
3763 Some("UserContract".to_string()),
3764 "executeRemote".to_string(),
3765 ))
3766 .copied()
3767 .ok_or_else(|| anyhow::anyhow!("Node ID for UserContract.executeRemote not found"))?;
3768
3769 let call_expr_node = find_nth_descendant_node_of_kind(
3770 &caller_func_def_node,
3771 source,
3772 &lang,
3773 "call_expression",
3774 0,
3775 )?
3776 .ok_or_else(|| {
3777 TypeError::Internal(
3778 "call_expression node for remoteService.performRemoteAction() not found"
3779 .to_string(),
3780 )
3781 })?;
3782
3783 let steps = analyze_chained_call(
3784 call_expr_node,
3785 caller_node_id,
3786 &Some("UserContract".to_string()),
3787 &ctx,
3788 &graph,
3789 source,
3790 &lang,
3791 &input,
3792 None,
3793 call_expr_node.start_byte(),
3794 )?;
3795
3796 assert_eq!(steps.len(), 1);
3797 let step = &steps[0];
3798 assert_eq!(step.object_type, Some("IExternalService".to_string()));
3799 assert_eq!(step.result_type, Some("bool".to_string())); match &step.target {
3802 ResolvedTarget::InterfaceMethod {
3803 interface_name,
3804 method_name,
3805 implementation,
3806 } => {
3807 assert_eq!(interface_name, "IExternalService");
3808 assert_eq!(method_name, "performRemoteAction");
3809 assert!(
3810 implementation.is_some(),
3811 "Implementation should be resolved via binding"
3812 );
3813
3814 match implementation.as_deref() {
3815 Some(ResolvedTarget::Function {
3816 contract_name,
3817 function_name,
3818 node_type,
3819 }) => {
3820 assert_eq!(contract_name.as_deref(), Some("BoundRemoteImpl"));
3821 assert_eq!(function_name, "performRemoteAction");
3822 assert_eq!(*node_type, NodeType::Function);
3823 }
3824 _ => panic!(
3825 "Expected implementation to be ResolvedTarget::Function, got {:?}",
3826 implementation
3827 ),
3828 }
3829 }
3830 _ => panic!(
3831 "Expected ResolvedTarget::InterfaceMethod, got {:?}",
3832 step.target
3833 ),
3834 }
3835 Ok(())
3836 }
3837
3838 #[test]
3839 fn test_analyze_interface_call_structural_typing_via_shared_key_no_inheritance() -> Result<()> {
3840 let source = r#"
3841 interface IExternal {
3842 /// @custom:binds-to ExternalKey
3843 function doExternalWork() external returns (string);
3844 }
3845
3846 /// @custom:binds-to ExternalKey
3847 contract ExternalContractImpl {
3848 // This contract does NOT explicitly implement IExternal
3849 function doExternalWork() public pure returns (string) {
3850 return "ExternalContractImpl.doExternalWork";
3851 }
3852 }
3853
3854 contract CallerContract {
3855 IExternal externalService;
3856
3857 function setService(address _serviceAddr) public {
3858 externalService = IExternal(_serviceAddr);
3859 }
3860
3861 function callExternalWork() public returns (string) {
3862 return externalService.doExternalWork(); // This should resolve to ExternalContractImpl.doExternalWork
3863 }
3864 }
3865 "#;
3866
3867 let mut manifest_entries = Vec::new();
3869
3870 manifest_entries.push(ManifestEntry {
3872 file_path: std::path::PathBuf::from("test.sol"),
3873 text: "/// @custom:binds-to ExternalKey".to_string(),
3874 raw_comment_span: default_natspec_text_range(), item_kind: NatspecSourceItemKind::Interface,
3876 item_name: Some("IExternal".to_string()),
3877 item_span: NatspecTextRange {
3878 start: NatspecTextIndex {
3880 utf8: 13,
3881 line: 1,
3882 column: 12,
3883 },
3884 end: NatspecTextIndex {
3885 utf8: 108,
3886 line: 4,
3887 column: 13,
3888 },
3889 },
3890 is_natspec: true,
3891 });
3892
3893 manifest_entries.push(ManifestEntry {
3895 file_path: std::path::PathBuf::from("test.sol"),
3896 text: "/// @custom:binds-to ExternalKey".to_string(),
3897 raw_comment_span: default_natspec_text_range(), item_kind: NatspecSourceItemKind::Contract,
3899 item_name: Some("ExternalContractImpl".to_string()),
3900 item_span: NatspecTextRange {
3901 start: NatspecTextIndex {
3903 utf8: 123,
3904 line: 6,
3905 column: 0,
3906 },
3907 end: NatspecTextIndex {
3908 utf8: 280,
3909 line: 11,
3910 column: 1,
3911 },
3912 },
3913 is_natspec: true,
3914 });
3915
3916 manifest_entries.push(ManifestEntry {
3918 file_path: std::path::PathBuf::from("test.sol"),
3919 text: "".to_string(), raw_comment_span: default_natspec_text_range(),
3921 item_kind: NatspecSourceItemKind::Function,
3922 item_name: Some("callExternalWork".to_string()),
3923 item_span: NatspecTextRange {
3924 start: NatspecTextIndex {
3926 utf8: 408,
3927 line: 19,
3928 column: 4,
3929 },
3930 end: NatspecTextIndex {
3931 utf8: 504,
3932 line: 21,
3933 column: 5,
3934 },
3935 },
3936 is_natspec: false,
3937 });
3938
3939 let manifest = Manifest {
3940 entries: manifest_entries,
3941 };
3942 let mut binding_registry = BindingRegistry::default();
3943
3944 binding_registry.populate_from_manifest(&manifest);
3946
3947 let binding_conf_after_populate = binding_registry.get_binding("ExternalKey");
3949 assert!(
3950 binding_conf_after_populate.is_some(),
3951 "BindingConfig for ExternalKey should exist after populate"
3952 );
3953 assert_eq!(
3954 binding_conf_after_populate.unwrap().contract_name,
3955 Some("ExternalContractImpl".to_string()),
3956 "BindingConfig for ExternalKey should have ExternalContractImpl as contract_name"
3957 );
3958
3959 let (ctx, graph, tree, lang, input) =
3960 setup_test_environment_customized(source, Some(manifest), Some(binding_registry))?;
3961
3962 let caller_func_def_node =
3964 find_function_definition_node_by_name(&tree, source, &lang, "callExternalWork")?;
3965
3966 let caller_node_id = graph
3968 .node_lookup
3969 .get(&(
3970 Some("CallerContract".to_string()),
3971 "callExternalWork".to_string(),
3972 ))
3973 .copied()
3974 .ok_or_else(|| {
3975 anyhow::anyhow!("Node ID for CallerContract.callExternalWork not found")
3976 })?;
3977
3978 let call_expr_node = find_nth_descendant_node_of_kind(
3980 &caller_func_def_node,
3981 source,
3982 &lang,
3983 "call_expression",
3984 0, )?
3986 .ok_or_else(|| {
3987 TypeError::Internal(
3988 "call_expression node for externalService.doExternalWork() not found".to_string(),
3989 )
3990 })?;
3991
3992 let steps = analyze_chained_call(
3993 call_expr_node,
3994 caller_node_id,
3995 &Some("CallerContract".to_string()),
3996 &ctx,
3997 &graph,
3998 source,
3999 &lang,
4000 &input,
4001 None, call_expr_node.start_byte(),
4003 )?;
4004
4005 assert_eq!(steps.len(), 1, "Expected one step for the call");
4006 let step = &steps[0];
4007
4008 assert_eq!(
4009 step.object_type,
4010 Some("IExternal".to_string()),
4011 "Object type should be IExternal"
4012 );
4013 assert_eq!(
4014 step.result_type,
4015 Some("string".to_string()),
4016 "Result type should be string (from ExternalContractImpl.doExternalWork)"
4017 );
4018
4019 match &step.target {
4020 ResolvedTarget::InterfaceMethod {
4021 interface_name,
4022 method_name,
4023 implementation,
4024 } => {
4025 assert_eq!(interface_name, "IExternal");
4026 assert_eq!(method_name, "doExternalWork");
4027 assert!(
4028 implementation.is_some(),
4029 "Implementation should be resolved via shared Natspec binding key"
4030 );
4031
4032 match implementation.as_deref() {
4033 Some(ResolvedTarget::Function {
4034 contract_name: impl_contract_name,
4035 function_name: impl_function_name,
4036 node_type,
4037 }) => {
4038 assert_eq!(
4039 impl_contract_name.as_deref(),
4040 Some("ExternalContractImpl"),
4041 "Implementation should be ExternalContractImpl"
4042 );
4043 assert_eq!(impl_function_name, "doExternalWork");
4044 assert_eq!(*node_type, NodeType::Function);
4045 }
4046 _ => panic!(
4047 "Expected implementation to be ResolvedTarget::Function, got {:?}",
4048 implementation
4049 ),
4050 }
4051 }
4052 _ => panic!(
4053 "Expected ResolvedTarget::InterfaceMethod, got {:?}",
4054 step.target
4055 ),
4056 }
4057 Ok(())
4058 }
4059
4060 #[test]
4061 fn test_analyze_interface_cast_call_structural_typing_shared_key() -> Result<()> {
4062 let source = r#"
4063 interface IERC20 {
4064 /// @custom:binds-to ERC20Key
4065 function balanceOf(address account) external view returns (uint256);
4066 }
4067
4068 /// @custom:binds-to ERC20Key
4069 contract MyToken {
4070 // This contract does NOT explicitly implement IERC20
4071 mapping(address => uint256) private _balances;
4072
4073 function balanceOf(address account) public view returns (uint256) {
4074 return _balances[account];
4075 }
4076 }
4077
4078 contract CallerContract {
4079 address token0; // Assume this is set to MyToken's address
4080
4081 constructor(address _token0) {
4082 token0 = _token0;
4083 }
4084
4085 function getMyBalance() public view returns (uint256) {
4086 // Call via type cast: InterfaceName(variable).method()
4087 return IERC20(token0).balanceOf(address(this));
4088 }
4089 }
4090 "#;
4091
4092 let mut manifest_entries = Vec::new();
4094
4095 manifest_entries.push(ManifestEntry {
4096 file_path: std::path::PathBuf::from("test.sol"),
4097 text: "/// @custom:binds-to ERC20Key".to_string(),
4098 raw_comment_span: default_natspec_text_range(),
4099 item_kind: NatspecSourceItemKind::Interface,
4100 item_name: Some("IERC20".to_string()),
4101 item_span: NatspecTextRange {
4102 start: NatspecTextIndex {
4104 utf8: 13,
4105 line: 1,
4106 column: 12,
4107 },
4108 end: NatspecTextIndex {
4109 utf8: 123,
4110 line: 4,
4111 column: 13,
4112 },
4113 },
4114 is_natspec: true,
4115 });
4116
4117 manifest_entries.push(ManifestEntry {
4118 file_path: std::path::PathBuf::from("test.sol"),
4119 text: "/// @custom:binds-to ERC20Key".to_string(),
4120 raw_comment_span: default_natspec_text_range(),
4121 item_kind: NatspecSourceItemKind::Contract,
4122 item_name: Some("MyToken".to_string()),
4123 item_span: NatspecTextRange {
4124 start: NatspecTextIndex {
4126 utf8: 138,
4127 line: 6,
4128 column: 0,
4129 },
4130 end: NatspecTextIndex {
4131 utf8: 330,
4132 line: 13,
4133 column: 1,
4134 },
4135 },
4136 is_natspec: true,
4137 });
4138
4139 manifest_entries.push(ManifestEntry {
4140 file_path: std::path::PathBuf::from("test.sol"),
4142 text: "".to_string(),
4143 raw_comment_span: default_natspec_text_range(),
4144 item_kind: NatspecSourceItemKind::Function,
4145 item_name: Some("getMyBalance".to_string()),
4146 item_span: NatspecTextRange {
4147 start: NatspecTextIndex {
4149 utf8: 450,
4150 line: 20,
4151 column: 4,
4152 },
4153 end: NatspecTextIndex {
4154 utf8: 590,
4155 line: 24,
4156 column: 5,
4157 },
4158 },
4159 is_natspec: false,
4160 });
4161
4162 let manifest = Manifest {
4163 entries: manifest_entries,
4164 };
4165 let mut binding_registry = BindingRegistry::default();
4166 binding_registry.populate_from_manifest(&manifest);
4167
4168 let binding_conf = binding_registry.get_binding("ERC20Key");
4170 assert!(
4171 binding_conf.is_some(),
4172 "BindingConfig for ERC20Key should exist"
4173 );
4174 assert_eq!(
4175 binding_conf.unwrap().contract_name,
4176 Some("MyToken".to_string()),
4177 "ERC20Key should bind to MyToken contract"
4178 );
4179
4180 let (ctx, graph, tree, lang, input) =
4181 setup_test_environment_customized(source, Some(manifest), Some(binding_registry))?;
4182
4183 let caller_func_def_node =
4184 find_function_definition_node_by_name(&tree, source, &lang, "getMyBalance")?;
4185 let caller_node_id = graph
4186 .node_lookup
4187 .get(&(
4188 Some("CallerContract".to_string()),
4189 "getMyBalance".to_string(),
4190 ))
4191 .copied()
4192 .ok_or_else(|| anyhow::anyhow!("Node ID for CallerContract.getMyBalance not found"))?;
4193
4194 let call_expr_node = find_nth_descendant_node_of_kind(
4197 &caller_func_def_node,
4198 source,
4199 &lang,
4200 "call_expression",
4201 0,
4202 )?
4203 .ok_or_else(|| {
4204 TypeError::Internal(
4205 "call_expression node for IERC20(token0).balanceOf(...) not found".to_string(),
4206 )
4207 })?;
4208
4209 let steps = analyze_chained_call(
4210 call_expr_node,
4211 caller_node_id,
4212 &Some("CallerContract".to_string()),
4213 &ctx,
4214 &graph,
4215 source,
4216 &lang,
4217 &input,
4218 None,
4219 call_expr_node.start_byte(),
4220 )?;
4221
4222 assert_eq!(steps.len(), 1, "Expected one step for the call");
4223 let step = &steps[0];
4224
4225 assert_eq!(
4226 step.object_type,
4227 Some("IERC20".to_string()),
4228 "Object type should be IERC20 (from type cast)"
4229 );
4230 assert_eq!(
4231 step.result_type,
4232 Some("uint256".to_string()),
4233 "Result type should be uint256 (from MyToken.balanceOf)"
4234 );
4235 assert_eq!(step.arguments.len(), 1, "Expected one argument");
4236 assert_eq!(
4237 step.arguments[0], "address(this)",
4238 "Argument should be address(this)"
4239 );
4240
4241 match &step.target {
4242 ResolvedTarget::InterfaceMethod {
4243 interface_name,
4244 method_name,
4245 implementation,
4246 } => {
4247 assert_eq!(interface_name, "IERC20");
4248 assert_eq!(method_name, "balanceOf");
4249 assert!(
4250 implementation.is_some(),
4251 "Implementation should be resolved via shared Natspec binding key"
4252 );
4253
4254 match implementation.as_deref() {
4255 Some(ResolvedTarget::Function {
4256 contract_name: impl_contract_name,
4257 function_name: impl_function_name,
4258 node_type,
4259 }) => {
4260 assert_eq!(
4261 impl_contract_name.as_deref(),
4262 Some("MyToken"),
4263 "Implementation should be MyToken"
4264 );
4265 assert_eq!(impl_function_name, "balanceOf");
4266 assert_eq!(*node_type, NodeType::Function);
4267 }
4268 _ => panic!(
4269 "Expected implementation to be ResolvedTarget::Function, got {:?}",
4270 implementation
4271 ),
4272 }
4273 }
4274 _ => panic!(
4275 "Expected ResolvedTarget::InterfaceMethod, got {:?}",
4276 step.target
4277 ),
4278 }
4279 Ok(())
4280 }
4281
4282 #[test]
4283 fn test_analyze_call_on_interface_method_to_mapping_getter_with_binding() -> Result<()> {
4284 let source = r#"
4285 interface IUniswapV2ERC20 {
4286 /// @custom:binds-to ERC20Impl
4287 function balanceOf(address account) external view returns (uint);
4288 }
4289
4290 /// @custom:binds-to ERC20Impl
4291 contract UniswapV2ERC20 { // Does not explicitly inherit
4292 mapping(address => uint) public balanceOf;
4293 // Other UniswapV2ERC20 members not needed for this specific test
4294 }
4295
4296 contract CallerContract {
4297 address token; // Changed from IUniswapV2ERC20 to address
4298
4299 constructor(address _tokenAddress) {
4300 token = _tokenAddress; // Store the address directly
4301 }
4302
4303 function getBalance(address who) public view returns (uint) {
4304 // Instantiate interface and call in one line
4305 return IUniswapV2ERC20(token).balanceOf(who);
4306 }
4307 }
4308 "#;
4309
4310 let mut manifest_entries = Vec::new();
4312 manifest_entries.push(ManifestEntry {
4313 file_path: std::path::PathBuf::from("test.sol"),
4314 text: "/// @custom:binds-to ERC20Impl".to_string(),
4315 raw_comment_span: default_natspec_text_range(),
4316 item_kind: NatspecSourceItemKind::Interface,
4317 item_name: Some("IUniswapV2ERC20".to_string()),
4318 item_span: default_natspec_text_range(),
4319 is_natspec: true,
4320 });
4321 manifest_entries.push(ManifestEntry {
4322 file_path: std::path::PathBuf::from("test.sol"),
4323 text: "/// @custom:binds-to ERC20Impl".to_string(),
4324 raw_comment_span: default_natspec_text_range(),
4325 item_kind: NatspecSourceItemKind::Contract,
4326 item_name: Some("UniswapV2ERC20".to_string()),
4327 item_span: default_natspec_text_range(),
4328 is_natspec: true,
4329 });
4330 manifest_entries.push(ManifestEntry {
4332 file_path: std::path::PathBuf::from("test.sol"),
4333 text: "".to_string(),
4334 raw_comment_span: default_natspec_text_range(),
4335 item_kind: NatspecSourceItemKind::Contract,
4336 item_name: Some("CallerContract".to_string()),
4337 item_span: default_natspec_text_range(),
4338 is_natspec: false,
4339 });
4340 manifest_entries.push(ManifestEntry {
4341 file_path: std::path::PathBuf::from("test.sol"),
4342 text: "".to_string(),
4343 raw_comment_span: default_natspec_text_range(),
4344 item_kind: NatspecSourceItemKind::Function,
4345 item_name: Some("getBalance".to_string()),
4346 item_span: default_natspec_text_range(),
4347 is_natspec: false,
4348 });
4349
4350 let manifest = Manifest {
4351 entries: manifest_entries,
4352 };
4353 let mut binding_registry = BindingRegistry::default();
4354 binding_registry.populate_from_manifest(&manifest);
4355
4356 let (ctx, graph, tree, lang, input) =
4357 setup_test_environment_customized(source, Some(manifest), Some(binding_registry))?;
4358
4359 let caller_func_def_node =
4360 find_function_definition_node_by_name(&tree, source, &lang, "getBalance")?;
4361 let caller_node_id = graph
4362 .node_lookup
4363 .get(&(Some("CallerContract".to_string()), "getBalance".to_string()))
4364 .copied()
4365 .ok_or_else(|| anyhow::anyhow!("Node ID for CallerContract.getBalance not found"))?;
4366
4367 let call_expr_node = find_nth_descendant_node_of_kind(
4369 &caller_func_def_node,
4370 source,
4371 &lang,
4372 "call_expression",
4373 0, )?
4375 .ok_or_else(|| {
4376 TypeError::Internal(
4377 "Could not find the call_expression for token.balanceOf(who)".to_string(),
4378 )
4379 })?;
4380
4381 let steps = analyze_chained_call(
4382 call_expr_node,
4383 caller_node_id,
4384 &Some("CallerContract".to_string()),
4385 &ctx,
4386 &graph,
4387 source,
4388 &lang,
4389 &input,
4390 None,
4391 call_expr_node.start_byte(),
4392 )?;
4393
4394 assert_eq!(steps.len(), 1, "Expected one step in the chain");
4395
4396 let step1 = &steps[0];
4397 trace!("Step 1: {:?}", step1);
4398 assert_eq!(
4399 step1.object_type,
4400 Some("IUniswapV2ERC20".to_string()),
4401 "Step 1 object_type"
4402 );
4403 assert_eq!(
4404 step1.object_instance_text,
4405 Some("IUniswapV2ERC20(token)".to_string()), "Step 1 object_instance_text"
4407 );
4408 assert_eq!(step1.arguments, vec!["who".to_string()], "Step 1 arguments");
4409 assert_eq!(
4410 step1.result_type,
4411 Some("uint".to_string()),
4412 "Step 1 result_type (must be uint from mapping)"
4413 );
4414
4415 match &step1.target {
4416 ResolvedTarget::InterfaceMethod {
4417 interface_name,
4418 method_name,
4419 implementation,
4420 } => {
4421 assert_eq!(
4422 interface_name, "IUniswapV2ERC20",
4423 "Step 1 target interface_name"
4424 );
4425 assert_eq!(method_name, "balanceOf", "Step 1 target method_name");
4426 assert!(
4427 implementation.is_some(),
4428 "Step 1 implementation should be resolved"
4429 );
4430
4431 match implementation.as_deref() {
4432 Some(ResolvedTarget::Function { contract_name, function_name, node_type }) => {
4433 assert_eq!(contract_name.as_deref(), Some("UniswapV2ERC20"), "Step 1 impl contract (concrete contract)");
4434 assert_eq!(function_name, "balanceOf", "Step 1 impl function (getter for mapping)");
4435 assert_eq!(*node_type, NodeType::Function, "Step 1 impl type (getter is a function)");
4436 }
4437 _ => panic!("Step 1: Expected implementation to be ResolvedTarget::Function for getter, got {:?}", implementation),
4438 }
4439 }
4440 _ => panic!(
4441 "Step 1: Expected ResolvedTarget::InterfaceMethod, got {:?}",
4442 step1.target
4443 ),
4444 }
4445
4446 Ok(())
4447 }
4448}