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_with_skip(
71 &self,
72 call_offset: usize,
73 func_name: &str,
74 arg_count: usize,
75 ) -> Option<(u64, usize)> {
76 if let Some(site) = lookup_call_site(self, call_offset, func_name, arg_count) {
78 return Some((site.decl_id, site.info.skip));
79 }
80 self.by_name
82 .iter()
83 .find(|((name, _), _)| name == func_name)
84 .map(|(_, site)| (site.decl_id, site.info.skip))
85 }
86
87 pub fn resolve_callsite_param(
93 &self,
94 call_offset: usize,
95 func_name: &str,
96 arg_count: usize,
97 arg_index: usize,
98 ) -> Option<ResolvedCallSite> {
99 let site = lookup_call_site(self, call_offset, func_name, arg_count)?;
100 let param_idx = arg_index + site.info.skip;
101 if param_idx >= site.info.names.len() {
102 return None;
103 }
104 let param_name = &site.info.names[param_idx];
105 if param_name.is_empty() {
106 return None;
107 }
108 Some(ResolvedCallSite {
109 param_name: param_name.clone(),
110 decl_id: site.decl_id,
111 })
112 }
113}
114
115pub type HintIndex = HashMap<String, HintLookup>;
118
119pub fn build_hint_index(sources: &Value) -> HintIndex {
122 let id_index = build_id_index(sources);
123 let mut hint_index = HashMap::new();
124
125 if let Some(obj) = sources.as_object() {
126 for (_, source_data) in obj {
127 if let Some(ast) = source_data.get("ast")
128 && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
129 {
130 let lookup = build_hint_lookup(ast, &id_index);
131 hint_index.insert(abs_path.to_string(), lookup);
132 }
133 }
134 }
135
136 hint_index
137}
138
139pub fn inlay_hints(
145 build: &CachedBuild,
146 uri: &Url,
147 range: Range,
148 live_source: &[u8],
149) -> Vec<InlayHint> {
150 let path_str = match uri.to_file_path() {
151 Ok(p) => p.to_str().unwrap_or("").to_string(),
152 Err(_) => return vec![],
153 };
154
155 let abs = match build
156 .path_to_abs
157 .iter()
158 .find(|(k, _)| path_str.ends_with(k.as_str()))
159 {
160 Some((_, v)) => v.clone(),
161 None => return vec![],
162 };
163
164 let lookup = match build.hint_index.get(&abs) {
166 Some(l) => l,
167 None => return vec![],
168 };
169
170 let source_str = String::from_utf8_lossy(live_source);
172 let tree = match ts_parse(&source_str) {
173 Some(t) => t,
174 None => return vec![],
175 };
176
177 let mut hints = Vec::new();
178 collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
179
180 if !build.gas_index.is_empty() {
182 collect_ts_gas_hints(
183 tree.root_node(),
184 &source_str,
185 &range,
186 &build.gas_index,
187 &abs,
188 &mut hints,
189 );
190 }
191
192 hints
193}
194
195fn build_id_index(sources: &Value) -> HashMap<u64, &Value> {
199 let mut index = HashMap::new();
200 if let Some(obj) = sources.as_object() {
201 for (_, source_data) in obj {
202 if let Some(ast) = source_data.get("ast") {
203 index_node_ids(ast, &mut index);
204 }
205 }
206 }
207 index
208}
209
210fn index_node_ids<'a>(node: &'a Value, index: &mut HashMap<u64, &'a Value>) {
212 if let Some(id) = node.get("id").and_then(|v| v.as_u64()) {
213 index.insert(id, node);
214 }
215 for key in crate::goto::CHILD_KEYS {
216 if let Some(child) = node.get(*key) {
217 if child.is_array() {
218 if let Some(arr) = child.as_array() {
219 for item in arr {
220 index_node_ids(item, index);
221 }
222 }
223 } else if child.is_object() {
224 index_node_ids(child, index);
225 }
226 }
227 }
228 if let Some(nodes) = node.get("nodes").and_then(|v| v.as_array()) {
229 for child in nodes {
230 index_node_ids(child, index);
231 }
232 }
233}
234
235pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
237 let mut parser = Parser::new();
238 parser
239 .set_language(&tree_sitter_solidity::LANGUAGE.into())
240 .expect("failed to load Solidity grammar");
241 parser.parse(source, None)
242}
243
244fn build_hint_lookup(file_ast: &Value, id_index: &HashMap<u64, &Value>) -> HintLookup {
246 let mut lookup = HintLookup {
247 by_offset: HashMap::new(),
248 by_name: HashMap::new(),
249 };
250 collect_ast_calls(file_ast, id_index, &mut lookup);
251 lookup
252}
253
254fn parse_src_offset(node: &Value) -> Option<usize> {
256 let src = node.get("src").and_then(|v| v.as_str())?;
257 SourceLoc::parse(src).map(|loc| loc.offset)
258}
259
260fn collect_ast_calls(node: &Value, id_index: &HashMap<u64, &Value>, lookup: &mut HintLookup) {
262 let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
263
264 match node_type {
265 "FunctionCall" => {
266 if let Some(call_info) = extract_call_info(node, id_index) {
267 let arg_count = node
268 .get("arguments")
269 .and_then(|v| v.as_array())
270 .map(|a| a.len())
271 .unwrap_or(0);
272 let site = CallSite {
273 info: ParamInfo {
274 names: call_info.params.names,
275 skip: call_info.params.skip,
276 },
277 name: call_info.name,
278 decl_id: call_info.decl_id,
279 };
280 if let Some(offset) = parse_src_offset(node) {
281 lookup.by_offset.insert(offset, site.clone());
282 }
283
284 lookup
285 .by_name
286 .entry((site.name.clone(), arg_count))
287 .or_insert(site);
288 }
289 }
290 "EmitStatement" => {
291 if let Some(event_call) = node.get("eventCall")
292 && let Some(call_info) = extract_call_info(event_call, id_index)
293 {
294 let arg_count = event_call
295 .get("arguments")
296 .and_then(|v| v.as_array())
297 .map(|a| a.len())
298 .unwrap_or(0);
299 let site = CallSite {
300 info: ParamInfo {
301 names: call_info.params.names,
302 skip: call_info.params.skip,
303 },
304 name: call_info.name,
305 decl_id: call_info.decl_id,
306 };
307 if let Some(offset) = parse_src_offset(node) {
308 lookup.by_offset.insert(offset, site.clone());
309 }
310
311 lookup
312 .by_name
313 .entry((site.name.clone(), arg_count))
314 .or_insert(site);
315 }
316 }
317 _ => {}
318 }
319
320 for key in crate::goto::CHILD_KEYS {
322 if let Some(child) = node.get(*key) {
323 if child.is_array() {
324 if let Some(arr) = child.as_array() {
325 for item in arr {
326 collect_ast_calls(item, id_index, lookup);
327 }
328 }
329 } else if child.is_object() {
330 collect_ast_calls(child, id_index, lookup);
331 }
332 }
333 }
334}
335
336struct CallInfo {
338 name: String,
340 params: ParamInfo,
342 decl_id: u64,
344}
345
346fn extract_call_info(node: &Value, id_index: &HashMap<u64, &Value>) -> Option<CallInfo> {
348 let args = node.get("arguments")?.as_array()?;
349 if args.is_empty() {
350 return None;
351 }
352
353 let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
355 if kind == "structConstructorCall"
356 && node
357 .get("names")
358 .and_then(|v| v.as_array())
359 .is_some_and(|n| !n.is_empty())
360 {
361 return None;
362 }
363
364 let expr = node.get("expression")?;
365 let decl_id = expr.get("referencedDeclaration").and_then(|v| v.as_u64())?;
366
367 let decl_node = id_index.get(&decl_id)?;
368 let names = get_parameter_names(decl_node)?;
369
370 let func_name = extract_function_name(expr)?;
372
373 let arg_count = node
378 .get("arguments")
379 .and_then(|v| v.as_array())
380 .map(|a| a.len())
381 .unwrap_or(0);
382 let skip = if is_member_access(expr) && arg_count < names.len() {
383 1
384 } else {
385 0
386 };
387
388 Some(CallInfo {
389 name: func_name,
390 params: ParamInfo { names, skip },
391 decl_id,
392 })
393}
394
395fn extract_function_name(expr: &Value) -> Option<String> {
397 let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
398 match node_type {
399 "Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
400 "MemberAccess" => expr
401 .get("memberName")
402 .and_then(|v| v.as_str())
403 .map(String::from),
404 _ => None,
405 }
406}
407
408fn is_member_access(expr: &Value) -> bool {
410 expr.get("nodeType")
411 .and_then(|v| v.as_str())
412 .is_some_and(|t| t == "MemberAccess")
413}
414
415fn lookup_call_site<'a>(
419 lookup: &'a HintLookup,
420 offset: usize,
421 name: &str,
422 arg_count: usize,
423) -> Option<&'a CallSite> {
424 if let Some(site) = lookup.by_offset.get(&offset)
426 && site.name == name
427 {
428 return Some(site);
429 }
430 lookup.by_name.get(&(name.to_string(), arg_count))
432}
433
434fn collect_ts_hints(
436 node: Node,
437 source: &str,
438 range: &Range,
439 lookup: &HintLookup,
440 hints: &mut Vec<InlayHint>,
441) {
442 let node_start = node.start_position();
444 let node_end = node.end_position();
445 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
446 return;
447 }
448
449 match node.kind() {
450 "call_expression" => {
451 emit_call_hints(node, source, lookup, hints);
452 }
453 "emit_statement" => {
454 emit_emit_hints(node, source, lookup, hints);
455 }
456 _ => {}
457 }
458
459 let mut cursor = node.walk();
461 for child in node.children(&mut cursor) {
462 collect_ts_hints(child, source, range, lookup, hints);
463 }
464}
465
466fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
468 let func_name = match ts_call_function_name(node, source) {
469 Some(n) => n,
470 None => return,
471 };
472
473 let args = ts_call_arguments(node);
474 if args.is_empty() {
475 return;
476 }
477
478 let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
479 Some(s) => s,
480 None => return,
481 };
482
483 emit_param_hints(&args, &site.info, hints);
484}
485
486fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
488 let event_name = match ts_emit_event_name(node, source) {
489 Some(n) => n,
490 None => return,
491 };
492
493 let args = ts_call_arguments(node);
494 if args.is_empty() {
495 return;
496 }
497
498 let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
499 Some(s) => s,
500 None => return,
501 };
502
503 emit_param_hints(&args, &site.info, hints);
504}
505
506fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
508 for (i, arg) in args.iter().enumerate() {
509 let pi = i + info.skip;
510 if pi >= info.names.len() || info.names[pi].is_empty() {
511 continue;
512 }
513
514 let start = arg.start_position();
515 let position = Position::new(start.row as u32, start.column as u32);
516
517 hints.push(InlayHint {
518 position,
519 kind: Some(InlayHintKind::PARAMETER),
520 label: InlayHintLabel::String(format!("{}:", info.names[pi])),
521 text_edits: None,
522 tooltip: None,
523 padding_left: None,
524 padding_right: Some(true),
525 data: None,
526 });
527 }
528}
529
530fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
538 let func_expr = node.child_by_field_name("function")?;
539 let inner = first_named_child(func_expr)?;
541 extract_name_from_expr(inner, source)
542}
543
544fn extract_name_from_expr<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
551 match node.kind() {
552 "identifier" => Some(&source[node.byte_range()]),
553 "member_expression" => {
554 let prop = node.child_by_field_name("property")?;
555 Some(&source[prop.byte_range()])
556 }
557 "struct_expression" => {
558 let type_expr = node.child_by_field_name("type")?;
560 extract_name_from_expr(type_expr, source)
561 }
562 "expression" => {
563 let inner = first_named_child(node)?;
565 extract_name_from_expr(inner, source)
566 }
567 _ => None,
568 }
569}
570
571fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
573 let name_expr = node.child_by_field_name("name")?;
574 let inner = first_named_child(name_expr)?;
575 match inner.kind() {
576 "identifier" => Some(&source[inner.byte_range()]),
577 "member_expression" => {
578 let prop = inner.child_by_field_name("property")?;
579 Some(&source[prop.byte_range()])
580 }
581 _ => None,
582 }
583}
584
585fn ts_call_arguments(node: Node) -> Vec<Node> {
588 let mut args = Vec::new();
589 let mut cursor = node.walk();
590 for child in node.children(&mut cursor) {
591 if child.kind() == "call_argument" {
592 args.push(child);
593 }
594 }
595 args
596}
597
598fn first_named_child(node: Node) -> Option<Node> {
600 let mut cursor = node.walk();
601 node.children(&mut cursor).find(|c| c.is_named())
602}
603
604pub struct TsCallContext<'a> {
606 pub name: &'a str,
608 pub arg_index: usize,
610 pub arg_count: usize,
612 pub call_start_byte: usize,
614 pub is_index_access: bool,
617}
618
619pub fn ts_find_call_at_byte<'a>(
624 root: tree_sitter::Node<'a>,
625 source: &'a str,
626 byte_pos: usize,
627) -> Option<TsCallContext<'a>> {
628 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
630
631 loop {
633 if node.kind() == "call_argument" {
634 break;
635 }
636 node = node.parent()?;
637 }
638
639 let call_node = node.parent()?;
641 let args = ts_call_arguments(call_node);
642 let arg_index = args.iter().position(|a| a.id() == node.id())?;
643
644 match call_node.kind() {
645 "call_expression" => {
646 let name = ts_call_function_name(call_node, source)?;
647 Some(TsCallContext {
648 name,
649 arg_index,
650 arg_count: args.len(),
651 call_start_byte: call_node.start_byte(),
652 is_index_access: false,
653 })
654 }
655 "emit_statement" => {
656 let name = ts_emit_event_name(call_node, source)?;
657 Some(TsCallContext {
658 name,
659 arg_index,
660 arg_count: args.len(),
661 call_start_byte: call_node.start_byte(),
662 is_index_access: false,
663 })
664 }
665 _ => None,
666 }
667}
668
669pub fn ts_find_call_for_signature<'a>(
679 root: tree_sitter::Node<'a>,
680 source: &'a str,
681 byte_pos: usize,
682) -> Option<TsCallContext<'a>> {
683 if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
685 return Some(ctx);
686 }
687
688 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
690 loop {
691 match node.kind() {
692 "call_expression" => {
693 let name = ts_call_function_name(node, source)?;
694 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
695 let args = ts_call_arguments(node);
696 let arg_count = args.len().max(arg_index + 1);
697 return Some(TsCallContext {
698 name,
699 arg_index,
700 arg_count,
701 call_start_byte: node.start_byte(),
702 is_index_access: false,
703 });
704 }
705 "emit_statement" => {
706 let name = ts_emit_event_name(node, source)?;
707 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
708 let args = ts_call_arguments(node);
709 let arg_count = args.len().max(arg_index + 1);
710 return Some(TsCallContext {
711 name,
712 arg_index,
713 arg_count,
714 call_start_byte: node.start_byte(),
715 is_index_access: false,
716 });
717 }
718 "array_access" => {
719 let base_node = node.child_by_field_name("base")?;
721 let name_node = if base_node.kind() == "member_expression" {
724 base_node
725 .child_by_field_name("property")
726 .unwrap_or(base_node)
727 } else {
728 base_node
729 };
730 let name = &source[name_node.byte_range()];
731 return Some(TsCallContext {
732 name,
733 arg_index: 0,
734 arg_count: 1,
735 call_start_byte: node.start_byte(),
736 is_index_access: true,
737 });
738 }
739 "source_file" => break,
740 _ => {
741 node = node.parent()?;
742 }
743 }
744 }
745
746 if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
748 return Some(ctx);
749 }
750
751 find_index_by_text_scan(source, byte_pos)
753}
754
755fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
761 let before = &source[..byte_pos.min(source.len())];
762
763 let mut depth: i32 = 0;
765 let mut paren_pos = None;
766 for (i, ch) in before.char_indices().rev() {
767 match ch {
768 ')' => depth += 1,
769 '(' => {
770 if depth == 0 {
771 paren_pos = Some(i);
772 break;
773 }
774 depth -= 1;
775 }
776 _ => {}
777 }
778 }
779 let paren_pos = paren_pos?;
780
781 let mut scan_end = paren_pos;
785 let before_paren = source[..scan_end].trim_end();
786 if before_paren.ends_with('}') {
787 let mut brace_depth: i32 = 0;
789 for (i, ch) in before_paren.char_indices().rev() {
790 match ch {
791 '}' => brace_depth += 1,
792 '{' => {
793 brace_depth -= 1;
794 if brace_depth == 0 {
795 scan_end = i;
796 break;
797 }
798 }
799 _ => {}
800 }
801 }
802 }
803 let before_name = &source[..scan_end];
804 let name_end = before_name.trim_end().len();
805 let name_start = before_name[..name_end]
806 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
807 .map(|i| i + 1)
808 .unwrap_or(0);
809 let raw_name = &source[name_start..name_end];
811 let name = match raw_name.rfind('.') {
812 Some(dot) => &raw_name[dot + 1..],
813 None => raw_name,
814 };
815
816 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
817 return None;
818 }
819
820 let arg_index = count_commas_before(source, paren_pos, byte_pos);
822
823 Some(TsCallContext {
824 name,
825 arg_index,
826 arg_count: arg_index + 1,
827 call_start_byte: name_start,
828 is_index_access: false,
829 })
830}
831
832fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
837 let before = &source[..byte_pos.min(source.len())];
838
839 let mut depth: i32 = 0;
841 let mut bracket_pos = None;
842 for (i, c) in before.char_indices().rev() {
843 match c {
844 ']' => depth += 1,
845 '[' => {
846 if depth == 0 {
847 bracket_pos = Some(i);
848 break;
849 }
850 depth -= 1;
851 }
852 _ => {}
853 }
854 }
855 let bracket_pos = bracket_pos?;
856
857 let before_bracket = &source[..bracket_pos];
859 let name_end = before_bracket.trim_end().len();
860 let name_start = before_bracket[..name_end]
861 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
862 .map(|i| i + 1)
863 .unwrap_or(0);
864 let name = &source[name_start..name_end];
865
866 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
867 return None;
868 }
869
870 Some(TsCallContext {
871 name,
872 arg_index: 0,
873 arg_count: 1,
874 call_start_byte: name_start,
875 is_index_access: true,
876 })
877}
878
879fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
881 let end = byte_pos.min(source.len());
882 let text = &source[start..end];
883
884 let mut count = 0;
885 let mut depth = 0;
886 let mut found_open = false;
887 for ch in text.chars() {
888 match ch {
889 '(' if !found_open => {
890 found_open = true;
891 depth = 1;
892 }
893 '(' => depth += 1,
894 ')' => depth -= 1,
895 ',' if found_open && depth == 1 => count += 1,
896 _ => {}
897 }
898 }
899 count
900}
901
902fn collect_ts_gas_hints(
907 node: Node,
908 source: &str,
909 range: &Range,
910 gas_index: &gas::GasIndex,
911 abs_path: &str,
912 hints: &mut Vec<InlayHint>,
913) {
914 let node_start = node.start_position();
915 let node_end = node.end_position();
916 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
917 return;
918 }
919
920 match node.kind() {
921 "function_definition" => {
922 if let Some(hint) = ts_gas_hint_for_function(node, source, range, gas_index, abs_path) {
923 hints.push(hint);
924 }
925 }
926 "contract_declaration" | "library_declaration" | "interface_declaration" => {
927 if let Some(hint) = ts_gas_hint_for_contract(node, source, range, gas_index, abs_path) {
928 hints.push(hint);
929 }
930 }
931 _ => {}
932 }
933
934 let mut cursor = node.walk();
935 for child in node.children(&mut cursor) {
936 collect_ts_gas_hints(child, source, range, gas_index, abs_path, hints);
937 }
938}
939
940fn ts_node_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
942 let mut cursor = node.walk();
943 node.children(&mut cursor)
944 .find(|c| c.kind() == "identifier" && c.is_named())
945 .map(|c| &source[c.byte_range()])
946}
947
948fn ts_body_open_brace(node: Node, body_kind: &str) -> Option<Position> {
950 let mut cursor = node.walk();
951 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
952 let start = body.start_position();
953 Some(Position::new(start.row as u32, start.column as u32))
954}
955
956fn ts_body_close_brace(node: Node, body_kind: &str) -> Option<Position> {
958 let mut cursor = node.walk();
959 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
960 let end = body.end_position();
961 Some(Position::new(
963 end.row as u32,
964 end.column.saturating_sub(1) as u32,
965 ))
966}
967
968fn ts_enclosing_contract_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
970 let mut parent = node.parent();
971 while let Some(p) = parent {
972 if p.kind() == "contract_declaration"
973 || p.kind() == "library_declaration"
974 || p.kind() == "interface_declaration"
975 {
976 return ts_node_name(p, source);
977 }
978 parent = p.parent();
979 }
980 None
981}
982
983fn find_gas_key<'a>(
985 gas_index: &'a gas::GasIndex,
986 abs_path: &str,
987 contract_name: &str,
988) -> Option<&'a str> {
989 let exact = format!("{abs_path}:{contract_name}");
990 if gas_index.contains_key(&exact) {
991 return Some(gas_index.get_key_value(&exact)?.0.as_str());
992 }
993 let file_name = std::path::Path::new(abs_path).file_name()?.to_str()?;
994 let suffix = format!("{file_name}:{contract_name}");
995 gas_index
996 .keys()
997 .find(|k| k.ends_with(&suffix))
998 .map(|k| k.as_str())
999}
1000
1001fn has_gas_sentinel(node: Node, source: &str) -> bool {
1006 let mut prev = node.prev_named_sibling();
1007 while let Some(sibling) = prev {
1008 if sibling.kind() == "comment" {
1009 let text = &source[sibling.byte_range()];
1010 if text.contains(gas::GAS_SENTINEL) {
1011 return true;
1012 }
1013 } else {
1014 break;
1015 }
1016 prev = sibling.prev_named_sibling();
1017 }
1018 false
1019}
1020
1021fn ts_gas_hint_for_function(
1023 node: Node,
1024 source: &str,
1025 range: &Range,
1026 gas_index: &gas::GasIndex,
1027 abs_path: &str,
1028) -> Option<InlayHint> {
1029 if !has_gas_sentinel(node, source) {
1031 return None;
1032 }
1033 let fn_name = ts_node_name(node, source)?;
1034 let contract_name = ts_enclosing_contract_name(node, source)?;
1035 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
1036 let contract_gas = gas_index.get(gas_key)?;
1037
1038 let prefix = format!("{fn_name}(");
1039 let cost = contract_gas
1040 .external_by_sig
1041 .iter()
1042 .find(|(sig, _)| sig.as_str().starts_with(&prefix))
1043 .map(|(_, c)| c.as_str())
1044 .or_else(|| {
1045 contract_gas
1046 .internal
1047 .iter()
1048 .find(|(sig, _)| sig.starts_with(&prefix))
1049 .map(|(_, c)| c.as_str())
1050 })?;
1051
1052 let (brace_pos, offset) = match FN_GAS_HINT_POSITION {
1054 FnGasHintPosition::Opening => (ts_body_open_brace(node, "function_body")?, 1),
1055 FnGasHintPosition::Closing => (ts_body_close_brace(node, "function_body")?, 1),
1056 };
1057 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1058 return None;
1059 }
1060
1061 Some(InlayHint {
1062 position: Position::new(brace_pos.line, brace_pos.character + offset),
1063 kind: Some(InlayHintKind::TYPE),
1064 label: InlayHintLabel::String(format!("🔥 gas: {}", gas::format_gas(cost))),
1065 text_edits: None,
1066 tooltip: Some(InlayHintTooltip::String("Estimated gas cost".to_string())),
1067 padding_left: Some(true),
1068 padding_right: None,
1069 data: None,
1070 })
1071}
1072
1073fn ts_gas_hint_for_contract(
1076 node: Node,
1077 source: &str,
1078 range: &Range,
1079 gas_index: &gas::GasIndex,
1080 abs_path: &str,
1081) -> Option<InlayHint> {
1082 if !has_gas_sentinel(node, source) {
1084 return None;
1085 }
1086 let contract_name = ts_node_name(node, source)?;
1087 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
1088 let contract_gas = gas_index.get(gas_key)?;
1089
1090 let display_cost = match contract_gas.creation.get("totalCost").map(|s| s.as_str()) {
1092 Some("infinite") | None => contract_gas
1093 .creation
1094 .get("codeDepositCost")
1095 .map(|s| s.as_str())?,
1096 Some(total) => total,
1097 };
1098
1099 let brace_pos = ts_body_open_brace(node, "contract_body")?;
1100 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1101 return None;
1102 }
1103
1104 Some(InlayHint {
1105 position: Position::new(brace_pos.line, brace_pos.character + 1),
1106 kind: Some(InlayHintKind::TYPE),
1107 label: InlayHintLabel::String(format!("🔥 deploy: {} ", gas::format_gas(display_cost))),
1108 text_edits: None,
1109 tooltip: Some(InlayHintTooltip::String(format!(
1110 "Deploy cost — code deposit: {}, execution: {}",
1111 gas::format_gas(
1112 contract_gas
1113 .creation
1114 .get("codeDepositCost")
1115 .map(|s| s.as_str())
1116 .unwrap_or("?")
1117 ),
1118 gas::format_gas(
1119 contract_gas
1120 .creation
1121 .get("executionCost")
1122 .map(|s| s.as_str())
1123 .unwrap_or("?")
1124 )
1125 ))),
1126 padding_left: Some(true),
1127 padding_right: None,
1128 data: None,
1129 })
1130}
1131
1132fn get_parameter_names(decl: &Value) -> Option<Vec<String>> {
1136 let items = decl
1139 .get("parameters")
1140 .and_then(|p| p.get("parameters"))
1141 .and_then(|v| v.as_array())
1142 .or_else(|| decl.get("members").and_then(|v| v.as_array()))?;
1143 Some(
1144 items
1145 .iter()
1146 .map(|p| {
1147 p.get("name")
1148 .and_then(|v| v.as_str())
1149 .unwrap_or("")
1150 .to_string()
1151 })
1152 .collect(),
1153 )
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159
1160 #[test]
1161 fn test_gas_sentinel_present() {
1162 let source = r#"
1163contract Foo {
1164 /// @custom:lsp-enable gas-estimates
1165 function bar() public {}
1166}
1167"#;
1168 let tree = ts_parse(source).unwrap();
1169 let root = tree.root_node();
1170 let contract = root.child(0).unwrap();
1172 let body = contract.child_by_field_name("body").unwrap();
1173 let mut cursor = body.walk();
1174 let fn_node = body
1175 .children(&mut cursor)
1176 .find(|c| c.kind() == "function_definition")
1177 .unwrap();
1178 assert!(has_gas_sentinel(fn_node, source));
1179 }
1180
1181 #[test]
1182 fn test_gas_sentinel_absent() {
1183 let source = r#"
1184contract Foo {
1185 function bar() public {}
1186}
1187"#;
1188 let tree = ts_parse(source).unwrap();
1189 let root = tree.root_node();
1190 let contract = root.child(0).unwrap();
1191 let body = contract.child_by_field_name("body").unwrap();
1192 let mut cursor = body.walk();
1193 let fn_node = body
1194 .children(&mut cursor)
1195 .find(|c| c.kind() == "function_definition")
1196 .unwrap();
1197 assert!(!has_gas_sentinel(fn_node, source));
1198 }
1199
1200 #[test]
1201 fn test_gas_sentinel_with_other_natspec() {
1202 let source = r#"
1203contract Foo {
1204 /// @notice Does something
1205 /// @custom:lsp-enable gas-estimates
1206 function bar() public {}
1207}
1208"#;
1209 let tree = ts_parse(source).unwrap();
1210 let root = tree.root_node();
1211 let contract = root.child(0).unwrap();
1212 let body = contract.child_by_field_name("body").unwrap();
1213 let mut cursor = body.walk();
1214 let fn_node = body
1215 .children(&mut cursor)
1216 .find(|c| c.kind() == "function_definition")
1217 .unwrap();
1218 assert!(has_gas_sentinel(fn_node, source));
1219 }
1220
1221 #[test]
1222 fn test_get_parameter_names() {
1223 let decl: Value = serde_json::json!({
1224 "parameters": {
1225 "parameters": [
1226 {"name": "to", "nodeType": "VariableDeclaration"},
1227 {"name": "amount", "nodeType": "VariableDeclaration"},
1228 ]
1229 }
1230 });
1231 let names = get_parameter_names(&decl).unwrap();
1232 assert_eq!(names, vec!["to", "amount"]);
1233 }
1234
1235 #[test]
1236 fn test_ts_call_function_name() {
1237 let source = r#"
1238contract Foo {
1239 function bar(uint x) public {}
1240 function test() public {
1241 bar(42);
1242 }
1243}
1244"#;
1245 let tree = ts_parse(source).unwrap();
1246 let mut found = Vec::new();
1247 find_calls(tree.root_node(), source, &mut found);
1248 assert_eq!(found.len(), 1);
1249 assert_eq!(found[0], "bar");
1250 }
1251
1252 #[test]
1253 fn test_ts_member_call_name() {
1254 let source = r#"
1255contract Foo {
1256 function test() public {
1257 PRICE.addTax(TAX, TAX_BASE);
1258 }
1259}
1260"#;
1261 let tree = ts_parse(source).unwrap();
1262 let mut found = Vec::new();
1263 find_calls(tree.root_node(), source, &mut found);
1264 assert_eq!(found.len(), 1);
1265 assert_eq!(found[0], "addTax");
1266 }
1267
1268 #[test]
1269 fn test_ts_call_with_value_modifier() {
1270 let source = r#"
1271contract Foo {
1272 function test() public {
1273 router.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
1274 }
1275}
1276"#;
1277 let tree = ts_parse(source).unwrap();
1278 let mut found = Vec::new();
1279 find_calls(tree.root_node(), source, &mut found);
1280 assert_eq!(found.len(), 1, "should find one call");
1281 assert_eq!(
1282 found[0], "swap",
1283 "should extract 'swap' through struct_expression"
1284 );
1285 }
1286
1287 #[test]
1288 fn test_ts_call_simple_with_value_modifier() {
1289 let source = r#"
1290contract Foo {
1291 function test() public {
1292 foo{value: 1 ether}(42);
1293 }
1294}
1295"#;
1296 let tree = ts_parse(source).unwrap();
1297 let mut found = Vec::new();
1298 find_calls(tree.root_node(), source, &mut found);
1299 assert_eq!(found.len(), 1, "should find one call");
1300 assert_eq!(
1301 found[0], "foo",
1302 "should extract 'foo' through struct_expression"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_ts_call_with_gas_modifier() {
1308 let source = r#"
1309contract Foo {
1310 function test() public {
1311 addr.call{gas: 5000, value: 1 ether}("");
1312 }
1313}
1314"#;
1315 let tree = ts_parse(source).unwrap();
1316 let mut found = Vec::new();
1317 find_calls(tree.root_node(), source, &mut found);
1318 assert_eq!(found.len(), 1, "should find one call");
1319 assert_eq!(
1320 found[0], "call",
1321 "should extract 'call' through struct_expression"
1322 );
1323 }
1324
1325 #[test]
1326 fn test_find_call_by_text_scan_with_value_modifier() {
1327 let source = "router.swap{value: 100}(nativeKey, SWAP_PARAMS)";
1329 let byte_pos = source.find("SWAP_PARAMS").unwrap();
1331 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1332 assert_eq!(ctx.name, "swap");
1333 assert_eq!(ctx.arg_index, 1);
1334 }
1335
1336 #[test]
1337 fn test_find_call_by_text_scan_simple_value_modifier() {
1338 let source = "foo{value: 1 ether}(42)";
1339 let byte_pos = source.find("42").unwrap();
1340 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1341 assert_eq!(ctx.name, "foo");
1342 assert_eq!(ctx.arg_index, 0);
1343 }
1344
1345 #[test]
1346 fn test_ts_emit_event_name() {
1347 let source = r#"
1348contract Foo {
1349 event Purchase(address buyer, uint256 price);
1350 function test() public {
1351 emit Purchase(msg.sender, 100);
1352 }
1353}
1354"#;
1355 let tree = ts_parse(source).unwrap();
1356 let mut found = Vec::new();
1357 find_emits(tree.root_node(), source, &mut found);
1358 assert_eq!(found.len(), 1);
1359 assert_eq!(found[0], "Purchase");
1360 }
1361
1362 #[test]
1363 fn test_ts_call_arguments_count() {
1364 let source = r#"
1365contract Foo {
1366 function bar(uint x, uint y) public {}
1367 function test() public {
1368 bar(1, 2);
1369 }
1370}
1371"#;
1372 let tree = ts_parse(source).unwrap();
1373 let mut arg_counts = Vec::new();
1374 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1375 assert_eq!(arg_counts, vec![2]);
1376 }
1377
1378 #[test]
1379 fn test_ts_argument_positions_follow_live_buffer() {
1380 let source = r#"
1382contract Foo {
1383 function bar(uint x, uint y) public {}
1384 function test() public {
1385 bar(
1386 1,
1387 2
1388 );
1389 }
1390}
1391"#;
1392 let tree = ts_parse(source).unwrap();
1393 let mut positions = Vec::new();
1394 find_arg_positions(tree.root_node(), &mut positions);
1395 assert_eq!(positions.len(), 2);
1397 assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
1400
1401 fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1404 if node.kind() == "call_expression"
1405 && let Some(name) = ts_call_function_name(node, source)
1406 {
1407 out.push(name);
1408 }
1409 let mut cursor = node.walk();
1410 for child in node.children(&mut cursor) {
1411 find_calls(child, source, out);
1412 }
1413 }
1414
1415 fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1416 if node.kind() == "emit_statement"
1417 && let Some(name) = ts_emit_event_name(node, source)
1418 {
1419 out.push(name);
1420 }
1421 let mut cursor = node.walk();
1422 for child in node.children(&mut cursor) {
1423 find_emits(child, source, out);
1424 }
1425 }
1426
1427 fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
1428 if node.kind() == "call_expression" {
1429 out.push(ts_call_arguments(node).len());
1430 }
1431 let mut cursor = node.walk();
1432 for child in node.children(&mut cursor) {
1433 find_call_arg_counts(child, out);
1434 }
1435 }
1436
1437 fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
1438 if node.kind() == "call_expression" {
1439 for arg in ts_call_arguments(node) {
1440 let p = arg.start_position();
1441 out.push((p.row, p.column));
1442 }
1443 }
1444 let mut cursor = node.walk();
1445 for child in node.children(&mut cursor) {
1446 find_arg_positions(child, out);
1447 }
1448 }
1449
1450 #[test]
1451 fn test_ts_find_call_at_byte_first_arg() {
1452 let source = r#"
1453contract Foo {
1454 function bar(uint x, uint y) public {}
1455 function test() public {
1456 bar(42, 99);
1457 }
1458}
1459"#;
1460 let tree = ts_parse(source).unwrap();
1461 let pos_42 = source.find("42").unwrap();
1463 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1464 assert_eq!(ctx.name, "bar");
1465 assert_eq!(ctx.arg_index, 0);
1466 assert_eq!(ctx.arg_count, 2);
1467 }
1468
1469 #[test]
1470 fn test_ts_find_call_at_byte_second_arg() {
1471 let source = r#"
1472contract Foo {
1473 function bar(uint x, uint y) public {}
1474 function test() public {
1475 bar(42, 99);
1476 }
1477}
1478"#;
1479 let tree = ts_parse(source).unwrap();
1480 let pos_99 = source.find("99").unwrap();
1481 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1482 assert_eq!(ctx.name, "bar");
1483 assert_eq!(ctx.arg_index, 1);
1484 assert_eq!(ctx.arg_count, 2);
1485 }
1486
1487 #[test]
1488 fn test_ts_find_call_at_byte_outside_call_returns_none() {
1489 let source = r#"
1490contract Foo {
1491 function bar(uint x) public {}
1492 function test() public {
1493 uint z = 10;
1494 bar(42);
1495 }
1496}
1497"#;
1498 let tree = ts_parse(source).unwrap();
1499 let pos_10 = source.find("10").unwrap();
1501 assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1502 }
1503
1504 #[test]
1505 fn test_ts_find_call_at_byte_member_call() {
1506 let source = r#"
1507contract Foo {
1508 function test() public {
1509 PRICE.addTax(TAX, TAX_BASE);
1510 }
1511}
1512"#;
1513 let tree = ts_parse(source).unwrap();
1514 let pos_tax = source.find("TAX,").unwrap();
1515 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1516 assert_eq!(ctx.name, "addTax");
1517 assert_eq!(ctx.arg_index, 0);
1518 assert_eq!(ctx.arg_count, 2);
1519 }
1520
1521 #[test]
1522 fn test_ts_find_call_at_byte_emit_statement() {
1523 let source = r#"
1524contract Foo {
1525 event Purchase(address buyer, uint256 price);
1526 function test() public {
1527 emit Purchase(msg.sender, 100);
1528 }
1529}
1530"#;
1531 let tree = ts_parse(source).unwrap();
1532 let pos_100 = source.find("100").unwrap();
1533 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1534 assert_eq!(ctx.name, "Purchase");
1535 assert_eq!(ctx.arg_index, 1);
1536 assert_eq!(ctx.arg_count, 2);
1537 }
1538
1539 #[test]
1540 fn test_ts_find_call_at_byte_multiline() {
1541 let source = r#"
1542contract Foo {
1543 function bar(uint x, uint y, uint z) public {}
1544 function test() public {
1545 bar(
1546 1,
1547 2,
1548 3
1549 );
1550 }
1551}
1552"#;
1553 let tree = ts_parse(source).unwrap();
1554 let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1558 assert_eq!(ctx.name, "bar");
1559 assert_eq!(ctx.arg_index, 1);
1560 assert_eq!(ctx.arg_count, 3);
1561 }
1562
1563 #[test]
1564 fn test_resolve_callsite_param_basic() {
1565 let mut lookup = HintLookup {
1567 by_offset: HashMap::new(),
1568 by_name: HashMap::new(),
1569 };
1570 lookup.by_name.insert(
1571 ("transfer".to_string(), 2),
1572 CallSite {
1573 info: ParamInfo {
1574 names: vec!["to".to_string(), "amount".to_string()],
1575 skip: 0,
1576 },
1577 name: "transfer".to_string(),
1578 decl_id: 42,
1579 },
1580 );
1581
1582 let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1584 assert_eq!(result.param_name, "to");
1585 assert_eq!(result.decl_id, 42);
1586
1587 let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1589 assert_eq!(result.param_name, "amount");
1590 assert_eq!(result.decl_id, 42);
1591 }
1592
1593 #[test]
1594 fn test_resolve_callsite_param_with_skip() {
1595 let mut lookup = HintLookup {
1597 by_offset: HashMap::new(),
1598 by_name: HashMap::new(),
1599 };
1600 lookup.by_name.insert(
1601 ("addTax".to_string(), 2),
1602 CallSite {
1603 info: ParamInfo {
1604 names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1605 skip: 1,
1606 },
1607 name: "addTax".to_string(),
1608 decl_id: 99,
1609 },
1610 );
1611
1612 let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1614 assert_eq!(result.param_name, "tax");
1615
1616 let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1618 assert_eq!(result.param_name, "base");
1619 }
1620
1621 #[test]
1622 fn test_resolve_callsite_param_out_of_bounds() {
1623 let mut lookup = HintLookup {
1624 by_offset: HashMap::new(),
1625 by_name: HashMap::new(),
1626 };
1627 lookup.by_name.insert(
1628 ("foo".to_string(), 1),
1629 CallSite {
1630 info: ParamInfo {
1631 names: vec!["x".to_string()],
1632 skip: 0,
1633 },
1634 name: "foo".to_string(),
1635 decl_id: 1,
1636 },
1637 );
1638
1639 assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1641 }
1642
1643 #[test]
1644 fn test_resolve_callsite_param_unknown_function() {
1645 let lookup = HintLookup {
1646 by_offset: HashMap::new(),
1647 by_name: HashMap::new(),
1648 };
1649 assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1650 }
1651
1652 #[test]
1653 fn test_ts_find_call_at_byte_emit_member_access() {
1654 let source = r#"
1658contract Foo {
1659 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1660 function test() public {
1661 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1662 }
1663}
1664"#;
1665 let tree = ts_parse(source).unwrap();
1666 let params_tick = source.find("params.tickLower,").unwrap();
1668 let tick_lower_pos = params_tick + "params.".len(); let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1671 assert_eq!(ctx.name, "ModifyLiquidity");
1672 assert_eq!(
1673 ctx.arg_index, 2,
1674 "params.tickLower is the 3rd argument (index 2)"
1675 );
1676 assert_eq!(ctx.arg_count, 4);
1677 }
1678
1679 #[test]
1680 fn test_ts_find_call_at_byte_member_access_on_property() {
1681 let source = r#"
1683contract Foo {
1684 event Transfer(address from, address to);
1685 function test() public {
1686 emit Transfer(msg.sender, addr);
1687 }
1688}
1689"#;
1690 let tree = ts_parse(source).unwrap();
1691 let sender_pos = source.find("sender").unwrap();
1692 let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1693 assert_eq!(ctx.name, "Transfer");
1694 assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1695 }
1696
1697 #[test]
1698 fn test_ts_find_call_at_byte_emit_all_args() {
1699 let source = r#"
1701contract Foo {
1702 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1703 function test() public {
1704 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1705 }
1706}
1707"#;
1708 let tree = ts_parse(source).unwrap();
1709
1710 let pos_id = source.find("(id,").unwrap() + 1;
1712 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1713 assert_eq!(ctx.name, "ModifyLiquidity");
1714 assert_eq!(ctx.arg_index, 0);
1715
1716 let pos_msg = source.find("msg.sender").unwrap();
1718 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1719 assert_eq!(ctx.arg_index, 1);
1720
1721 let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1723 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1724 assert_eq!(ctx.arg_index, 2);
1725
1726 let pos_tu = source.find("params.tickUpper").unwrap();
1728 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1729 assert_eq!(ctx.arg_index, 3);
1730 }
1731
1732 #[test]
1733 fn test_ts_find_call_at_byte_nested_call_arg() {
1734 let source = r#"
1737contract Foo {
1738 function inner(uint x) public returns (uint) {}
1739 function outer(uint a, uint b) public {}
1740 function test() public {
1741 outer(inner(42), 99);
1742 }
1743}
1744"#;
1745 let tree = ts_parse(source).unwrap();
1746
1747 let pos_42 = source.find("42").unwrap();
1749 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1750 assert_eq!(ctx.name, "inner");
1751 assert_eq!(ctx.arg_index, 0);
1752
1753 let pos_99 = source.find("99").unwrap();
1755 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1756 assert_eq!(ctx.name, "outer");
1757 assert_eq!(ctx.arg_index, 1);
1758 }
1759
1760 #[test]
1761 fn test_ts_find_call_for_signature_incomplete_call() {
1762 let source = r#"
1764contract Foo {
1765 function bar(uint x, uint y) public {}
1766 function test() public {
1767 bar(
1768 }
1769}
1770"#;
1771 let tree = ts_parse(source).unwrap();
1772 let pos = source.find("bar(").unwrap() + 4;
1773 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1774 assert_eq!(ctx.name, "bar");
1775 assert_eq!(ctx.arg_index, 0);
1776 }
1777
1778 #[test]
1779 fn test_ts_find_call_for_signature_after_comma() {
1780 let source = r#"
1782contract Foo {
1783 function bar(uint x, uint y) public {}
1784 function test() public {
1785 bar(42,
1786 }
1787}
1788"#;
1789 let tree = ts_parse(source).unwrap();
1790 let pos = source.find("42,").unwrap() + 3;
1791 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1792 assert_eq!(ctx.name, "bar");
1793 assert_eq!(ctx.arg_index, 1);
1794 }
1795
1796 #[test]
1797 fn test_ts_find_call_for_signature_complete_call() {
1798 let source = r#"
1800contract Foo {
1801 function bar(uint x, uint y) public {}
1802 function test() public {
1803 bar(42, 99);
1804 }
1805}
1806"#;
1807 let tree = ts_parse(source).unwrap();
1808 let pos = source.find("42").unwrap();
1809 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1810 assert_eq!(ctx.name, "bar");
1811 assert_eq!(ctx.arg_index, 0);
1812 }
1813
1814 #[test]
1815 fn test_ts_find_call_for_signature_member_call() {
1816 let source = r#"
1818contract Foo {
1819 function test() public {
1820 PRICE.addTax(
1821 }
1822}
1823"#;
1824 let tree = ts_parse(source).unwrap();
1825 let pos = source.find("addTax(").unwrap() + 7;
1826 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1827 assert_eq!(ctx.name, "addTax");
1828 assert_eq!(ctx.arg_index, 0);
1829 }
1830
1831 #[test]
1832 fn test_ts_find_call_for_signature_array_access() {
1833 let source = r#"
1835contract Foo {
1836 mapping(bytes32 => uint256) public orders;
1837 function test() public {
1838 orders[orderId];
1839 }
1840}
1841"#;
1842 let tree = ts_parse(source).unwrap();
1843 let pos = source.find("[orderId]").unwrap() + 1;
1845 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1846 assert_eq!(ctx.name, "orders");
1847 assert_eq!(ctx.arg_index, 0);
1848 assert!(ctx.is_index_access);
1849 }
1850
1851 #[test]
1852 fn test_ts_find_call_for_signature_array_access_empty() {
1853 let source = r#"
1855contract Foo {
1856 mapping(bytes32 => uint256) public orders;
1857 function test() public {
1858 orders[
1859 }
1860}
1861"#;
1862 let tree = ts_parse(source).unwrap();
1863 let pos = source.find("orders[").unwrap() + 7;
1864 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1865 assert_eq!(ctx.name, "orders");
1866 assert!(ctx.is_index_access);
1867 }
1868}