1use crate::gas;
2use crate::goto::CachedBuild;
3use crate::types::SourceLoc;
4use serde_json::Value;
5use std::collections::HashMap;
6use tower_lsp::lsp_types::*;
7use tree_sitter::{Node, Parser};
8
9#[allow(dead_code)]
12enum FnGasHintPosition {
13 Opening,
15 Closing,
17}
18
19const FN_GAS_HINT_POSITION: FnGasHintPosition = FnGasHintPosition::Closing;
21
22#[derive(Debug, Clone)]
24struct ParamInfo {
25 names: Vec<String>,
27 skip: usize,
29}
30
31#[derive(Debug, Clone)]
33struct CallSite {
34 info: ParamInfo,
36 name: String,
38 decl_id: u64,
40}
41
42#[derive(Debug, Clone)]
44pub struct ResolvedCallSite {
45 pub param_name: String,
47 pub decl_id: u64,
49}
50
51#[derive(Debug, Clone)]
54pub struct HintLookup {
55 by_offset: HashMap<usize, CallSite>,
57 by_name: HashMap<(String, usize), CallSite>,
59}
60
61impl HintLookup {
62 pub fn resolve_callsite_param(
68 &self,
69 call_offset: usize,
70 func_name: &str,
71 arg_count: usize,
72 arg_index: usize,
73 ) -> Option<ResolvedCallSite> {
74 let site = lookup_call_site(self, call_offset, func_name, arg_count)?;
75 let param_idx = arg_index + site.info.skip;
76 if param_idx >= site.info.names.len() {
77 return None;
78 }
79 let param_name = &site.info.names[param_idx];
80 if param_name.is_empty() {
81 return None;
82 }
83 Some(ResolvedCallSite {
84 param_name: param_name.clone(),
85 decl_id: site.decl_id,
86 })
87 }
88}
89
90pub type HintIndex = HashMap<String, HintLookup>;
93
94pub fn build_hint_index(sources: &Value) -> HintIndex {
97 let id_index = build_id_index(sources);
98 let mut hint_index = HashMap::new();
99
100 if let Some(obj) = sources.as_object() {
101 for (_, source_data) in obj {
102 if let Some(ast) = source_data.get("ast")
103 && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
104 {
105 let lookup = build_hint_lookup(ast, &id_index);
106 hint_index.insert(abs_path.to_string(), lookup);
107 }
108 }
109 }
110
111 hint_index
112}
113
114pub fn inlay_hints(
120 build: &CachedBuild,
121 uri: &Url,
122 range: Range,
123 live_source: &[u8],
124) -> Vec<InlayHint> {
125 let path_str = match uri.to_file_path() {
126 Ok(p) => p.to_str().unwrap_or("").to_string(),
127 Err(_) => return vec![],
128 };
129
130 let abs = match build
131 .path_to_abs
132 .iter()
133 .find(|(k, _)| path_str.ends_with(k.as_str()))
134 {
135 Some((_, v)) => v.clone(),
136 None => return vec![],
137 };
138
139 let lookup = match build.hint_index.get(&abs) {
141 Some(l) => l,
142 None => return vec![],
143 };
144
145 let source_str = String::from_utf8_lossy(live_source);
147 let tree = match ts_parse(&source_str) {
148 Some(t) => t,
149 None => return vec![],
150 };
151
152 let mut hints = Vec::new();
153 collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
154
155 if !build.gas_index.is_empty() {
157 collect_ts_gas_hints(
158 tree.root_node(),
159 &source_str,
160 &range,
161 &build.gas_index,
162 &abs,
163 &mut hints,
164 );
165 }
166
167 hints
168}
169
170fn build_id_index(sources: &Value) -> HashMap<u64, &Value> {
174 let mut index = HashMap::new();
175 if let Some(obj) = sources.as_object() {
176 for (_, source_data) in obj {
177 if let Some(ast) = source_data.get("ast") {
178 index_node_ids(ast, &mut index);
179 }
180 }
181 }
182 index
183}
184
185fn index_node_ids<'a>(node: &'a Value, index: &mut HashMap<u64, &'a Value>) {
187 if let Some(id) = node.get("id").and_then(|v| v.as_u64()) {
188 index.insert(id, node);
189 }
190 for key in crate::goto::CHILD_KEYS {
191 if let Some(child) = node.get(*key) {
192 if child.is_array() {
193 if let Some(arr) = child.as_array() {
194 for item in arr {
195 index_node_ids(item, index);
196 }
197 }
198 } else if child.is_object() {
199 index_node_ids(child, index);
200 }
201 }
202 }
203 if let Some(nodes) = node.get("nodes").and_then(|v| v.as_array()) {
204 for child in nodes {
205 index_node_ids(child, index);
206 }
207 }
208}
209
210pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
212 let mut parser = Parser::new();
213 parser
214 .set_language(&tree_sitter_solidity::LANGUAGE.into())
215 .expect("failed to load Solidity grammar");
216 parser.parse(source, None)
217}
218
219fn build_hint_lookup(file_ast: &Value, id_index: &HashMap<u64, &Value>) -> HintLookup {
221 let mut lookup = HintLookup {
222 by_offset: HashMap::new(),
223 by_name: HashMap::new(),
224 };
225 collect_ast_calls(file_ast, id_index, &mut lookup);
226 lookup
227}
228
229fn parse_src_offset(node: &Value) -> Option<usize> {
231 let src = node.get("src").and_then(|v| v.as_str())?;
232 SourceLoc::parse(src).map(|loc| loc.offset)
233}
234
235fn collect_ast_calls(node: &Value, id_index: &HashMap<u64, &Value>, lookup: &mut HintLookup) {
237 let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
238
239 match node_type {
240 "FunctionCall" => {
241 if let Some(call_info) = extract_call_info(node, id_index) {
242 let arg_count = node
243 .get("arguments")
244 .and_then(|v| v.as_array())
245 .map(|a| a.len())
246 .unwrap_or(0);
247 let site = CallSite {
248 info: ParamInfo {
249 names: call_info.params.names,
250 skip: call_info.params.skip,
251 },
252 name: call_info.name,
253 decl_id: call_info.decl_id,
254 };
255 if let Some(offset) = parse_src_offset(node) {
256 lookup.by_offset.insert(offset, site.clone());
257 }
258
259 lookup
260 .by_name
261 .entry((site.name.clone(), arg_count))
262 .or_insert(site);
263 }
264 }
265 "EmitStatement" => {
266 if let Some(event_call) = node.get("eventCall")
267 && let Some(call_info) = extract_call_info(event_call, id_index)
268 {
269 let arg_count = event_call
270 .get("arguments")
271 .and_then(|v| v.as_array())
272 .map(|a| a.len())
273 .unwrap_or(0);
274 let site = CallSite {
275 info: ParamInfo {
276 names: call_info.params.names,
277 skip: call_info.params.skip,
278 },
279 name: call_info.name,
280 decl_id: call_info.decl_id,
281 };
282 if let Some(offset) = parse_src_offset(node) {
283 lookup.by_offset.insert(offset, site.clone());
284 }
285
286 lookup
287 .by_name
288 .entry((site.name.clone(), arg_count))
289 .or_insert(site);
290 }
291 }
292 _ => {}
293 }
294
295 for key in crate::goto::CHILD_KEYS {
297 if let Some(child) = node.get(*key) {
298 if child.is_array() {
299 if let Some(arr) = child.as_array() {
300 for item in arr {
301 collect_ast_calls(item, id_index, lookup);
302 }
303 }
304 } else if child.is_object() {
305 collect_ast_calls(child, id_index, lookup);
306 }
307 }
308 }
309}
310
311struct CallInfo {
313 name: String,
315 params: ParamInfo,
317 decl_id: u64,
319}
320
321fn extract_call_info(node: &Value, id_index: &HashMap<u64, &Value>) -> Option<CallInfo> {
323 let args = node.get("arguments")?.as_array()?;
324 if args.is_empty() {
325 return None;
326 }
327
328 let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
330 if kind == "structConstructorCall"
331 && node
332 .get("names")
333 .and_then(|v| v.as_array())
334 .is_some_and(|n| !n.is_empty())
335 {
336 return None;
337 }
338
339 let expr = node.get("expression")?;
340 let decl_id = expr.get("referencedDeclaration").and_then(|v| v.as_u64())?;
341
342 let decl_node = id_index.get(&decl_id)?;
343 let names = get_parameter_names(decl_node)?;
344
345 let func_name = extract_function_name(expr)?;
347
348 let arg_count = node
353 .get("arguments")
354 .and_then(|v| v.as_array())
355 .map(|a| a.len())
356 .unwrap_or(0);
357 let skip = if is_member_access(expr) && arg_count < names.len() {
358 1
359 } else {
360 0
361 };
362
363 Some(CallInfo {
364 name: func_name,
365 params: ParamInfo { names, skip },
366 decl_id,
367 })
368}
369
370fn extract_function_name(expr: &Value) -> Option<String> {
372 let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
373 match node_type {
374 "Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
375 "MemberAccess" => expr
376 .get("memberName")
377 .and_then(|v| v.as_str())
378 .map(String::from),
379 _ => None,
380 }
381}
382
383fn is_member_access(expr: &Value) -> bool {
385 expr.get("nodeType")
386 .and_then(|v| v.as_str())
387 .is_some_and(|t| t == "MemberAccess")
388}
389
390fn lookup_call_site<'a>(
394 lookup: &'a HintLookup,
395 offset: usize,
396 name: &str,
397 arg_count: usize,
398) -> Option<&'a CallSite> {
399 if let Some(site) = lookup.by_offset.get(&offset)
401 && site.name == name
402 {
403 return Some(site);
404 }
405 lookup.by_name.get(&(name.to_string(), arg_count))
407}
408
409fn collect_ts_hints(
411 node: Node,
412 source: &str,
413 range: &Range,
414 lookup: &HintLookup,
415 hints: &mut Vec<InlayHint>,
416) {
417 let node_start = node.start_position();
419 let node_end = node.end_position();
420 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
421 return;
422 }
423
424 match node.kind() {
425 "call_expression" => {
426 emit_call_hints(node, source, lookup, hints);
427 }
428 "emit_statement" => {
429 emit_emit_hints(node, source, lookup, hints);
430 }
431 _ => {}
432 }
433
434 let mut cursor = node.walk();
436 for child in node.children(&mut cursor) {
437 collect_ts_hints(child, source, range, lookup, hints);
438 }
439}
440
441fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
443 let func_name = match ts_call_function_name(node, source) {
444 Some(n) => n,
445 None => return,
446 };
447
448 let args = ts_call_arguments(node);
449 if args.is_empty() {
450 return;
451 }
452
453 let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
454 Some(s) => s,
455 None => return,
456 };
457
458 emit_param_hints(&args, &site.info, hints);
459}
460
461fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
463 let event_name = match ts_emit_event_name(node, source) {
464 Some(n) => n,
465 None => return,
466 };
467
468 let args = ts_call_arguments(node);
469 if args.is_empty() {
470 return;
471 }
472
473 let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
474 Some(s) => s,
475 None => return,
476 };
477
478 emit_param_hints(&args, &site.info, hints);
479}
480
481fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
483 for (i, arg) in args.iter().enumerate() {
484 let pi = i + info.skip;
485 if pi >= info.names.len() || info.names[pi].is_empty() {
486 continue;
487 }
488
489 let start = arg.start_position();
490 let position = Position::new(start.row as u32, start.column as u32);
491
492 hints.push(InlayHint {
493 position,
494 kind: Some(InlayHintKind::PARAMETER),
495 label: InlayHintLabel::String(format!("{}:", info.names[pi])),
496 text_edits: None,
497 tooltip: None,
498 padding_left: None,
499 padding_right: Some(true),
500 data: None,
501 });
502 }
503}
504
505fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
512 let func_expr = node.child_by_field_name("function")?;
513 let inner = first_named_child(func_expr)?;
515 match inner.kind() {
516 "identifier" => Some(&source[inner.byte_range()]),
517 "member_expression" => {
518 let prop = inner.child_by_field_name("property")?;
519 Some(&source[prop.byte_range()])
520 }
521 _ => None,
522 }
523}
524
525fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
527 let name_expr = node.child_by_field_name("name")?;
528 let inner = first_named_child(name_expr)?;
529 match inner.kind() {
530 "identifier" => Some(&source[inner.byte_range()]),
531 "member_expression" => {
532 let prop = inner.child_by_field_name("property")?;
533 Some(&source[prop.byte_range()])
534 }
535 _ => None,
536 }
537}
538
539fn ts_call_arguments(node: Node) -> Vec<Node> {
542 let mut args = Vec::new();
543 let mut cursor = node.walk();
544 for child in node.children(&mut cursor) {
545 if child.kind() == "call_argument" {
546 args.push(child);
547 }
548 }
549 args
550}
551
552fn first_named_child(node: Node) -> Option<Node> {
554 let mut cursor = node.walk();
555 node.children(&mut cursor).find(|c| c.is_named())
556}
557
558pub struct TsCallContext<'a> {
560 pub name: &'a str,
562 pub arg_index: usize,
564 pub arg_count: usize,
566 pub call_start_byte: usize,
568}
569
570pub fn ts_find_call_at_byte<'a>(
575 root: tree_sitter::Node<'a>,
576 source: &'a str,
577 byte_pos: usize,
578) -> Option<TsCallContext<'a>> {
579 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
581
582 loop {
584 if node.kind() == "call_argument" {
585 break;
586 }
587 node = node.parent()?;
588 }
589
590 let call_node = node.parent()?;
592 let args = ts_call_arguments(call_node);
593 let arg_index = args.iter().position(|a| a.id() == node.id())?;
594
595 match call_node.kind() {
596 "call_expression" => {
597 let name = ts_call_function_name(call_node, source)?;
598 Some(TsCallContext {
599 name,
600 arg_index,
601 arg_count: args.len(),
602 call_start_byte: call_node.start_byte(),
603 })
604 }
605 "emit_statement" => {
606 let name = ts_emit_event_name(call_node, source)?;
607 Some(TsCallContext {
608 name,
609 arg_index,
610 arg_count: args.len(),
611 call_start_byte: call_node.start_byte(),
612 })
613 }
614 _ => None,
615 }
616}
617
618fn collect_ts_gas_hints(
623 node: Node,
624 source: &str,
625 range: &Range,
626 gas_index: &gas::GasIndex,
627 abs_path: &str,
628 hints: &mut Vec<InlayHint>,
629) {
630 let node_start = node.start_position();
631 let node_end = node.end_position();
632 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
633 return;
634 }
635
636 match node.kind() {
637 "function_definition" => {
638 if let Some(hint) = ts_gas_hint_for_function(node, source, range, gas_index, abs_path) {
639 hints.push(hint);
640 }
641 }
642 "contract_declaration" | "library_declaration" | "interface_declaration" => {
643 if let Some(hint) = ts_gas_hint_for_contract(node, source, range, gas_index, abs_path) {
644 hints.push(hint);
645 }
646 }
647 _ => {}
648 }
649
650 let mut cursor = node.walk();
651 for child in node.children(&mut cursor) {
652 collect_ts_gas_hints(child, source, range, gas_index, abs_path, hints);
653 }
654}
655
656fn ts_node_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
658 let mut cursor = node.walk();
659 node.children(&mut cursor)
660 .find(|c| c.kind() == "identifier" && c.is_named())
661 .map(|c| &source[c.byte_range()])
662}
663
664fn ts_body_open_brace(node: Node, body_kind: &str) -> Option<Position> {
666 let mut cursor = node.walk();
667 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
668 let start = body.start_position();
669 Some(Position::new(start.row as u32, start.column as u32))
670}
671
672fn ts_body_close_brace(node: Node, body_kind: &str) -> Option<Position> {
674 let mut cursor = node.walk();
675 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
676 let end = body.end_position();
677 Some(Position::new(
679 end.row as u32,
680 end.column.saturating_sub(1) as u32,
681 ))
682}
683
684fn ts_enclosing_contract_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
686 let mut parent = node.parent();
687 while let Some(p) = parent {
688 if p.kind() == "contract_declaration"
689 || p.kind() == "library_declaration"
690 || p.kind() == "interface_declaration"
691 {
692 return ts_node_name(p, source);
693 }
694 parent = p.parent();
695 }
696 None
697}
698
699fn find_gas_key<'a>(
701 gas_index: &'a gas::GasIndex,
702 abs_path: &str,
703 contract_name: &str,
704) -> Option<&'a str> {
705 let exact = format!("{abs_path}:{contract_name}");
706 if gas_index.contains_key(&exact) {
707 return Some(gas_index.get_key_value(&exact)?.0.as_str());
708 }
709 let file_name = std::path::Path::new(abs_path).file_name()?.to_str()?;
710 let suffix = format!("{file_name}:{contract_name}");
711 gas_index
712 .keys()
713 .find(|k| k.ends_with(&suffix))
714 .map(|k| k.as_str())
715}
716
717fn ts_gas_hint_for_function(
719 node: Node,
720 source: &str,
721 range: &Range,
722 gas_index: &gas::GasIndex,
723 abs_path: &str,
724) -> Option<InlayHint> {
725 let fn_name = ts_node_name(node, source)?;
726 let contract_name = ts_enclosing_contract_name(node, source)?;
727 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
728 let contract_gas = gas_index.get(gas_key)?;
729
730 let prefix = format!("{fn_name}(");
731 let cost = contract_gas
732 .external_by_sig
733 .iter()
734 .find(|(sig, _)| sig.as_str().starts_with(&prefix))
735 .map(|(_, c)| c.as_str())
736 .or_else(|| {
737 contract_gas
738 .internal
739 .iter()
740 .find(|(sig, _)| sig.starts_with(&prefix))
741 .map(|(_, c)| c.as_str())
742 })?;
743
744 let (brace_pos, offset) = match FN_GAS_HINT_POSITION {
746 FnGasHintPosition::Opening => (ts_body_open_brace(node, "function_body")?, 1),
747 FnGasHintPosition::Closing => (ts_body_close_brace(node, "function_body")?, 1),
748 };
749 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
750 return None;
751 }
752
753 Some(InlayHint {
754 position: Position::new(brace_pos.line, brace_pos.character + offset),
755 kind: Some(InlayHintKind::TYPE),
756 label: InlayHintLabel::String(format!("{} {}", gas::GAS_ICON, gas::format_gas(cost))),
757 text_edits: None,
758 tooltip: Some(InlayHintTooltip::String("Estimated gas cost".to_string())),
759 padding_left: Some(true),
760 padding_right: None,
761 data: None,
762 })
763}
764
765fn ts_gas_hint_for_contract(
768 node: Node,
769 source: &str,
770 range: &Range,
771 gas_index: &gas::GasIndex,
772 abs_path: &str,
773) -> Option<InlayHint> {
774 let contract_name = ts_node_name(node, source)?;
775 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
776 let contract_gas = gas_index.get(gas_key)?;
777
778 let display_cost = match contract_gas.creation.get("totalCost").map(|s| s.as_str()) {
780 Some("infinite") | None => contract_gas
781 .creation
782 .get("codeDepositCost")
783 .map(|s| s.as_str())?,
784 Some(total) => total,
785 };
786
787 let brace_pos = ts_body_open_brace(node, "contract_body")?;
788 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
789 return None;
790 }
791
792 Some(InlayHint {
793 position: Position::new(brace_pos.line, brace_pos.character + 1),
794 kind: Some(InlayHintKind::TYPE),
795 label: InlayHintLabel::String(format!(
796 "{} {} ",
797 gas::GAS_ICON,
798 gas::format_gas(display_cost)
799 )),
800 text_edits: None,
801 tooltip: Some(InlayHintTooltip::String(format!(
802 "Deploy cost — code deposit: {}, execution: {}",
803 gas::format_gas(
804 contract_gas
805 .creation
806 .get("codeDepositCost")
807 .map(|s| s.as_str())
808 .unwrap_or("?")
809 ),
810 gas::format_gas(
811 contract_gas
812 .creation
813 .get("executionCost")
814 .map(|s| s.as_str())
815 .unwrap_or("?")
816 )
817 ))),
818 padding_left: Some(true),
819 padding_right: None,
820 data: None,
821 })
822}
823
824fn get_parameter_names(decl: &Value) -> Option<Vec<String>> {
828 let items = decl
831 .get("parameters")
832 .and_then(|p| p.get("parameters"))
833 .and_then(|v| v.as_array())
834 .or_else(|| decl.get("members").and_then(|v| v.as_array()))?;
835 Some(
836 items
837 .iter()
838 .map(|p| {
839 p.get("name")
840 .and_then(|v| v.as_str())
841 .unwrap_or("")
842 .to_string()
843 })
844 .collect(),
845 )
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 #[test]
853 fn test_get_parameter_names() {
854 let decl: Value = serde_json::json!({
855 "parameters": {
856 "parameters": [
857 {"name": "to", "nodeType": "VariableDeclaration"},
858 {"name": "amount", "nodeType": "VariableDeclaration"},
859 ]
860 }
861 });
862 let names = get_parameter_names(&decl).unwrap();
863 assert_eq!(names, vec!["to", "amount"]);
864 }
865
866 #[test]
867 fn test_ts_call_function_name() {
868 let source = r#"
869contract Foo {
870 function bar(uint x) public {}
871 function test() public {
872 bar(42);
873 }
874}
875"#;
876 let tree = ts_parse(source).unwrap();
877 let mut found = Vec::new();
878 find_calls(tree.root_node(), source, &mut found);
879 assert_eq!(found.len(), 1);
880 assert_eq!(found[0], "bar");
881 }
882
883 #[test]
884 fn test_ts_member_call_name() {
885 let source = r#"
886contract Foo {
887 function test() public {
888 PRICE.addTax(TAX, TAX_BASE);
889 }
890}
891"#;
892 let tree = ts_parse(source).unwrap();
893 let mut found = Vec::new();
894 find_calls(tree.root_node(), source, &mut found);
895 assert_eq!(found.len(), 1);
896 assert_eq!(found[0], "addTax");
897 }
898
899 #[test]
900 fn test_ts_emit_event_name() {
901 let source = r#"
902contract Foo {
903 event Purchase(address buyer, uint256 price);
904 function test() public {
905 emit Purchase(msg.sender, 100);
906 }
907}
908"#;
909 let tree = ts_parse(source).unwrap();
910 let mut found = Vec::new();
911 find_emits(tree.root_node(), source, &mut found);
912 assert_eq!(found.len(), 1);
913 assert_eq!(found[0], "Purchase");
914 }
915
916 #[test]
917 fn test_ts_call_arguments_count() {
918 let source = r#"
919contract Foo {
920 function bar(uint x, uint y) public {}
921 function test() public {
922 bar(1, 2);
923 }
924}
925"#;
926 let tree = ts_parse(source).unwrap();
927 let mut arg_counts = Vec::new();
928 find_call_arg_counts(tree.root_node(), &mut arg_counts);
929 assert_eq!(arg_counts, vec![2]);
930 }
931
932 #[test]
933 fn test_ts_argument_positions_follow_live_buffer() {
934 let source = r#"
936contract Foo {
937 function bar(uint x, uint y) public {}
938 function test() public {
939 bar(
940 1,
941 2
942 );
943 }
944}
945"#;
946 let tree = ts_parse(source).unwrap();
947 let mut positions = Vec::new();
948 find_arg_positions(tree.root_node(), &mut positions);
949 assert_eq!(positions.len(), 2);
951 assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
954
955 fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
958 if node.kind() == "call_expression"
959 && let Some(name) = ts_call_function_name(node, source)
960 {
961 out.push(name);
962 }
963 let mut cursor = node.walk();
964 for child in node.children(&mut cursor) {
965 find_calls(child, source, out);
966 }
967 }
968
969 fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
970 if node.kind() == "emit_statement"
971 && let Some(name) = ts_emit_event_name(node, source)
972 {
973 out.push(name);
974 }
975 let mut cursor = node.walk();
976 for child in node.children(&mut cursor) {
977 find_emits(child, source, out);
978 }
979 }
980
981 fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
982 if node.kind() == "call_expression" {
983 out.push(ts_call_arguments(node).len());
984 }
985 let mut cursor = node.walk();
986 for child in node.children(&mut cursor) {
987 find_call_arg_counts(child, out);
988 }
989 }
990
991 fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
992 if node.kind() == "call_expression" {
993 for arg in ts_call_arguments(node) {
994 let p = arg.start_position();
995 out.push((p.row, p.column));
996 }
997 }
998 let mut cursor = node.walk();
999 for child in node.children(&mut cursor) {
1000 find_arg_positions(child, out);
1001 }
1002 }
1003
1004 #[test]
1005 fn test_ts_find_call_at_byte_first_arg() {
1006 let source = r#"
1007contract Foo {
1008 function bar(uint x, uint y) public {}
1009 function test() public {
1010 bar(42, 99);
1011 }
1012}
1013"#;
1014 let tree = ts_parse(source).unwrap();
1015 let pos_42 = source.find("42").unwrap();
1017 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1018 assert_eq!(ctx.name, "bar");
1019 assert_eq!(ctx.arg_index, 0);
1020 assert_eq!(ctx.arg_count, 2);
1021 }
1022
1023 #[test]
1024 fn test_ts_find_call_at_byte_second_arg() {
1025 let source = r#"
1026contract Foo {
1027 function bar(uint x, uint y) public {}
1028 function test() public {
1029 bar(42, 99);
1030 }
1031}
1032"#;
1033 let tree = ts_parse(source).unwrap();
1034 let pos_99 = source.find("99").unwrap();
1035 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1036 assert_eq!(ctx.name, "bar");
1037 assert_eq!(ctx.arg_index, 1);
1038 assert_eq!(ctx.arg_count, 2);
1039 }
1040
1041 #[test]
1042 fn test_ts_find_call_at_byte_outside_call_returns_none() {
1043 let source = r#"
1044contract Foo {
1045 function bar(uint x) public {}
1046 function test() public {
1047 uint z = 10;
1048 bar(42);
1049 }
1050}
1051"#;
1052 let tree = ts_parse(source).unwrap();
1053 let pos_10 = source.find("10").unwrap();
1055 assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1056 }
1057
1058 #[test]
1059 fn test_ts_find_call_at_byte_member_call() {
1060 let source = r#"
1061contract Foo {
1062 function test() public {
1063 PRICE.addTax(TAX, TAX_BASE);
1064 }
1065}
1066"#;
1067 let tree = ts_parse(source).unwrap();
1068 let pos_tax = source.find("TAX,").unwrap();
1069 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1070 assert_eq!(ctx.name, "addTax");
1071 assert_eq!(ctx.arg_index, 0);
1072 assert_eq!(ctx.arg_count, 2);
1073 }
1074
1075 #[test]
1076 fn test_ts_find_call_at_byte_emit_statement() {
1077 let source = r#"
1078contract Foo {
1079 event Purchase(address buyer, uint256 price);
1080 function test() public {
1081 emit Purchase(msg.sender, 100);
1082 }
1083}
1084"#;
1085 let tree = ts_parse(source).unwrap();
1086 let pos_100 = source.find("100").unwrap();
1087 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1088 assert_eq!(ctx.name, "Purchase");
1089 assert_eq!(ctx.arg_index, 1);
1090 assert_eq!(ctx.arg_count, 2);
1091 }
1092
1093 #[test]
1094 fn test_ts_find_call_at_byte_multiline() {
1095 let source = r#"
1096contract Foo {
1097 function bar(uint x, uint y, uint z) public {}
1098 function test() public {
1099 bar(
1100 1,
1101 2,
1102 3
1103 );
1104 }
1105}
1106"#;
1107 let tree = ts_parse(source).unwrap();
1108 let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1112 assert_eq!(ctx.name, "bar");
1113 assert_eq!(ctx.arg_index, 1);
1114 assert_eq!(ctx.arg_count, 3);
1115 }
1116
1117 #[test]
1118 fn test_resolve_callsite_param_basic() {
1119 let mut lookup = HintLookup {
1121 by_offset: HashMap::new(),
1122 by_name: HashMap::new(),
1123 };
1124 lookup.by_name.insert(
1125 ("transfer".to_string(), 2),
1126 CallSite {
1127 info: ParamInfo {
1128 names: vec!["to".to_string(), "amount".to_string()],
1129 skip: 0,
1130 },
1131 name: "transfer".to_string(),
1132 decl_id: 42,
1133 },
1134 );
1135
1136 let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1138 assert_eq!(result.param_name, "to");
1139 assert_eq!(result.decl_id, 42);
1140
1141 let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1143 assert_eq!(result.param_name, "amount");
1144 assert_eq!(result.decl_id, 42);
1145 }
1146
1147 #[test]
1148 fn test_resolve_callsite_param_with_skip() {
1149 let mut lookup = HintLookup {
1151 by_offset: HashMap::new(),
1152 by_name: HashMap::new(),
1153 };
1154 lookup.by_name.insert(
1155 ("addTax".to_string(), 2),
1156 CallSite {
1157 info: ParamInfo {
1158 names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1159 skip: 1,
1160 },
1161 name: "addTax".to_string(),
1162 decl_id: 99,
1163 },
1164 );
1165
1166 let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1168 assert_eq!(result.param_name, "tax");
1169
1170 let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1172 assert_eq!(result.param_name, "base");
1173 }
1174
1175 #[test]
1176 fn test_resolve_callsite_param_out_of_bounds() {
1177 let mut lookup = HintLookup {
1178 by_offset: HashMap::new(),
1179 by_name: HashMap::new(),
1180 };
1181 lookup.by_name.insert(
1182 ("foo".to_string(), 1),
1183 CallSite {
1184 info: ParamInfo {
1185 names: vec!["x".to_string()],
1186 skip: 0,
1187 },
1188 name: "foo".to_string(),
1189 decl_id: 1,
1190 },
1191 );
1192
1193 assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1195 }
1196
1197 #[test]
1198 fn test_resolve_callsite_param_unknown_function() {
1199 let lookup = HintLookup {
1200 by_offset: HashMap::new(),
1201 by_name: HashMap::new(),
1202 };
1203 assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1204 }
1205
1206 #[test]
1207 fn test_ts_find_call_at_byte_emit_member_access() {
1208 let source = r#"
1212contract Foo {
1213 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1214 function test() public {
1215 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1216 }
1217}
1218"#;
1219 let tree = ts_parse(source).unwrap();
1220 let params_tick = source.find("params.tickLower,").unwrap();
1222 let tick_lower_pos = params_tick + "params.".len(); let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1225 assert_eq!(ctx.name, "ModifyLiquidity");
1226 assert_eq!(
1227 ctx.arg_index, 2,
1228 "params.tickLower is the 3rd argument (index 2)"
1229 );
1230 assert_eq!(ctx.arg_count, 4);
1231 }
1232
1233 #[test]
1234 fn test_ts_find_call_at_byte_member_access_on_property() {
1235 let source = r#"
1237contract Foo {
1238 event Transfer(address from, address to);
1239 function test() public {
1240 emit Transfer(msg.sender, addr);
1241 }
1242}
1243"#;
1244 let tree = ts_parse(source).unwrap();
1245 let sender_pos = source.find("sender").unwrap();
1246 let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1247 assert_eq!(ctx.name, "Transfer");
1248 assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1249 }
1250
1251 #[test]
1252 fn test_ts_find_call_at_byte_emit_all_args() {
1253 let source = r#"
1255contract Foo {
1256 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1257 function test() public {
1258 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1259 }
1260}
1261"#;
1262 let tree = ts_parse(source).unwrap();
1263
1264 let pos_id = source.find("(id,").unwrap() + 1;
1266 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1267 assert_eq!(ctx.name, "ModifyLiquidity");
1268 assert_eq!(ctx.arg_index, 0);
1269
1270 let pos_msg = source.find("msg.sender").unwrap();
1272 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1273 assert_eq!(ctx.arg_index, 1);
1274
1275 let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1277 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1278 assert_eq!(ctx.arg_index, 2);
1279
1280 let pos_tu = source.find("params.tickUpper").unwrap();
1282 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1283 assert_eq!(ctx.arg_index, 3);
1284 }
1285
1286 #[test]
1287 fn test_ts_find_call_at_byte_nested_call_arg() {
1288 let source = r#"
1291contract Foo {
1292 function inner(uint x) public returns (uint) {}
1293 function outer(uint a, uint b) public {}
1294 function test() public {
1295 outer(inner(42), 99);
1296 }
1297}
1298"#;
1299 let tree = ts_parse(source).unwrap();
1300
1301 let pos_42 = source.find("42").unwrap();
1303 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1304 assert_eq!(ctx.name, "inner");
1305 assert_eq!(ctx.arg_index, 0);
1306
1307 let pos_99 = source.find("99").unwrap();
1309 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1310 assert_eq!(ctx.name, "outer");
1311 assert_eq!(ctx.arg_index, 1);
1312 }
1313}