1use crate::goto::CachedBuild;
2use crate::types::{AbsPath, NodeId, SourceLoc};
3use serde_json::Value;
4use std::collections::HashMap;
5use tower_lsp::lsp_types::*;
6use tree_sitter::{Node, Parser};
7
8#[derive(Debug, Clone)]
10struct ParamInfo {
11 names: Vec<String>,
13 skip: usize,
15}
16
17#[derive(Debug, Clone)]
19struct CallSite {
20 info: ParamInfo,
22 name: String,
24 decl_id: NodeId,
26}
27
28#[derive(Debug, Clone)]
30pub struct ResolvedCallSite {
31 pub param_name: String,
33 pub decl_id: NodeId,
35}
36
37#[derive(Debug, Clone)]
40pub struct HintLookup {
41 by_offset: HashMap<usize, CallSite>,
43 by_name: HashMap<(String, usize), CallSite>,
45}
46
47impl HintLookup {
48 pub fn resolve_callsite_with_skip(
57 &self,
58 call_offset: usize,
59 func_name: &str,
60 arg_count: usize,
61 ) -> Option<(NodeId, usize)> {
62 if let Some(site) = lookup_call_site(self, call_offset, func_name, arg_count) {
64 return Some((site.decl_id, site.info.skip));
65 }
66 self.by_name
68 .iter()
69 .find(|((name, _), _)| name == func_name)
70 .map(|(_, site)| (site.decl_id, site.info.skip))
71 }
72
73 pub fn resolve_callsite_param(
79 &self,
80 call_offset: usize,
81 func_name: &str,
82 arg_count: usize,
83 arg_index: usize,
84 ) -> Option<ResolvedCallSite> {
85 let site = lookup_call_site(self, call_offset, func_name, arg_count)?;
86 let param_idx = arg_index + site.info.skip;
87 if param_idx >= site.info.names.len() {
88 return None;
89 }
90 let param_name = &site.info.names[param_idx];
91 if param_name.is_empty() {
92 return None;
93 }
94 Some(ResolvedCallSite {
95 param_name: param_name.clone(),
96 decl_id: site.decl_id,
97 })
98 }
99}
100
101pub type HintIndex = HashMap<AbsPath, HintLookup>;
104
105#[derive(Debug, Clone)]
109pub struct ConstructorInfo {
110 pub constructor_id: NodeId,
111 pub contract_name: String,
112 pub param_names: Vec<String>,
113}
114
115pub type ConstructorIndex = HashMap<NodeId, ConstructorInfo>;
117
118pub fn build_constructor_index(
122 decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
123) -> ConstructorIndex {
124 let mut index = HashMap::with_capacity(decl_index.len() / 8);
125 for (id, decl) in decl_index {
126 if decl.is_constructor()
127 && let Some(scope) = decl.scope()
128 {
129 let scope = NodeId(scope);
130 let names = decl.param_names().unwrap_or_default();
131 let contract_name = decl_index
133 .get(&scope)
134 .map(|d| d.name().to_string())
135 .unwrap_or_default();
136 index.insert(
137 scope,
138 ConstructorInfo {
139 constructor_id: *id,
140 contract_name,
141 param_names: names,
142 },
143 );
144 }
145 }
146 index
147}
148
149pub fn build_hint_index(
154 sources: &Value,
155 decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
156 constructor_index: &ConstructorIndex,
157) -> HintIndex {
158 let source_count = sources.as_object().map_or(0, |obj| obj.len());
159 let mut hint_index = HashMap::with_capacity(source_count);
160
161 if let Some(obj) = sources.as_object() {
162 for (_, source_data) in obj {
163 if let Some(ast) = source_data.get("ast")
164 && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
165 {
166 let lookup = build_hint_lookup(ast, decl_index, constructor_index);
167 hint_index.insert(AbsPath::new(abs_path), lookup);
168 }
169 }
170 }
171
172 hint_index
173}
174
175pub fn inlay_hints(
181 build: &CachedBuild,
182 uri: &Url,
183 range: Range,
184 live_source: &[u8],
185) -> Vec<InlayHint> {
186 let path_str = match uri.to_file_path() {
187 Ok(p) => p.to_str().unwrap_or("").to_string(),
188 Err(_) => return vec![],
189 };
190
191 let abs = match build
192 .path_to_abs
193 .iter()
194 .find(|(k, _)| path_str.ends_with(k.as_str()))
195 {
196 Some((_, v)) => v.clone(),
197 None => return vec![],
198 };
199
200 let lookup = match build.hint_index.get(&abs) {
202 Some(l) => l,
203 None => return vec![],
204 };
205
206 let source_str = String::from_utf8_lossy(live_source);
208 let tree = match ts_parse(&source_str) {
209 Some(t) => t,
210 None => return vec![],
211 };
212
213 let mut hints = Vec::new();
214 collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
215
216 hints
217}
218
219pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
221 let mut parser = Parser::new();
222 parser
223 .set_language(&tree_sitter_solidity::LANGUAGE.into())
224 .expect("failed to load Solidity grammar");
225 parser.parse(source, None)
226}
227
228fn build_hint_lookup(
230 file_ast: &Value,
231 decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
232 constructor_index: &ConstructorIndex,
233) -> HintLookup {
234 let mut lookup = HintLookup {
235 by_offset: HashMap::new(),
236 by_name: HashMap::new(),
237 };
238 collect_ast_calls(file_ast, decl_index, constructor_index, &mut lookup);
239 lookup
240}
241
242fn parse_src_offset(node: &Value) -> Option<usize> {
244 let src = node.get("src").and_then(|v| v.as_str())?;
245 SourceLoc::parse(src).map(|loc| loc.offset)
246}
247
248fn collect_ast_calls(
250 node: &Value,
251 decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
252 constructor_index: &ConstructorIndex,
253 lookup: &mut HintLookup,
254) {
255 let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
256
257 match node_type {
258 "FunctionCall" => {
259 if let Some(call_info) = extract_call_info(node, decl_index, constructor_index) {
260 let arg_count = node
261 .get("arguments")
262 .and_then(|v| v.as_array())
263 .map(|a| a.len())
264 .unwrap_or(0);
265 let site = CallSite {
266 info: ParamInfo {
267 names: call_info.params.names,
268 skip: call_info.params.skip,
269 },
270 name: call_info.name,
271 decl_id: call_info.decl_id,
272 };
273 if let Some(offset) = parse_src_offset(node) {
274 lookup.by_offset.insert(offset, site.clone());
275 }
276
277 lookup
278 .by_name
279 .entry((site.name.clone(), arg_count))
280 .or_insert(site);
281 }
282 }
283 "EmitStatement" => {
284 if let Some(event_call) = node.get("eventCall")
285 && let Some(call_info) =
286 extract_call_info(event_call, decl_index, constructor_index)
287 {
288 let arg_count = event_call
289 .get("arguments")
290 .and_then(|v| v.as_array())
291 .map(|a| a.len())
292 .unwrap_or(0);
293 let site = CallSite {
294 info: ParamInfo {
295 names: call_info.params.names,
296 skip: call_info.params.skip,
297 },
298 name: call_info.name,
299 decl_id: call_info.decl_id,
300 };
301 if let Some(offset) = parse_src_offset(node) {
302 lookup.by_offset.insert(offset, site.clone());
303 }
304
305 lookup
306 .by_name
307 .entry((site.name.clone(), arg_count))
308 .or_insert(site);
309 }
310 }
311 _ => {}
312 }
313
314 for key in crate::goto::CHILD_KEYS {
316 if let Some(child) = node.get(*key) {
317 if child.is_array() {
318 if let Some(arr) = child.as_array() {
319 for item in arr {
320 collect_ast_calls(item, decl_index, constructor_index, lookup);
321 }
322 }
323 } else if child.is_object() {
324 collect_ast_calls(child, decl_index, constructor_index, lookup);
325 }
326 }
327 }
328}
329
330struct CallInfo {
332 name: String,
334 params: ParamInfo,
336 decl_id: NodeId,
338}
339
340fn extract_call_info(
342 node: &Value,
343 decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
344 constructor_index: &ConstructorIndex,
345) -> Option<CallInfo> {
346 let args = node.get("arguments")?.as_array()?;
347 if args.is_empty() {
348 return None;
349 }
350
351 let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
353 if kind == "structConstructorCall"
354 && node
355 .get("names")
356 .and_then(|v| v.as_array())
357 .is_some_and(|n| !n.is_empty())
358 {
359 return None;
360 }
361
362 let expr = node.get("expression")?;
363 let expr_type = expr.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
364
365 if expr_type == "NewExpression" {
369 return extract_new_expression_call_info(expr, args.len(), constructor_index);
370 }
371
372 let decl_id = NodeId(expr.get("referencedDeclaration").and_then(|v| v.as_i64())?);
373
374 let decl = decl_index.get(&decl_id)?;
375 let names = decl.param_names()?;
376
377 let func_name = extract_function_name(expr)?;
379
380 let arg_count = node
385 .get("arguments")
386 .and_then(|v| v.as_array())
387 .map(|a| a.len())
388 .unwrap_or(0);
389 let skip = if is_member_access(expr) && arg_count < names.len() {
390 1
391 } else {
392 0
393 };
394
395 Some(CallInfo {
396 name: func_name,
397 params: ParamInfo { names, skip },
398 decl_id,
399 })
400}
401
402fn extract_new_expression_call_info(
409 new_expr: &Value,
410 _arg_count: usize,
411 constructor_index: &ConstructorIndex,
412) -> Option<CallInfo> {
413 let type_name = new_expr.get("typeName")?;
414 let contract_id = NodeId(
415 type_name
416 .get("referencedDeclaration")
417 .and_then(|v| v.as_i64())?,
418 );
419
420 let info = constructor_index.get(&contract_id)?;
421
422 let contract_name = type_name
424 .get("pathNode")
425 .and_then(|p| p.get("name"))
426 .and_then(|v| v.as_str())
427 .map(|s| s.to_string())
428 .unwrap_or_else(|| info.contract_name.clone());
429
430 Some(CallInfo {
431 name: contract_name,
432 params: ParamInfo {
433 names: info.param_names.clone(),
434 skip: 0,
435 },
436 decl_id: info.constructor_id,
437 })
438}
439
440fn extract_function_name(expr: &Value) -> Option<String> {
442 let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
443 match node_type {
444 "Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
445 "MemberAccess" => expr
446 .get("memberName")
447 .and_then(|v| v.as_str())
448 .map(String::from),
449 "NewExpression" => expr
450 .get("typeName")
451 .and_then(|t| {
452 t.get("pathNode")
453 .and_then(|p| p.get("name"))
454 .and_then(|v| v.as_str())
455 .or_else(|| t.get("name").and_then(|v| v.as_str()))
456 })
457 .map(String::from),
458 _ => None,
459 }
460}
461
462fn is_member_access(expr: &Value) -> bool {
464 expr.get("nodeType")
465 .and_then(|v| v.as_str())
466 .is_some_and(|t| t == "MemberAccess")
467}
468
469fn lookup_call_site<'a>(
473 lookup: &'a HintLookup,
474 offset: usize,
475 name: &str,
476 arg_count: usize,
477) -> Option<&'a CallSite> {
478 if let Some(site) = lookup.by_offset.get(&offset)
480 && site.name == name
481 {
482 return Some(site);
483 }
484 lookup.by_name.get(&(name.to_string(), arg_count))
486}
487
488fn collect_ts_hints(
490 node: Node,
491 source: &str,
492 range: &Range,
493 lookup: &HintLookup,
494 hints: &mut Vec<InlayHint>,
495) {
496 let node_start = node.start_position();
498 let node_end = node.end_position();
499 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
500 return;
501 }
502
503 match node.kind() {
504 "call_expression" => {
505 emit_call_hints(node, source, lookup, hints);
506 }
507 "emit_statement" => {
508 emit_emit_hints(node, source, lookup, hints);
509 }
510 _ => {}
511 }
512
513 let mut cursor = node.walk();
515 for child in node.children(&mut cursor) {
516 collect_ts_hints(child, source, range, lookup, hints);
517 }
518}
519
520fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
522 let func_name = match ts_call_function_name(node, source) {
523 Some(n) => n,
524 None => return,
525 };
526
527 let args = ts_call_arguments(node);
528 if args.is_empty() {
529 return;
530 }
531
532 let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
533 Some(s) => s,
534 None => return,
535 };
536
537 emit_param_hints(&args, &site.info, hints);
538}
539
540fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
542 let event_name = match ts_emit_event_name(node, source) {
543 Some(n) => n,
544 None => return,
545 };
546
547 let args = ts_call_arguments(node);
548 if args.is_empty() {
549 return;
550 }
551
552 let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
553 Some(s) => s,
554 None => return,
555 };
556
557 emit_param_hints(&args, &site.info, hints);
558}
559
560fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
562 for (i, arg) in args.iter().enumerate() {
563 let pi = i + info.skip;
564 if pi >= info.names.len() || info.names[pi].is_empty() {
565 continue;
566 }
567
568 let start = arg.start_position();
569 let position = Position::new(start.row as u32, start.column as u32);
570
571 hints.push(InlayHint {
572 position,
573 kind: Some(InlayHintKind::PARAMETER),
574 label: InlayHintLabel::String(format!("{}:", info.names[pi])),
575 text_edits: None,
576 tooltip: None,
577 padding_left: None,
578 padding_right: Some(true),
579 data: None,
580 });
581 }
582}
583
584fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
592 let func_expr = node.child_by_field_name("function")?;
593 let inner = first_named_child(func_expr)?;
595 extract_name_from_expr(inner, source)
596}
597
598fn extract_name_from_expr<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
605 match node.kind() {
606 "identifier" => Some(&source[node.byte_range()]),
607 "member_expression" => {
608 let prop = node.child_by_field_name("property")?;
609 Some(&source[prop.byte_range()])
610 }
611 "struct_expression" => {
612 let type_expr = node.child_by_field_name("type")?;
614 extract_name_from_expr(type_expr, source)
615 }
616 "new_expression" => {
617 ts_new_expression_name(node, source)
619 }
620 "expression" => {
621 let inner = first_named_child(node)?;
623 extract_name_from_expr(inner, source)
624 }
625 _ => None,
626 }
627}
628
629fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
631 let name_expr = node.child_by_field_name("name")?;
632 let inner = first_named_child(name_expr)?;
633 match inner.kind() {
634 "identifier" => Some(&source[inner.byte_range()]),
635 "member_expression" => {
636 let prop = inner.child_by_field_name("property")?;
637 Some(&source[prop.byte_range()])
638 }
639 _ => None,
640 }
641}
642
643fn ts_new_expression_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
648 let name_node = node.child_by_field_name("name")?;
649 if name_node.kind() == "user_defined_type" || name_node.kind() == "type_name" {
651 let mut cursor = name_node.walk();
653 for child in name_node.children(&mut cursor) {
654 if child.kind() == "identifier" {
655 return Some(&source[child.byte_range()]);
656 }
657 }
658 Some(&source[name_node.byte_range()])
660 } else {
661 Some(&source[name_node.byte_range()])
662 }
663}
664
665fn ts_call_arguments(node: Node) -> Vec<Node> {
668 let mut args = Vec::new();
669 let mut cursor = node.walk();
670 for child in node.children(&mut cursor) {
671 if child.kind() == "call_argument" {
672 args.push(child);
673 }
674 }
675 args
676}
677
678fn first_named_child(node: Node) -> Option<Node> {
680 let mut cursor = node.walk();
681 node.children(&mut cursor).find(|c| c.is_named())
682}
683
684pub struct TsCallContext<'a> {
686 pub name: &'a str,
688 pub arg_index: usize,
690 pub arg_count: usize,
692 pub call_start_byte: usize,
694 pub is_index_access: bool,
697}
698
699pub fn ts_find_call_at_byte<'a>(
704 root: tree_sitter::Node<'a>,
705 source: &'a str,
706 byte_pos: usize,
707) -> Option<TsCallContext<'a>> {
708 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
710
711 loop {
713 if node.kind() == "call_argument" {
714 break;
715 }
716 node = node.parent()?;
717 }
718
719 let call_node = node.parent()?;
721 let args = ts_call_arguments(call_node);
722 let arg_index = args.iter().position(|a| a.id() == node.id())?;
723
724 match call_node.kind() {
725 "call_expression" => {
726 let name = ts_call_function_name(call_node, source)?;
727 Some(TsCallContext {
728 name,
729 arg_index,
730 arg_count: args.len(),
731 call_start_byte: call_node.start_byte(),
732 is_index_access: false,
733 })
734 }
735 "emit_statement" => {
736 let name = ts_emit_event_name(call_node, source)?;
737 Some(TsCallContext {
738 name,
739 arg_index,
740 arg_count: args.len(),
741 call_start_byte: call_node.start_byte(),
742 is_index_access: false,
743 })
744 }
745 _ => None,
746 }
747}
748
749pub fn ts_find_call_for_signature<'a>(
759 root: tree_sitter::Node<'a>,
760 source: &'a str,
761 byte_pos: usize,
762) -> Option<TsCallContext<'a>> {
763 if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
765 return Some(ctx);
766 }
767
768 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
770 loop {
771 match node.kind() {
772 "call_expression" => {
773 let name = ts_call_function_name(node, source)?;
774 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
775 let args = ts_call_arguments(node);
776 let arg_count = args.len().max(arg_index + 1);
777 return Some(TsCallContext {
778 name,
779 arg_index,
780 arg_count,
781 call_start_byte: node.start_byte(),
782 is_index_access: false,
783 });
784 }
785 "emit_statement" => {
786 let name = ts_emit_event_name(node, source)?;
787 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
788 let args = ts_call_arguments(node);
789 let arg_count = args.len().max(arg_index + 1);
790 return Some(TsCallContext {
791 name,
792 arg_index,
793 arg_count,
794 call_start_byte: node.start_byte(),
795 is_index_access: false,
796 });
797 }
798 "array_access" => {
799 let base_node = node.child_by_field_name("base")?;
801 let name_node = if base_node.kind() == "member_expression" {
804 base_node
805 .child_by_field_name("property")
806 .unwrap_or(base_node)
807 } else {
808 base_node
809 };
810 let name = &source[name_node.byte_range()];
811 return Some(TsCallContext {
812 name,
813 arg_index: 0,
814 arg_count: 1,
815 call_start_byte: node.start_byte(),
816 is_index_access: true,
817 });
818 }
819 "source_file" => break,
820 _ => {
821 node = node.parent()?;
822 }
823 }
824 }
825
826 if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
828 return Some(ctx);
829 }
830
831 find_index_by_text_scan(source, byte_pos)
833}
834
835fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
841 let before = &source[..byte_pos.min(source.len())];
842
843 let mut depth: i32 = 0;
845 let mut paren_pos = None;
846 for (i, ch) in before.char_indices().rev() {
847 match ch {
848 ')' => depth += 1,
849 '(' => {
850 if depth == 0 {
851 paren_pos = Some(i);
852 break;
853 }
854 depth -= 1;
855 }
856 _ => {}
857 }
858 }
859 let paren_pos = paren_pos?;
860
861 let mut scan_end = paren_pos;
865 let before_paren = source[..scan_end].trim_end();
866 if before_paren.ends_with('}') {
867 let mut brace_depth: i32 = 0;
869 for (i, ch) in before_paren.char_indices().rev() {
870 match ch {
871 '}' => brace_depth += 1,
872 '{' => {
873 brace_depth -= 1;
874 if brace_depth == 0 {
875 scan_end = i;
876 break;
877 }
878 }
879 _ => {}
880 }
881 }
882 }
883 let before_name = &source[..scan_end];
884 let name_end = before_name.trim_end().len();
885 let name_start = before_name[..name_end]
886 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
887 .map(|i| i + 1)
888 .unwrap_or(0);
889 let raw_name = &source[name_start..name_end];
891 let name = match raw_name.rfind('.') {
892 Some(dot) => &raw_name[dot + 1..],
893 None => raw_name,
894 };
895
896 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
897 return None;
898 }
899
900 let arg_index = count_commas_before(source, paren_pos, byte_pos);
902
903 Some(TsCallContext {
904 name,
905 arg_index,
906 arg_count: arg_index + 1,
907 call_start_byte: name_start,
908 is_index_access: false,
909 })
910}
911
912fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
917 let before = &source[..byte_pos.min(source.len())];
918
919 let mut depth: i32 = 0;
921 let mut bracket_pos = None;
922 for (i, c) in before.char_indices().rev() {
923 match c {
924 ']' => depth += 1,
925 '[' => {
926 if depth == 0 {
927 bracket_pos = Some(i);
928 break;
929 }
930 depth -= 1;
931 }
932 _ => {}
933 }
934 }
935 let bracket_pos = bracket_pos?;
936
937 let before_bracket = &source[..bracket_pos];
939 let name_end = before_bracket.trim_end().len();
940 let name_start = before_bracket[..name_end]
941 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
942 .map(|i| i + 1)
943 .unwrap_or(0);
944 let name = &source[name_start..name_end];
945
946 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
947 return None;
948 }
949
950 Some(TsCallContext {
951 name,
952 arg_index: 0,
953 arg_count: 1,
954 call_start_byte: name_start,
955 is_index_access: true,
956 })
957}
958
959fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
961 let end = byte_pos.min(source.len());
962 let text = &source[start..end];
963
964 let mut count = 0;
965 let mut depth = 0;
966 let mut found_open = false;
967 for ch in text.chars() {
968 match ch {
969 '(' if !found_open => {
970 found_open = true;
971 depth = 1;
972 }
973 '(' => depth += 1,
974 ')' => depth -= 1,
975 ',' if found_open && depth == 1 => count += 1,
976 _ => {}
977 }
978 }
979 count
980}
981
982#[cfg(test)]
985mod tests {
986 use super::*;
987
988 #[test]
989 fn test_ts_call_function_name() {
990 let source = r#"
991contract Foo {
992 function bar(uint x) public {}
993 function test() public {
994 bar(42);
995 }
996}
997"#;
998 let tree = ts_parse(source).unwrap();
999 let mut found = Vec::new();
1000 find_calls(tree.root_node(), source, &mut found);
1001 assert_eq!(found.len(), 1);
1002 assert_eq!(found[0], "bar");
1003 }
1004
1005 #[test]
1006 fn test_ts_member_call_name() {
1007 let source = r#"
1008contract Foo {
1009 function test() public {
1010 PRICE.addTax(TAX, TAX_BASE);
1011 }
1012}
1013"#;
1014 let tree = ts_parse(source).unwrap();
1015 let mut found = Vec::new();
1016 find_calls(tree.root_node(), source, &mut found);
1017 assert_eq!(found.len(), 1);
1018 assert_eq!(found[0], "addTax");
1019 }
1020
1021 #[test]
1022 fn test_ts_call_with_value_modifier() {
1023 let source = r#"
1024contract Foo {
1025 function test() public {
1026 router.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
1027 }
1028}
1029"#;
1030 let tree = ts_parse(source).unwrap();
1031 let mut found = Vec::new();
1032 find_calls(tree.root_node(), source, &mut found);
1033 assert_eq!(found.len(), 1, "should find one call");
1034 assert_eq!(
1035 found[0], "swap",
1036 "should extract 'swap' through struct_expression"
1037 );
1038 }
1039
1040 #[test]
1041 fn test_ts_call_simple_with_value_modifier() {
1042 let source = r#"
1043contract Foo {
1044 function test() public {
1045 foo{value: 1 ether}(42);
1046 }
1047}
1048"#;
1049 let tree = ts_parse(source).unwrap();
1050 let mut found = Vec::new();
1051 find_calls(tree.root_node(), source, &mut found);
1052 assert_eq!(found.len(), 1, "should find one call");
1053 assert_eq!(
1054 found[0], "foo",
1055 "should extract 'foo' through struct_expression"
1056 );
1057 }
1058
1059 #[test]
1060 fn test_ts_call_with_gas_modifier() {
1061 let source = r#"
1062contract Foo {
1063 function test() public {
1064 addr.call{gas: 5000, value: 1 ether}("");
1065 }
1066}
1067"#;
1068 let tree = ts_parse(source).unwrap();
1069 let mut found = Vec::new();
1070 find_calls(tree.root_node(), source, &mut found);
1071 assert_eq!(found.len(), 1, "should find one call");
1072 assert_eq!(
1073 found[0], "call",
1074 "should extract 'call' through struct_expression"
1075 );
1076 }
1077
1078 #[test]
1079 fn test_find_call_by_text_scan_with_value_modifier() {
1080 let source = "router.swap{value: 100}(nativeKey, SWAP_PARAMS)";
1082 let byte_pos = source.find("SWAP_PARAMS").unwrap();
1084 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1085 assert_eq!(ctx.name, "swap");
1086 assert_eq!(ctx.arg_index, 1);
1087 }
1088
1089 #[test]
1090 fn test_find_call_by_text_scan_simple_value_modifier() {
1091 let source = "foo{value: 1 ether}(42)";
1092 let byte_pos = source.find("42").unwrap();
1093 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1094 assert_eq!(ctx.name, "foo");
1095 assert_eq!(ctx.arg_index, 0);
1096 }
1097
1098 #[test]
1099 fn test_ts_emit_event_name() {
1100 let source = r#"
1101contract Foo {
1102 event Purchase(address buyer, uint256 price);
1103 function test() public {
1104 emit Purchase(msg.sender, 100);
1105 }
1106}
1107"#;
1108 let tree = ts_parse(source).unwrap();
1109 let mut found = Vec::new();
1110 find_emits(tree.root_node(), source, &mut found);
1111 assert_eq!(found.len(), 1);
1112 assert_eq!(found[0], "Purchase");
1113 }
1114
1115 #[test]
1116 fn test_ts_new_expression_name() {
1117 let source = r#"
1118contract Token {
1119 constructor(string memory _name, uint256 _supply) {}
1120}
1121contract Factory {
1122 function create() public {
1123 Token t = new Token("MyToken", 1000);
1124 }
1125}
1126"#;
1127 let tree = ts_parse(source).unwrap();
1128 let mut found = Vec::new();
1129 find_new_exprs(tree.root_node(), source, &mut found);
1130 assert_eq!(found.len(), 1);
1131 assert_eq!(found[0], "Token");
1132 }
1133
1134 #[test]
1135 fn test_ts_new_expression_arguments() {
1136 let source = r#"
1140contract Router {
1141 constructor(address _manager, address _hook) {}
1142}
1143contract Factory {
1144 function create() public {
1145 Router r = new Router(address(this), address(0));
1146 }
1147}
1148"#;
1149 let tree = ts_parse(source).unwrap();
1150 let mut arg_counts = Vec::new();
1151 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1152 assert_eq!(arg_counts, vec![2]);
1154 }
1155
1156 #[test]
1157 fn test_ts_find_call_at_byte_new_expression() {
1158 let source = r#"
1159contract Token {
1160 constructor(string memory _name, uint256 _supply) {}
1161}
1162contract Factory {
1163 function create() public {
1164 Token t = new Token("MyToken", 1000);
1165 }
1166}
1167"#;
1168 let tree = ts_parse(source).unwrap();
1169 let pos = source.find("1000").unwrap();
1170 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1171 assert_eq!(ctx.name, "Token");
1172 assert_eq!(ctx.arg_index, 1);
1173 assert_eq!(ctx.arg_count, 2);
1174 }
1175
1176 #[test]
1177 fn test_ts_find_call_at_byte_new_expression_first_arg() {
1178 let source = r#"
1179contract Token {
1180 constructor(string memory _name, uint256 _supply) {}
1181}
1182contract Factory {
1183 function create() public {
1184 Token t = new Token("MyToken", 1000);
1185 }
1186}
1187"#;
1188 let tree = ts_parse(source).unwrap();
1189 let pos = source.find("\"MyToken\"").unwrap();
1190 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1191 assert_eq!(ctx.name, "Token");
1192 assert_eq!(ctx.arg_index, 0);
1193 assert_eq!(ctx.arg_count, 2);
1194 }
1195
1196 #[test]
1197 fn test_extract_new_expression_call_info() {
1198 let new_expr: Value = serde_json::json!({
1200 "nodeType": "NewExpression",
1201 "typeName": {
1202 "nodeType": "UserDefinedTypeName",
1203 "referencedDeclaration": 22,
1204 "pathNode": {
1205 "name": "Token"
1206 }
1207 }
1208 });
1209
1210 let mut constructor_index = ConstructorIndex::new();
1211 constructor_index.insert(
1212 NodeId(22),
1213 ConstructorInfo {
1214 constructor_id: NodeId(21),
1215 contract_name: "Token".to_string(),
1216 param_names: vec!["_name".to_string(), "_supply".to_string()],
1217 },
1218 );
1219
1220 let info = extract_new_expression_call_info(&new_expr, 2, &constructor_index).unwrap();
1221 assert_eq!(info.name, "Token");
1222 assert_eq!(info.params.names, vec!["_name", "_supply"]);
1223 assert_eq!(info.params.skip, 0);
1224 assert_eq!(info.decl_id, NodeId(21));
1225 }
1226
1227 #[test]
1228 fn test_ts_call_arguments_count() {
1229 let source = r#"
1230contract Foo {
1231 function bar(uint x, uint y) public {}
1232 function test() public {
1233 bar(1, 2);
1234 }
1235}
1236"#;
1237 let tree = ts_parse(source).unwrap();
1238 let mut arg_counts = Vec::new();
1239 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1240 assert_eq!(arg_counts, vec![2]);
1241 }
1242
1243 #[test]
1244 fn test_ts_argument_positions_follow_live_buffer() {
1245 let source = r#"
1247contract Foo {
1248 function bar(uint x, uint y) public {}
1249 function test() public {
1250 bar(
1251 1,
1252 2
1253 );
1254 }
1255}
1256"#;
1257 let tree = ts_parse(source).unwrap();
1258 let mut positions = Vec::new();
1259 find_arg_positions(tree.root_node(), &mut positions);
1260 assert_eq!(positions.len(), 2);
1262 assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
1265
1266 fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1269 if node.kind() == "call_expression"
1270 && let Some(name) = ts_call_function_name(node, source)
1271 {
1272 out.push(name);
1273 }
1274 let mut cursor = node.walk();
1275 for child in node.children(&mut cursor) {
1276 find_calls(child, source, out);
1277 }
1278 }
1279
1280 fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1281 if node.kind() == "emit_statement"
1282 && let Some(name) = ts_emit_event_name(node, source)
1283 {
1284 out.push(name);
1285 }
1286 let mut cursor = node.walk();
1287 for child in node.children(&mut cursor) {
1288 find_emits(child, source, out);
1289 }
1290 }
1291
1292 fn find_new_exprs<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1293 if node.kind() == "new_expression"
1294 && let Some(name) = ts_new_expression_name(node, source)
1295 {
1296 out.push(name);
1297 }
1298 let mut cursor = node.walk();
1299 for child in node.children(&mut cursor) {
1300 find_new_exprs(child, source, out);
1301 }
1302 }
1303
1304 fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
1305 if node.kind() == "call_expression" {
1306 out.push(ts_call_arguments(node).len());
1307 }
1308 let mut cursor = node.walk();
1309 for child in node.children(&mut cursor) {
1310 find_call_arg_counts(child, out);
1311 }
1312 }
1313
1314 fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
1315 if node.kind() == "call_expression" {
1316 for arg in ts_call_arguments(node) {
1317 let p = arg.start_position();
1318 out.push((p.row, p.column));
1319 }
1320 }
1321 let mut cursor = node.walk();
1322 for child in node.children(&mut cursor) {
1323 find_arg_positions(child, out);
1324 }
1325 }
1326
1327 #[test]
1328 fn test_ts_find_call_at_byte_first_arg() {
1329 let source = r#"
1330contract Foo {
1331 function bar(uint x, uint y) public {}
1332 function test() public {
1333 bar(42, 99);
1334 }
1335}
1336"#;
1337 let tree = ts_parse(source).unwrap();
1338 let pos_42 = source.find("42").unwrap();
1340 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1341 assert_eq!(ctx.name, "bar");
1342 assert_eq!(ctx.arg_index, 0);
1343 assert_eq!(ctx.arg_count, 2);
1344 }
1345
1346 #[test]
1347 fn test_ts_find_call_at_byte_second_arg() {
1348 let source = r#"
1349contract Foo {
1350 function bar(uint x, uint y) public {}
1351 function test() public {
1352 bar(42, 99);
1353 }
1354}
1355"#;
1356 let tree = ts_parse(source).unwrap();
1357 let pos_99 = source.find("99").unwrap();
1358 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1359 assert_eq!(ctx.name, "bar");
1360 assert_eq!(ctx.arg_index, 1);
1361 assert_eq!(ctx.arg_count, 2);
1362 }
1363
1364 #[test]
1365 fn test_ts_find_call_at_byte_outside_call_returns_none() {
1366 let source = r#"
1367contract Foo {
1368 function bar(uint x) public {}
1369 function test() public {
1370 uint z = 10;
1371 bar(42);
1372 }
1373}
1374"#;
1375 let tree = ts_parse(source).unwrap();
1376 let pos_10 = source.find("10").unwrap();
1378 assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1379 }
1380
1381 #[test]
1382 fn test_ts_find_call_at_byte_member_call() {
1383 let source = r#"
1384contract Foo {
1385 function test() public {
1386 PRICE.addTax(TAX, TAX_BASE);
1387 }
1388}
1389"#;
1390 let tree = ts_parse(source).unwrap();
1391 let pos_tax = source.find("TAX,").unwrap();
1392 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1393 assert_eq!(ctx.name, "addTax");
1394 assert_eq!(ctx.arg_index, 0);
1395 assert_eq!(ctx.arg_count, 2);
1396 }
1397
1398 #[test]
1399 fn test_ts_find_call_at_byte_emit_statement() {
1400 let source = r#"
1401contract Foo {
1402 event Purchase(address buyer, uint256 price);
1403 function test() public {
1404 emit Purchase(msg.sender, 100);
1405 }
1406}
1407"#;
1408 let tree = ts_parse(source).unwrap();
1409 let pos_100 = source.find("100").unwrap();
1410 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1411 assert_eq!(ctx.name, "Purchase");
1412 assert_eq!(ctx.arg_index, 1);
1413 assert_eq!(ctx.arg_count, 2);
1414 }
1415
1416 #[test]
1417 fn test_ts_find_call_at_byte_multiline() {
1418 let source = r#"
1419contract Foo {
1420 function bar(uint x, uint y, uint z) public {}
1421 function test() public {
1422 bar(
1423 1,
1424 2,
1425 3
1426 );
1427 }
1428}
1429"#;
1430 let tree = ts_parse(source).unwrap();
1431 let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1435 assert_eq!(ctx.name, "bar");
1436 assert_eq!(ctx.arg_index, 1);
1437 assert_eq!(ctx.arg_count, 3);
1438 }
1439
1440 #[test]
1441 fn test_resolve_callsite_param_basic() {
1442 let mut lookup = HintLookup {
1444 by_offset: HashMap::new(),
1445 by_name: HashMap::new(),
1446 };
1447 lookup.by_name.insert(
1448 ("transfer".to_string(), 2),
1449 CallSite {
1450 info: ParamInfo {
1451 names: vec!["to".to_string(), "amount".to_string()],
1452 skip: 0,
1453 },
1454 name: "transfer".to_string(),
1455 decl_id: NodeId(42),
1456 },
1457 );
1458
1459 let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1461 assert_eq!(result.param_name, "to");
1462 assert_eq!(result.decl_id, NodeId(42));
1463
1464 let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1466 assert_eq!(result.param_name, "amount");
1467 assert_eq!(result.decl_id, NodeId(42));
1468 }
1469
1470 #[test]
1471 fn test_resolve_callsite_param_with_skip() {
1472 let mut lookup = HintLookup {
1474 by_offset: HashMap::new(),
1475 by_name: HashMap::new(),
1476 };
1477 lookup.by_name.insert(
1478 ("addTax".to_string(), 2),
1479 CallSite {
1480 info: ParamInfo {
1481 names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1482 skip: 1,
1483 },
1484 name: "addTax".to_string(),
1485 decl_id: NodeId(99),
1486 },
1487 );
1488
1489 let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1491 assert_eq!(result.param_name, "tax");
1492
1493 let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1495 assert_eq!(result.param_name, "base");
1496 }
1497
1498 #[test]
1499 fn test_resolve_callsite_param_out_of_bounds() {
1500 let mut lookup = HintLookup {
1501 by_offset: HashMap::new(),
1502 by_name: HashMap::new(),
1503 };
1504 lookup.by_name.insert(
1505 ("foo".to_string(), 1),
1506 CallSite {
1507 info: ParamInfo {
1508 names: vec!["x".to_string()],
1509 skip: 0,
1510 },
1511 name: "foo".to_string(),
1512 decl_id: NodeId(1),
1513 },
1514 );
1515
1516 assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1518 }
1519
1520 #[test]
1521 fn test_resolve_callsite_param_unknown_function() {
1522 let lookup = HintLookup {
1523 by_offset: HashMap::new(),
1524 by_name: HashMap::new(),
1525 };
1526 assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1527 }
1528
1529 #[test]
1530 fn test_ts_find_call_at_byte_emit_member_access() {
1531 let source = r#"
1535contract Foo {
1536 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1537 function test() public {
1538 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1539 }
1540}
1541"#;
1542 let tree = ts_parse(source).unwrap();
1543 let params_tick = source.find("params.tickLower,").unwrap();
1545 let tick_lower_pos = params_tick + "params.".len(); let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1548 assert_eq!(ctx.name, "ModifyLiquidity");
1549 assert_eq!(
1550 ctx.arg_index, 2,
1551 "params.tickLower is the 3rd argument (index 2)"
1552 );
1553 assert_eq!(ctx.arg_count, 4);
1554 }
1555
1556 #[test]
1557 fn test_ts_find_call_at_byte_member_access_on_property() {
1558 let source = r#"
1560contract Foo {
1561 event Transfer(address from, address to);
1562 function test() public {
1563 emit Transfer(msg.sender, addr);
1564 }
1565}
1566"#;
1567 let tree = ts_parse(source).unwrap();
1568 let sender_pos = source.find("sender").unwrap();
1569 let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1570 assert_eq!(ctx.name, "Transfer");
1571 assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1572 }
1573
1574 #[test]
1575 fn test_ts_find_call_at_byte_emit_all_args() {
1576 let source = r#"
1578contract Foo {
1579 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1580 function test() public {
1581 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1582 }
1583}
1584"#;
1585 let tree = ts_parse(source).unwrap();
1586
1587 let pos_id = source.find("(id,").unwrap() + 1;
1589 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1590 assert_eq!(ctx.name, "ModifyLiquidity");
1591 assert_eq!(ctx.arg_index, 0);
1592
1593 let pos_msg = source.find("msg.sender").unwrap();
1595 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1596 assert_eq!(ctx.arg_index, 1);
1597
1598 let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1600 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1601 assert_eq!(ctx.arg_index, 2);
1602
1603 let pos_tu = source.find("params.tickUpper").unwrap();
1605 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1606 assert_eq!(ctx.arg_index, 3);
1607 }
1608
1609 #[test]
1610 fn test_ts_find_call_at_byte_nested_call_arg() {
1611 let source = r#"
1614contract Foo {
1615 function inner(uint x) public returns (uint) {}
1616 function outer(uint a, uint b) public {}
1617 function test() public {
1618 outer(inner(42), 99);
1619 }
1620}
1621"#;
1622 let tree = ts_parse(source).unwrap();
1623
1624 let pos_42 = source.find("42").unwrap();
1626 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1627 assert_eq!(ctx.name, "inner");
1628 assert_eq!(ctx.arg_index, 0);
1629
1630 let pos_99 = source.find("99").unwrap();
1632 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1633 assert_eq!(ctx.name, "outer");
1634 assert_eq!(ctx.arg_index, 1);
1635 }
1636
1637 #[test]
1638 fn test_ts_find_call_for_signature_incomplete_call() {
1639 let source = r#"
1641contract Foo {
1642 function bar(uint x, uint y) public {}
1643 function test() public {
1644 bar(
1645 }
1646}
1647"#;
1648 let tree = ts_parse(source).unwrap();
1649 let pos = source.find("bar(").unwrap() + 4;
1650 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1651 assert_eq!(ctx.name, "bar");
1652 assert_eq!(ctx.arg_index, 0);
1653 }
1654
1655 #[test]
1656 fn test_ts_find_call_for_signature_after_comma() {
1657 let source = r#"
1659contract Foo {
1660 function bar(uint x, uint y) public {}
1661 function test() public {
1662 bar(42,
1663 }
1664}
1665"#;
1666 let tree = ts_parse(source).unwrap();
1667 let pos = source.find("42,").unwrap() + 3;
1668 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1669 assert_eq!(ctx.name, "bar");
1670 assert_eq!(ctx.arg_index, 1);
1671 }
1672
1673 #[test]
1674 fn test_ts_find_call_for_signature_complete_call() {
1675 let source = r#"
1677contract Foo {
1678 function bar(uint x, uint y) public {}
1679 function test() public {
1680 bar(42, 99);
1681 }
1682}
1683"#;
1684 let tree = ts_parse(source).unwrap();
1685 let pos = source.find("42").unwrap();
1686 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1687 assert_eq!(ctx.name, "bar");
1688 assert_eq!(ctx.arg_index, 0);
1689 }
1690
1691 #[test]
1692 fn test_ts_find_call_for_signature_member_call() {
1693 let source = r#"
1695contract Foo {
1696 function test() public {
1697 PRICE.addTax(
1698 }
1699}
1700"#;
1701 let tree = ts_parse(source).unwrap();
1702 let pos = source.find("addTax(").unwrap() + 7;
1703 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1704 assert_eq!(ctx.name, "addTax");
1705 assert_eq!(ctx.arg_index, 0);
1706 }
1707
1708 #[test]
1709 fn test_ts_find_call_for_signature_array_access() {
1710 let source = r#"
1712contract Foo {
1713 mapping(bytes32 => uint256) public orders;
1714 function test() public {
1715 orders[orderId];
1716 }
1717}
1718"#;
1719 let tree = ts_parse(source).unwrap();
1720 let pos = source.find("[orderId]").unwrap() + 1;
1722 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1723 assert_eq!(ctx.name, "orders");
1724 assert_eq!(ctx.arg_index, 0);
1725 assert!(ctx.is_index_access);
1726 }
1727
1728 #[test]
1729 fn test_ts_find_call_for_signature_array_access_empty() {
1730 let source = r#"
1732contract Foo {
1733 mapping(bytes32 => uint256) public orders;
1734 function test() public {
1735 orders[
1736 }
1737}
1738"#;
1739 let tree = ts_parse(source).unwrap();
1740 let pos = source.find("orders[").unwrap() + 7;
1741 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1742 assert_eq!(ctx.name, "orders");
1743 assert!(ctx.is_index_access);
1744 }
1745}