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
119#[derive(Debug, Clone)]
123pub struct ConstructorInfo {
124 pub constructor_id: u64,
125 pub contract_name: String,
126 pub param_names: Vec<String>,
127}
128
129pub type ConstructorIndex = HashMap<u64, ConstructorInfo>;
131
132pub fn build_constructor_index(
136 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
137) -> ConstructorIndex {
138 let mut index = HashMap::with_capacity(decl_index.len() / 8);
139 for (id, decl) in decl_index {
140 if decl.is_constructor()
141 && let Some(scope) = decl.scope()
142 {
143 let names = decl.param_names().unwrap_or_default();
144 let contract_name = decl_index
146 .get(&scope)
147 .map(|d| d.name().to_string())
148 .unwrap_or_default();
149 index.insert(
150 scope as u64,
151 ConstructorInfo {
152 constructor_id: *id as u64,
153 contract_name,
154 param_names: names,
155 },
156 );
157 }
158 }
159 index
160}
161
162pub fn build_hint_index(
167 sources: &Value,
168 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
169 constructor_index: &ConstructorIndex,
170) -> HintIndex {
171 let source_count = sources.as_object().map_or(0, |obj| obj.len());
172 let mut hint_index = HashMap::with_capacity(source_count);
173
174 if let Some(obj) = sources.as_object() {
175 for (_, source_data) in obj {
176 if let Some(ast) = source_data.get("ast")
177 && let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
178 {
179 let lookup = build_hint_lookup(ast, decl_index, constructor_index);
180 hint_index.insert(abs_path.to_string(), lookup);
181 }
182 }
183 }
184
185 hint_index
186}
187
188pub fn inlay_hints(
194 build: &CachedBuild,
195 uri: &Url,
196 range: Range,
197 live_source: &[u8],
198) -> Vec<InlayHint> {
199 let path_str = match uri.to_file_path() {
200 Ok(p) => p.to_str().unwrap_or("").to_string(),
201 Err(_) => return vec![],
202 };
203
204 let abs = match build
205 .path_to_abs
206 .iter()
207 .find(|(k, _)| path_str.ends_with(k.as_str()))
208 {
209 Some((_, v)) => v.clone(),
210 None => return vec![],
211 };
212
213 let lookup = match build.hint_index.get(&abs) {
215 Some(l) => l,
216 None => return vec![],
217 };
218
219 let source_str = String::from_utf8_lossy(live_source);
221 let tree = match ts_parse(&source_str) {
222 Some(t) => t,
223 None => return vec![],
224 };
225
226 let mut hints = Vec::new();
227 collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
228
229 if !build.gas_index.is_empty() {
231 collect_ts_gas_hints(
232 tree.root_node(),
233 &source_str,
234 &range,
235 &build.gas_index,
236 &abs,
237 &mut hints,
238 );
239 }
240
241 hints
242}
243
244pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
246 let mut parser = Parser::new();
247 parser
248 .set_language(&tree_sitter_solidity::LANGUAGE.into())
249 .expect("failed to load Solidity grammar");
250 parser.parse(source, None)
251}
252
253fn build_hint_lookup(
255 file_ast: &Value,
256 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
257 constructor_index: &ConstructorIndex,
258) -> HintLookup {
259 let mut lookup = HintLookup {
260 by_offset: HashMap::new(),
261 by_name: HashMap::new(),
262 };
263 collect_ast_calls(file_ast, decl_index, constructor_index, &mut lookup);
264 lookup
265}
266
267fn parse_src_offset(node: &Value) -> Option<usize> {
269 let src = node.get("src").and_then(|v| v.as_str())?;
270 SourceLoc::parse(src).map(|loc| loc.offset)
271}
272
273fn collect_ast_calls(
275 node: &Value,
276 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
277 constructor_index: &ConstructorIndex,
278 lookup: &mut HintLookup,
279) {
280 let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
281
282 match node_type {
283 "FunctionCall" => {
284 if let Some(call_info) = extract_call_info(node, decl_index, constructor_index) {
285 let arg_count = node
286 .get("arguments")
287 .and_then(|v| v.as_array())
288 .map(|a| a.len())
289 .unwrap_or(0);
290 let site = CallSite {
291 info: ParamInfo {
292 names: call_info.params.names,
293 skip: call_info.params.skip,
294 },
295 name: call_info.name,
296 decl_id: call_info.decl_id,
297 };
298 if let Some(offset) = parse_src_offset(node) {
299 lookup.by_offset.insert(offset, site.clone());
300 }
301
302 lookup
303 .by_name
304 .entry((site.name.clone(), arg_count))
305 .or_insert(site);
306 }
307 }
308 "EmitStatement" => {
309 if let Some(event_call) = node.get("eventCall")
310 && let Some(call_info) =
311 extract_call_info(event_call, decl_index, constructor_index)
312 {
313 let arg_count = event_call
314 .get("arguments")
315 .and_then(|v| v.as_array())
316 .map(|a| a.len())
317 .unwrap_or(0);
318 let site = CallSite {
319 info: ParamInfo {
320 names: call_info.params.names,
321 skip: call_info.params.skip,
322 },
323 name: call_info.name,
324 decl_id: call_info.decl_id,
325 };
326 if let Some(offset) = parse_src_offset(node) {
327 lookup.by_offset.insert(offset, site.clone());
328 }
329
330 lookup
331 .by_name
332 .entry((site.name.clone(), arg_count))
333 .or_insert(site);
334 }
335 }
336 _ => {}
337 }
338
339 for key in crate::goto::CHILD_KEYS {
341 if let Some(child) = node.get(*key) {
342 if child.is_array() {
343 if let Some(arr) = child.as_array() {
344 for item in arr {
345 collect_ast_calls(item, decl_index, constructor_index, lookup);
346 }
347 }
348 } else if child.is_object() {
349 collect_ast_calls(child, decl_index, constructor_index, lookup);
350 }
351 }
352 }
353}
354
355struct CallInfo {
357 name: String,
359 params: ParamInfo,
361 decl_id: u64,
363}
364
365fn extract_call_info(
367 node: &Value,
368 decl_index: &HashMap<i64, crate::solc_ast::DeclNode>,
369 constructor_index: &ConstructorIndex,
370) -> Option<CallInfo> {
371 let args = node.get("arguments")?.as_array()?;
372 if args.is_empty() {
373 return None;
374 }
375
376 let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
378 if kind == "structConstructorCall"
379 && node
380 .get("names")
381 .and_then(|v| v.as_array())
382 .is_some_and(|n| !n.is_empty())
383 {
384 return None;
385 }
386
387 let expr = node.get("expression")?;
388 let expr_type = expr.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
389
390 if expr_type == "NewExpression" {
394 return extract_new_expression_call_info(expr, args.len(), constructor_index);
395 }
396
397 let decl_id = expr.get("referencedDeclaration").and_then(|v| v.as_u64())?;
398
399 let decl = decl_index.get(&(decl_id as i64))?;
400 let names = decl.param_names()?;
401
402 let func_name = extract_function_name(expr)?;
404
405 let arg_count = node
410 .get("arguments")
411 .and_then(|v| v.as_array())
412 .map(|a| a.len())
413 .unwrap_or(0);
414 let skip = if is_member_access(expr) && arg_count < names.len() {
415 1
416 } else {
417 0
418 };
419
420 Some(CallInfo {
421 name: func_name,
422 params: ParamInfo { names, skip },
423 decl_id,
424 })
425}
426
427fn extract_new_expression_call_info(
434 new_expr: &Value,
435 _arg_count: usize,
436 constructor_index: &ConstructorIndex,
437) -> Option<CallInfo> {
438 let type_name = new_expr.get("typeName")?;
439 let contract_id = type_name
440 .get("referencedDeclaration")
441 .and_then(|v| v.as_u64())?;
442
443 let info = constructor_index.get(&contract_id)?;
444
445 let contract_name = type_name
447 .get("pathNode")
448 .and_then(|p| p.get("name"))
449 .and_then(|v| v.as_str())
450 .map(|s| s.to_string())
451 .unwrap_or_else(|| info.contract_name.clone());
452
453 Some(CallInfo {
454 name: contract_name,
455 params: ParamInfo {
456 names: info.param_names.clone(),
457 skip: 0,
458 },
459 decl_id: info.constructor_id,
460 })
461}
462
463fn extract_function_name(expr: &Value) -> Option<String> {
465 let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
466 match node_type {
467 "Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
468 "MemberAccess" => expr
469 .get("memberName")
470 .and_then(|v| v.as_str())
471 .map(String::from),
472 "NewExpression" => expr
473 .get("typeName")
474 .and_then(|t| {
475 t.get("pathNode")
476 .and_then(|p| p.get("name"))
477 .and_then(|v| v.as_str())
478 .or_else(|| t.get("name").and_then(|v| v.as_str()))
479 })
480 .map(String::from),
481 _ => None,
482 }
483}
484
485fn is_member_access(expr: &Value) -> bool {
487 expr.get("nodeType")
488 .and_then(|v| v.as_str())
489 .is_some_and(|t| t == "MemberAccess")
490}
491
492fn lookup_call_site<'a>(
496 lookup: &'a HintLookup,
497 offset: usize,
498 name: &str,
499 arg_count: usize,
500) -> Option<&'a CallSite> {
501 if let Some(site) = lookup.by_offset.get(&offset)
503 && site.name == name
504 {
505 return Some(site);
506 }
507 lookup.by_name.get(&(name.to_string(), arg_count))
509}
510
511fn collect_ts_hints(
513 node: Node,
514 source: &str,
515 range: &Range,
516 lookup: &HintLookup,
517 hints: &mut Vec<InlayHint>,
518) {
519 let node_start = node.start_position();
521 let node_end = node.end_position();
522 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
523 return;
524 }
525
526 match node.kind() {
527 "call_expression" => {
528 emit_call_hints(node, source, lookup, hints);
529 }
530 "emit_statement" => {
531 emit_emit_hints(node, source, lookup, hints);
532 }
533 _ => {}
534 }
535
536 let mut cursor = node.walk();
538 for child in node.children(&mut cursor) {
539 collect_ts_hints(child, source, range, lookup, hints);
540 }
541}
542
543fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
545 let func_name = match ts_call_function_name(node, source) {
546 Some(n) => n,
547 None => return,
548 };
549
550 let args = ts_call_arguments(node);
551 if args.is_empty() {
552 return;
553 }
554
555 let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
556 Some(s) => s,
557 None => return,
558 };
559
560 emit_param_hints(&args, &site.info, hints);
561}
562
563fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
565 let event_name = match ts_emit_event_name(node, source) {
566 Some(n) => n,
567 None => return,
568 };
569
570 let args = ts_call_arguments(node);
571 if args.is_empty() {
572 return;
573 }
574
575 let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
576 Some(s) => s,
577 None => return,
578 };
579
580 emit_param_hints(&args, &site.info, hints);
581}
582
583fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
585 for (i, arg) in args.iter().enumerate() {
586 let pi = i + info.skip;
587 if pi >= info.names.len() || info.names[pi].is_empty() {
588 continue;
589 }
590
591 let start = arg.start_position();
592 let position = Position::new(start.row as u32, start.column as u32);
593
594 hints.push(InlayHint {
595 position,
596 kind: Some(InlayHintKind::PARAMETER),
597 label: InlayHintLabel::String(format!("{}:", info.names[pi])),
598 text_edits: None,
599 tooltip: None,
600 padding_left: None,
601 padding_right: Some(true),
602 data: None,
603 });
604 }
605}
606
607fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
615 let func_expr = node.child_by_field_name("function")?;
616 let inner = first_named_child(func_expr)?;
618 extract_name_from_expr(inner, source)
619}
620
621fn extract_name_from_expr<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
628 match node.kind() {
629 "identifier" => Some(&source[node.byte_range()]),
630 "member_expression" => {
631 let prop = node.child_by_field_name("property")?;
632 Some(&source[prop.byte_range()])
633 }
634 "struct_expression" => {
635 let type_expr = node.child_by_field_name("type")?;
637 extract_name_from_expr(type_expr, source)
638 }
639 "new_expression" => {
640 ts_new_expression_name(node, source)
642 }
643 "expression" => {
644 let inner = first_named_child(node)?;
646 extract_name_from_expr(inner, source)
647 }
648 _ => None,
649 }
650}
651
652fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
654 let name_expr = node.child_by_field_name("name")?;
655 let inner = first_named_child(name_expr)?;
656 match inner.kind() {
657 "identifier" => Some(&source[inner.byte_range()]),
658 "member_expression" => {
659 let prop = inner.child_by_field_name("property")?;
660 Some(&source[prop.byte_range()])
661 }
662 _ => None,
663 }
664}
665
666fn ts_new_expression_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
671 let name_node = node.child_by_field_name("name")?;
672 if name_node.kind() == "user_defined_type" || name_node.kind() == "type_name" {
674 let mut cursor = name_node.walk();
676 for child in name_node.children(&mut cursor) {
677 if child.kind() == "identifier" {
678 return Some(&source[child.byte_range()]);
679 }
680 }
681 Some(&source[name_node.byte_range()])
683 } else {
684 Some(&source[name_node.byte_range()])
685 }
686}
687
688fn ts_call_arguments(node: Node) -> Vec<Node> {
691 let mut args = Vec::new();
692 let mut cursor = node.walk();
693 for child in node.children(&mut cursor) {
694 if child.kind() == "call_argument" {
695 args.push(child);
696 }
697 }
698 args
699}
700
701fn first_named_child(node: Node) -> Option<Node> {
703 let mut cursor = node.walk();
704 node.children(&mut cursor).find(|c| c.is_named())
705}
706
707pub struct TsCallContext<'a> {
709 pub name: &'a str,
711 pub arg_index: usize,
713 pub arg_count: usize,
715 pub call_start_byte: usize,
717 pub is_index_access: bool,
720}
721
722pub fn ts_find_call_at_byte<'a>(
727 root: tree_sitter::Node<'a>,
728 source: &'a str,
729 byte_pos: usize,
730) -> Option<TsCallContext<'a>> {
731 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
733
734 loop {
736 if node.kind() == "call_argument" {
737 break;
738 }
739 node = node.parent()?;
740 }
741
742 let call_node = node.parent()?;
744 let args = ts_call_arguments(call_node);
745 let arg_index = args.iter().position(|a| a.id() == node.id())?;
746
747 match call_node.kind() {
748 "call_expression" => {
749 let name = ts_call_function_name(call_node, source)?;
750 Some(TsCallContext {
751 name,
752 arg_index,
753 arg_count: args.len(),
754 call_start_byte: call_node.start_byte(),
755 is_index_access: false,
756 })
757 }
758 "emit_statement" => {
759 let name = ts_emit_event_name(call_node, source)?;
760 Some(TsCallContext {
761 name,
762 arg_index,
763 arg_count: args.len(),
764 call_start_byte: call_node.start_byte(),
765 is_index_access: false,
766 })
767 }
768 _ => None,
769 }
770}
771
772pub fn ts_find_call_for_signature<'a>(
782 root: tree_sitter::Node<'a>,
783 source: &'a str,
784 byte_pos: usize,
785) -> Option<TsCallContext<'a>> {
786 if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
788 return Some(ctx);
789 }
790
791 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
793 loop {
794 match node.kind() {
795 "call_expression" => {
796 let name = ts_call_function_name(node, source)?;
797 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
798 let args = ts_call_arguments(node);
799 let arg_count = args.len().max(arg_index + 1);
800 return Some(TsCallContext {
801 name,
802 arg_index,
803 arg_count,
804 call_start_byte: node.start_byte(),
805 is_index_access: false,
806 });
807 }
808 "emit_statement" => {
809 let name = ts_emit_event_name(node, source)?;
810 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
811 let args = ts_call_arguments(node);
812 let arg_count = args.len().max(arg_index + 1);
813 return Some(TsCallContext {
814 name,
815 arg_index,
816 arg_count,
817 call_start_byte: node.start_byte(),
818 is_index_access: false,
819 });
820 }
821 "array_access" => {
822 let base_node = node.child_by_field_name("base")?;
824 let name_node = if base_node.kind() == "member_expression" {
827 base_node
828 .child_by_field_name("property")
829 .unwrap_or(base_node)
830 } else {
831 base_node
832 };
833 let name = &source[name_node.byte_range()];
834 return Some(TsCallContext {
835 name,
836 arg_index: 0,
837 arg_count: 1,
838 call_start_byte: node.start_byte(),
839 is_index_access: true,
840 });
841 }
842 "source_file" => break,
843 _ => {
844 node = node.parent()?;
845 }
846 }
847 }
848
849 if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
851 return Some(ctx);
852 }
853
854 find_index_by_text_scan(source, byte_pos)
856}
857
858fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
864 let before = &source[..byte_pos.min(source.len())];
865
866 let mut depth: i32 = 0;
868 let mut paren_pos = None;
869 for (i, ch) in before.char_indices().rev() {
870 match ch {
871 ')' => depth += 1,
872 '(' => {
873 if depth == 0 {
874 paren_pos = Some(i);
875 break;
876 }
877 depth -= 1;
878 }
879 _ => {}
880 }
881 }
882 let paren_pos = paren_pos?;
883
884 let mut scan_end = paren_pos;
888 let before_paren = source[..scan_end].trim_end();
889 if before_paren.ends_with('}') {
890 let mut brace_depth: i32 = 0;
892 for (i, ch) in before_paren.char_indices().rev() {
893 match ch {
894 '}' => brace_depth += 1,
895 '{' => {
896 brace_depth -= 1;
897 if brace_depth == 0 {
898 scan_end = i;
899 break;
900 }
901 }
902 _ => {}
903 }
904 }
905 }
906 let before_name = &source[..scan_end];
907 let name_end = before_name.trim_end().len();
908 let name_start = before_name[..name_end]
909 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
910 .map(|i| i + 1)
911 .unwrap_or(0);
912 let raw_name = &source[name_start..name_end];
914 let name = match raw_name.rfind('.') {
915 Some(dot) => &raw_name[dot + 1..],
916 None => raw_name,
917 };
918
919 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
920 return None;
921 }
922
923 let arg_index = count_commas_before(source, paren_pos, byte_pos);
925
926 Some(TsCallContext {
927 name,
928 arg_index,
929 arg_count: arg_index + 1,
930 call_start_byte: name_start,
931 is_index_access: false,
932 })
933}
934
935fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
940 let before = &source[..byte_pos.min(source.len())];
941
942 let mut depth: i32 = 0;
944 let mut bracket_pos = None;
945 for (i, c) in before.char_indices().rev() {
946 match c {
947 ']' => depth += 1,
948 '[' => {
949 if depth == 0 {
950 bracket_pos = Some(i);
951 break;
952 }
953 depth -= 1;
954 }
955 _ => {}
956 }
957 }
958 let bracket_pos = bracket_pos?;
959
960 let before_bracket = &source[..bracket_pos];
962 let name_end = before_bracket.trim_end().len();
963 let name_start = before_bracket[..name_end]
964 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
965 .map(|i| i + 1)
966 .unwrap_or(0);
967 let name = &source[name_start..name_end];
968
969 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
970 return None;
971 }
972
973 Some(TsCallContext {
974 name,
975 arg_index: 0,
976 arg_count: 1,
977 call_start_byte: name_start,
978 is_index_access: true,
979 })
980}
981
982fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
984 let end = byte_pos.min(source.len());
985 let text = &source[start..end];
986
987 let mut count = 0;
988 let mut depth = 0;
989 let mut found_open = false;
990 for ch in text.chars() {
991 match ch {
992 '(' if !found_open => {
993 found_open = true;
994 depth = 1;
995 }
996 '(' => depth += 1,
997 ')' => depth -= 1,
998 ',' if found_open && depth == 1 => count += 1,
999 _ => {}
1000 }
1001 }
1002 count
1003}
1004
1005fn collect_ts_gas_hints(
1010 node: Node,
1011 source: &str,
1012 range: &Range,
1013 gas_index: &gas::GasIndex,
1014 abs_path: &str,
1015 hints: &mut Vec<InlayHint>,
1016) {
1017 let node_start = node.start_position();
1018 let node_end = node.end_position();
1019 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
1020 return;
1021 }
1022
1023 match node.kind() {
1024 "function_definition" => {
1025 if let Some(hint) = ts_gas_hint_for_function(node, source, range, gas_index, abs_path) {
1026 hints.push(hint);
1027 }
1028 }
1029 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1030 if let Some(hint) = ts_gas_hint_for_contract(node, source, range, gas_index, abs_path) {
1031 hints.push(hint);
1032 }
1033 }
1034 _ => {}
1035 }
1036
1037 let mut cursor = node.walk();
1038 for child in node.children(&mut cursor) {
1039 collect_ts_gas_hints(child, source, range, gas_index, abs_path, hints);
1040 }
1041}
1042
1043fn ts_node_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1045 let mut cursor = node.walk();
1046 node.children(&mut cursor)
1047 .find(|c| c.kind() == "identifier" && c.is_named())
1048 .map(|c| &source[c.byte_range()])
1049}
1050
1051fn ts_body_open_brace(node: Node, body_kind: &str) -> Option<Position> {
1053 let mut cursor = node.walk();
1054 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
1055 let start = body.start_position();
1056 Some(Position::new(start.row as u32, start.column as u32))
1057}
1058
1059fn ts_body_close_brace(node: Node, body_kind: &str) -> Option<Position> {
1061 let mut cursor = node.walk();
1062 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
1063 let end = body.end_position();
1064 Some(Position::new(
1066 end.row as u32,
1067 end.column.saturating_sub(1) as u32,
1068 ))
1069}
1070
1071fn ts_enclosing_contract_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1073 let mut parent = node.parent();
1074 while let Some(p) = parent {
1075 if p.kind() == "contract_declaration"
1076 || p.kind() == "library_declaration"
1077 || p.kind() == "interface_declaration"
1078 {
1079 return ts_node_name(p, source);
1080 }
1081 parent = p.parent();
1082 }
1083 None
1084}
1085
1086fn find_gas_key<'a>(
1088 gas_index: &'a gas::GasIndex,
1089 abs_path: &str,
1090 contract_name: &str,
1091) -> Option<&'a str> {
1092 let exact = format!("{abs_path}:{contract_name}");
1093 if gas_index.contains_key(&exact) {
1094 return Some(gas_index.get_key_value(&exact)?.0.as_str());
1095 }
1096 let file_name = std::path::Path::new(abs_path).file_name()?.to_str()?;
1097 let suffix = format!("{file_name}:{contract_name}");
1098 gas_index
1099 .keys()
1100 .find(|k| k.ends_with(&suffix))
1101 .map(|k| k.as_str())
1102}
1103
1104fn has_gas_sentinel(node: Node, source: &str) -> bool {
1109 let mut prev = node.prev_named_sibling();
1110 while let Some(sibling) = prev {
1111 if sibling.kind() == "comment" {
1112 let text = &source[sibling.byte_range()];
1113 if text.contains(gas::GAS_SENTINEL) {
1114 return true;
1115 }
1116 } else {
1117 break;
1118 }
1119 prev = sibling.prev_named_sibling();
1120 }
1121 false
1122}
1123
1124fn ts_gas_hint_for_function(
1126 node: Node,
1127 source: &str,
1128 range: &Range,
1129 gas_index: &gas::GasIndex,
1130 abs_path: &str,
1131) -> Option<InlayHint> {
1132 if !has_gas_sentinel(node, source) {
1134 return None;
1135 }
1136 let fn_name = ts_node_name(node, source)?;
1137 let contract_name = ts_enclosing_contract_name(node, source)?;
1138 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
1139 let contract_gas = gas_index.get(gas_key)?;
1140
1141 let prefix = format!("{fn_name}(");
1142 let cost = contract_gas
1143 .external_by_sig
1144 .iter()
1145 .find(|(sig, _)| sig.as_str().starts_with(&prefix))
1146 .map(|(_, c)| c.as_str())
1147 .or_else(|| {
1148 contract_gas
1149 .internal
1150 .iter()
1151 .find(|(sig, _)| sig.starts_with(&prefix))
1152 .map(|(_, c)| c.as_str())
1153 })?;
1154
1155 let (brace_pos, offset) = match FN_GAS_HINT_POSITION {
1157 FnGasHintPosition::Opening => (ts_body_open_brace(node, "function_body")?, 1),
1158 FnGasHintPosition::Closing => (ts_body_close_brace(node, "function_body")?, 1),
1159 };
1160 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1161 return None;
1162 }
1163
1164 Some(InlayHint {
1165 position: Position::new(brace_pos.line, brace_pos.character + offset),
1166 kind: Some(InlayHintKind::TYPE),
1167 label: InlayHintLabel::String(format!("🔥 gas: {}", gas::format_gas(cost))),
1168 text_edits: None,
1169 tooltip: Some(InlayHintTooltip::String("Estimated gas cost".to_string())),
1170 padding_left: Some(true),
1171 padding_right: None,
1172 data: None,
1173 })
1174}
1175
1176fn ts_gas_hint_for_contract(
1179 node: Node,
1180 source: &str,
1181 range: &Range,
1182 gas_index: &gas::GasIndex,
1183 abs_path: &str,
1184) -> Option<InlayHint> {
1185 if !has_gas_sentinel(node, source) {
1187 return None;
1188 }
1189 let contract_name = ts_node_name(node, source)?;
1190 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
1191 let contract_gas = gas_index.get(gas_key)?;
1192
1193 let display_cost = match contract_gas.creation.get("totalCost").map(|s| s.as_str()) {
1195 Some("infinite") | None => contract_gas
1196 .creation
1197 .get("codeDepositCost")
1198 .map(|s| s.as_str())?,
1199 Some(total) => total,
1200 };
1201
1202 let brace_pos = ts_body_open_brace(node, "contract_body")?;
1203 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1204 return None;
1205 }
1206
1207 Some(InlayHint {
1208 position: Position::new(brace_pos.line, brace_pos.character + 1),
1209 kind: Some(InlayHintKind::TYPE),
1210 label: InlayHintLabel::String(format!("🔥 deploy: {} ", gas::format_gas(display_cost))),
1211 text_edits: None,
1212 tooltip: Some(InlayHintTooltip::String(format!(
1213 "Deploy cost — code deposit: {}, execution: {}",
1214 gas::format_gas(
1215 contract_gas
1216 .creation
1217 .get("codeDepositCost")
1218 .map(|s| s.as_str())
1219 .unwrap_or("?")
1220 ),
1221 gas::format_gas(
1222 contract_gas
1223 .creation
1224 .get("executionCost")
1225 .map(|s| s.as_str())
1226 .unwrap_or("?")
1227 )
1228 ))),
1229 padding_left: Some(true),
1230 padding_right: None,
1231 data: None,
1232 })
1233}
1234
1235#[cfg(test)]
1238mod tests {
1239 use super::*;
1240
1241 #[test]
1242 fn test_gas_sentinel_present() {
1243 let source = r#"
1244contract Foo {
1245 /// @custom:lsp-enable gas-estimates
1246 function bar() public {}
1247}
1248"#;
1249 let tree = ts_parse(source).unwrap();
1250 let root = tree.root_node();
1251 let contract = root.child(0).unwrap();
1253 let body = contract.child_by_field_name("body").unwrap();
1254 let mut cursor = body.walk();
1255 let fn_node = body
1256 .children(&mut cursor)
1257 .find(|c| c.kind() == "function_definition")
1258 .unwrap();
1259 assert!(has_gas_sentinel(fn_node, source));
1260 }
1261
1262 #[test]
1263 fn test_gas_sentinel_absent() {
1264 let source = r#"
1265contract Foo {
1266 function bar() public {}
1267}
1268"#;
1269 let tree = ts_parse(source).unwrap();
1270 let root = tree.root_node();
1271 let contract = root.child(0).unwrap();
1272 let body = contract.child_by_field_name("body").unwrap();
1273 let mut cursor = body.walk();
1274 let fn_node = body
1275 .children(&mut cursor)
1276 .find(|c| c.kind() == "function_definition")
1277 .unwrap();
1278 assert!(!has_gas_sentinel(fn_node, source));
1279 }
1280
1281 #[test]
1282 fn test_gas_sentinel_with_other_natspec() {
1283 let source = r#"
1284contract Foo {
1285 /// @notice Does something
1286 /// @custom:lsp-enable gas-estimates
1287 function bar() public {}
1288}
1289"#;
1290 let tree = ts_parse(source).unwrap();
1291 let root = tree.root_node();
1292 let contract = root.child(0).unwrap();
1293 let body = contract.child_by_field_name("body").unwrap();
1294 let mut cursor = body.walk();
1295 let fn_node = body
1296 .children(&mut cursor)
1297 .find(|c| c.kind() == "function_definition")
1298 .unwrap();
1299 assert!(has_gas_sentinel(fn_node, source));
1300 }
1301
1302 #[test]
1303 fn test_ts_call_function_name() {
1304 let source = r#"
1305contract Foo {
1306 function bar(uint x) public {}
1307 function test() public {
1308 bar(42);
1309 }
1310}
1311"#;
1312 let tree = ts_parse(source).unwrap();
1313 let mut found = Vec::new();
1314 find_calls(tree.root_node(), source, &mut found);
1315 assert_eq!(found.len(), 1);
1316 assert_eq!(found[0], "bar");
1317 }
1318
1319 #[test]
1320 fn test_ts_member_call_name() {
1321 let source = r#"
1322contract Foo {
1323 function test() public {
1324 PRICE.addTax(TAX, TAX_BASE);
1325 }
1326}
1327"#;
1328 let tree = ts_parse(source).unwrap();
1329 let mut found = Vec::new();
1330 find_calls(tree.root_node(), source, &mut found);
1331 assert_eq!(found.len(), 1);
1332 assert_eq!(found[0], "addTax");
1333 }
1334
1335 #[test]
1336 fn test_ts_call_with_value_modifier() {
1337 let source = r#"
1338contract Foo {
1339 function test() public {
1340 router.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
1341 }
1342}
1343"#;
1344 let tree = ts_parse(source).unwrap();
1345 let mut found = Vec::new();
1346 find_calls(tree.root_node(), source, &mut found);
1347 assert_eq!(found.len(), 1, "should find one call");
1348 assert_eq!(
1349 found[0], "swap",
1350 "should extract 'swap' through struct_expression"
1351 );
1352 }
1353
1354 #[test]
1355 fn test_ts_call_simple_with_value_modifier() {
1356 let source = r#"
1357contract Foo {
1358 function test() public {
1359 foo{value: 1 ether}(42);
1360 }
1361}
1362"#;
1363 let tree = ts_parse(source).unwrap();
1364 let mut found = Vec::new();
1365 find_calls(tree.root_node(), source, &mut found);
1366 assert_eq!(found.len(), 1, "should find one call");
1367 assert_eq!(
1368 found[0], "foo",
1369 "should extract 'foo' through struct_expression"
1370 );
1371 }
1372
1373 #[test]
1374 fn test_ts_call_with_gas_modifier() {
1375 let source = r#"
1376contract Foo {
1377 function test() public {
1378 addr.call{gas: 5000, value: 1 ether}("");
1379 }
1380}
1381"#;
1382 let tree = ts_parse(source).unwrap();
1383 let mut found = Vec::new();
1384 find_calls(tree.root_node(), source, &mut found);
1385 assert_eq!(found.len(), 1, "should find one call");
1386 assert_eq!(
1387 found[0], "call",
1388 "should extract 'call' through struct_expression"
1389 );
1390 }
1391
1392 #[test]
1393 fn test_find_call_by_text_scan_with_value_modifier() {
1394 let source = "router.swap{value: 100}(nativeKey, SWAP_PARAMS)";
1396 let byte_pos = source.find("SWAP_PARAMS").unwrap();
1398 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1399 assert_eq!(ctx.name, "swap");
1400 assert_eq!(ctx.arg_index, 1);
1401 }
1402
1403 #[test]
1404 fn test_find_call_by_text_scan_simple_value_modifier() {
1405 let source = "foo{value: 1 ether}(42)";
1406 let byte_pos = source.find("42").unwrap();
1407 let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
1408 assert_eq!(ctx.name, "foo");
1409 assert_eq!(ctx.arg_index, 0);
1410 }
1411
1412 #[test]
1413 fn test_ts_emit_event_name() {
1414 let source = r#"
1415contract Foo {
1416 event Purchase(address buyer, uint256 price);
1417 function test() public {
1418 emit Purchase(msg.sender, 100);
1419 }
1420}
1421"#;
1422 let tree = ts_parse(source).unwrap();
1423 let mut found = Vec::new();
1424 find_emits(tree.root_node(), source, &mut found);
1425 assert_eq!(found.len(), 1);
1426 assert_eq!(found[0], "Purchase");
1427 }
1428
1429 #[test]
1430 fn test_ts_new_expression_name() {
1431 let source = r#"
1432contract Token {
1433 constructor(string memory _name, uint256 _supply) {}
1434}
1435contract Factory {
1436 function create() public {
1437 Token t = new Token("MyToken", 1000);
1438 }
1439}
1440"#;
1441 let tree = ts_parse(source).unwrap();
1442 let mut found = Vec::new();
1443 find_new_exprs(tree.root_node(), source, &mut found);
1444 assert_eq!(found.len(), 1);
1445 assert_eq!(found[0], "Token");
1446 }
1447
1448 #[test]
1449 fn test_ts_new_expression_arguments() {
1450 let source = r#"
1454contract Router {
1455 constructor(address _manager, address _hook) {}
1456}
1457contract Factory {
1458 function create() public {
1459 Router r = new Router(address(this), address(0));
1460 }
1461}
1462"#;
1463 let tree = ts_parse(source).unwrap();
1464 let mut arg_counts = Vec::new();
1465 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1466 assert_eq!(arg_counts, vec![2]);
1468 }
1469
1470 #[test]
1471 fn test_ts_find_call_at_byte_new_expression() {
1472 let source = r#"
1473contract Token {
1474 constructor(string memory _name, uint256 _supply) {}
1475}
1476contract Factory {
1477 function create() public {
1478 Token t = new Token("MyToken", 1000);
1479 }
1480}
1481"#;
1482 let tree = ts_parse(source).unwrap();
1483 let pos = source.find("1000").unwrap();
1484 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1485 assert_eq!(ctx.name, "Token");
1486 assert_eq!(ctx.arg_index, 1);
1487 assert_eq!(ctx.arg_count, 2);
1488 }
1489
1490 #[test]
1491 fn test_ts_find_call_at_byte_new_expression_first_arg() {
1492 let source = r#"
1493contract Token {
1494 constructor(string memory _name, uint256 _supply) {}
1495}
1496contract Factory {
1497 function create() public {
1498 Token t = new Token("MyToken", 1000);
1499 }
1500}
1501"#;
1502 let tree = ts_parse(source).unwrap();
1503 let pos = source.find("\"MyToken\"").unwrap();
1504 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
1505 assert_eq!(ctx.name, "Token");
1506 assert_eq!(ctx.arg_index, 0);
1507 assert_eq!(ctx.arg_count, 2);
1508 }
1509
1510 #[test]
1511 fn test_extract_new_expression_call_info() {
1512 let new_expr: Value = serde_json::json!({
1514 "nodeType": "NewExpression",
1515 "typeName": {
1516 "nodeType": "UserDefinedTypeName",
1517 "referencedDeclaration": 22,
1518 "pathNode": {
1519 "name": "Token"
1520 }
1521 }
1522 });
1523
1524 let mut constructor_index = ConstructorIndex::new();
1525 constructor_index.insert(
1526 22,
1527 ConstructorInfo {
1528 constructor_id: 21,
1529 contract_name: "Token".to_string(),
1530 param_names: vec!["_name".to_string(), "_supply".to_string()],
1531 },
1532 );
1533
1534 let info = extract_new_expression_call_info(&new_expr, 2, &constructor_index).unwrap();
1535 assert_eq!(info.name, "Token");
1536 assert_eq!(info.params.names, vec!["_name", "_supply"]);
1537 assert_eq!(info.params.skip, 0);
1538 assert_eq!(info.decl_id, 21);
1539 }
1540
1541 #[test]
1542 fn test_ts_call_arguments_count() {
1543 let source = r#"
1544contract Foo {
1545 function bar(uint x, uint y) public {}
1546 function test() public {
1547 bar(1, 2);
1548 }
1549}
1550"#;
1551 let tree = ts_parse(source).unwrap();
1552 let mut arg_counts = Vec::new();
1553 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1554 assert_eq!(arg_counts, vec![2]);
1555 }
1556
1557 #[test]
1558 fn test_ts_argument_positions_follow_live_buffer() {
1559 let source = r#"
1561contract Foo {
1562 function bar(uint x, uint y) public {}
1563 function test() public {
1564 bar(
1565 1,
1566 2
1567 );
1568 }
1569}
1570"#;
1571 let tree = ts_parse(source).unwrap();
1572 let mut positions = Vec::new();
1573 find_arg_positions(tree.root_node(), &mut positions);
1574 assert_eq!(positions.len(), 2);
1576 assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
1579
1580 fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1583 if node.kind() == "call_expression"
1584 && let Some(name) = ts_call_function_name(node, source)
1585 {
1586 out.push(name);
1587 }
1588 let mut cursor = node.walk();
1589 for child in node.children(&mut cursor) {
1590 find_calls(child, source, out);
1591 }
1592 }
1593
1594 fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1595 if node.kind() == "emit_statement"
1596 && let Some(name) = ts_emit_event_name(node, source)
1597 {
1598 out.push(name);
1599 }
1600 let mut cursor = node.walk();
1601 for child in node.children(&mut cursor) {
1602 find_emits(child, source, out);
1603 }
1604 }
1605
1606 fn find_new_exprs<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1607 if node.kind() == "new_expression"
1608 && let Some(name) = ts_new_expression_name(node, source)
1609 {
1610 out.push(name);
1611 }
1612 let mut cursor = node.walk();
1613 for child in node.children(&mut cursor) {
1614 find_new_exprs(child, source, out);
1615 }
1616 }
1617
1618 fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
1619 if node.kind() == "call_expression" {
1620 out.push(ts_call_arguments(node).len());
1621 }
1622 let mut cursor = node.walk();
1623 for child in node.children(&mut cursor) {
1624 find_call_arg_counts(child, out);
1625 }
1626 }
1627
1628 fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
1629 if node.kind() == "call_expression" {
1630 for arg in ts_call_arguments(node) {
1631 let p = arg.start_position();
1632 out.push((p.row, p.column));
1633 }
1634 }
1635 let mut cursor = node.walk();
1636 for child in node.children(&mut cursor) {
1637 find_arg_positions(child, out);
1638 }
1639 }
1640
1641 #[test]
1642 fn test_ts_find_call_at_byte_first_arg() {
1643 let source = r#"
1644contract Foo {
1645 function bar(uint x, uint y) public {}
1646 function test() public {
1647 bar(42, 99);
1648 }
1649}
1650"#;
1651 let tree = ts_parse(source).unwrap();
1652 let pos_42 = source.find("42").unwrap();
1654 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1655 assert_eq!(ctx.name, "bar");
1656 assert_eq!(ctx.arg_index, 0);
1657 assert_eq!(ctx.arg_count, 2);
1658 }
1659
1660 #[test]
1661 fn test_ts_find_call_at_byte_second_arg() {
1662 let source = r#"
1663contract Foo {
1664 function bar(uint x, uint y) public {}
1665 function test() public {
1666 bar(42, 99);
1667 }
1668}
1669"#;
1670 let tree = ts_parse(source).unwrap();
1671 let pos_99 = source.find("99").unwrap();
1672 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1673 assert_eq!(ctx.name, "bar");
1674 assert_eq!(ctx.arg_index, 1);
1675 assert_eq!(ctx.arg_count, 2);
1676 }
1677
1678 #[test]
1679 fn test_ts_find_call_at_byte_outside_call_returns_none() {
1680 let source = r#"
1681contract Foo {
1682 function bar(uint x) public {}
1683 function test() public {
1684 uint z = 10;
1685 bar(42);
1686 }
1687}
1688"#;
1689 let tree = ts_parse(source).unwrap();
1690 let pos_10 = source.find("10").unwrap();
1692 assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1693 }
1694
1695 #[test]
1696 fn test_ts_find_call_at_byte_member_call() {
1697 let source = r#"
1698contract Foo {
1699 function test() public {
1700 PRICE.addTax(TAX, TAX_BASE);
1701 }
1702}
1703"#;
1704 let tree = ts_parse(source).unwrap();
1705 let pos_tax = source.find("TAX,").unwrap();
1706 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1707 assert_eq!(ctx.name, "addTax");
1708 assert_eq!(ctx.arg_index, 0);
1709 assert_eq!(ctx.arg_count, 2);
1710 }
1711
1712 #[test]
1713 fn test_ts_find_call_at_byte_emit_statement() {
1714 let source = r#"
1715contract Foo {
1716 event Purchase(address buyer, uint256 price);
1717 function test() public {
1718 emit Purchase(msg.sender, 100);
1719 }
1720}
1721"#;
1722 let tree = ts_parse(source).unwrap();
1723 let pos_100 = source.find("100").unwrap();
1724 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1725 assert_eq!(ctx.name, "Purchase");
1726 assert_eq!(ctx.arg_index, 1);
1727 assert_eq!(ctx.arg_count, 2);
1728 }
1729
1730 #[test]
1731 fn test_ts_find_call_at_byte_multiline() {
1732 let source = r#"
1733contract Foo {
1734 function bar(uint x, uint y, uint z) public {}
1735 function test() public {
1736 bar(
1737 1,
1738 2,
1739 3
1740 );
1741 }
1742}
1743"#;
1744 let tree = ts_parse(source).unwrap();
1745 let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1749 assert_eq!(ctx.name, "bar");
1750 assert_eq!(ctx.arg_index, 1);
1751 assert_eq!(ctx.arg_count, 3);
1752 }
1753
1754 #[test]
1755 fn test_resolve_callsite_param_basic() {
1756 let mut lookup = HintLookup {
1758 by_offset: HashMap::new(),
1759 by_name: HashMap::new(),
1760 };
1761 lookup.by_name.insert(
1762 ("transfer".to_string(), 2),
1763 CallSite {
1764 info: ParamInfo {
1765 names: vec!["to".to_string(), "amount".to_string()],
1766 skip: 0,
1767 },
1768 name: "transfer".to_string(),
1769 decl_id: 42,
1770 },
1771 );
1772
1773 let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1775 assert_eq!(result.param_name, "to");
1776 assert_eq!(result.decl_id, 42);
1777
1778 let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1780 assert_eq!(result.param_name, "amount");
1781 assert_eq!(result.decl_id, 42);
1782 }
1783
1784 #[test]
1785 fn test_resolve_callsite_param_with_skip() {
1786 let mut lookup = HintLookup {
1788 by_offset: HashMap::new(),
1789 by_name: HashMap::new(),
1790 };
1791 lookup.by_name.insert(
1792 ("addTax".to_string(), 2),
1793 CallSite {
1794 info: ParamInfo {
1795 names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1796 skip: 1,
1797 },
1798 name: "addTax".to_string(),
1799 decl_id: 99,
1800 },
1801 );
1802
1803 let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1805 assert_eq!(result.param_name, "tax");
1806
1807 let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1809 assert_eq!(result.param_name, "base");
1810 }
1811
1812 #[test]
1813 fn test_resolve_callsite_param_out_of_bounds() {
1814 let mut lookup = HintLookup {
1815 by_offset: HashMap::new(),
1816 by_name: HashMap::new(),
1817 };
1818 lookup.by_name.insert(
1819 ("foo".to_string(), 1),
1820 CallSite {
1821 info: ParamInfo {
1822 names: vec!["x".to_string()],
1823 skip: 0,
1824 },
1825 name: "foo".to_string(),
1826 decl_id: 1,
1827 },
1828 );
1829
1830 assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1832 }
1833
1834 #[test]
1835 fn test_resolve_callsite_param_unknown_function() {
1836 let lookup = HintLookup {
1837 by_offset: HashMap::new(),
1838 by_name: HashMap::new(),
1839 };
1840 assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1841 }
1842
1843 #[test]
1844 fn test_ts_find_call_at_byte_emit_member_access() {
1845 let source = r#"
1849contract Foo {
1850 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1851 function test() public {
1852 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1853 }
1854}
1855"#;
1856 let tree = ts_parse(source).unwrap();
1857 let params_tick = source.find("params.tickLower,").unwrap();
1859 let tick_lower_pos = params_tick + "params.".len(); let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1862 assert_eq!(ctx.name, "ModifyLiquidity");
1863 assert_eq!(
1864 ctx.arg_index, 2,
1865 "params.tickLower is the 3rd argument (index 2)"
1866 );
1867 assert_eq!(ctx.arg_count, 4);
1868 }
1869
1870 #[test]
1871 fn test_ts_find_call_at_byte_member_access_on_property() {
1872 let source = r#"
1874contract Foo {
1875 event Transfer(address from, address to);
1876 function test() public {
1877 emit Transfer(msg.sender, addr);
1878 }
1879}
1880"#;
1881 let tree = ts_parse(source).unwrap();
1882 let sender_pos = source.find("sender").unwrap();
1883 let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1884 assert_eq!(ctx.name, "Transfer");
1885 assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1886 }
1887
1888 #[test]
1889 fn test_ts_find_call_at_byte_emit_all_args() {
1890 let source = r#"
1892contract Foo {
1893 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1894 function test() public {
1895 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1896 }
1897}
1898"#;
1899 let tree = ts_parse(source).unwrap();
1900
1901 let pos_id = source.find("(id,").unwrap() + 1;
1903 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1904 assert_eq!(ctx.name, "ModifyLiquidity");
1905 assert_eq!(ctx.arg_index, 0);
1906
1907 let pos_msg = source.find("msg.sender").unwrap();
1909 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1910 assert_eq!(ctx.arg_index, 1);
1911
1912 let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1914 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1915 assert_eq!(ctx.arg_index, 2);
1916
1917 let pos_tu = source.find("params.tickUpper").unwrap();
1919 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1920 assert_eq!(ctx.arg_index, 3);
1921 }
1922
1923 #[test]
1924 fn test_ts_find_call_at_byte_nested_call_arg() {
1925 let source = r#"
1928contract Foo {
1929 function inner(uint x) public returns (uint) {}
1930 function outer(uint a, uint b) public {}
1931 function test() public {
1932 outer(inner(42), 99);
1933 }
1934}
1935"#;
1936 let tree = ts_parse(source).unwrap();
1937
1938 let pos_42 = source.find("42").unwrap();
1940 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1941 assert_eq!(ctx.name, "inner");
1942 assert_eq!(ctx.arg_index, 0);
1943
1944 let pos_99 = source.find("99").unwrap();
1946 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1947 assert_eq!(ctx.name, "outer");
1948 assert_eq!(ctx.arg_index, 1);
1949 }
1950
1951 #[test]
1952 fn test_ts_find_call_for_signature_incomplete_call() {
1953 let source = r#"
1955contract Foo {
1956 function bar(uint x, uint y) public {}
1957 function test() public {
1958 bar(
1959 }
1960}
1961"#;
1962 let tree = ts_parse(source).unwrap();
1963 let pos = source.find("bar(").unwrap() + 4;
1964 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1965 assert_eq!(ctx.name, "bar");
1966 assert_eq!(ctx.arg_index, 0);
1967 }
1968
1969 #[test]
1970 fn test_ts_find_call_for_signature_after_comma() {
1971 let source = r#"
1973contract Foo {
1974 function bar(uint x, uint y) public {}
1975 function test() public {
1976 bar(42,
1977 }
1978}
1979"#;
1980 let tree = ts_parse(source).unwrap();
1981 let pos = source.find("42,").unwrap() + 3;
1982 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1983 assert_eq!(ctx.name, "bar");
1984 assert_eq!(ctx.arg_index, 1);
1985 }
1986
1987 #[test]
1988 fn test_ts_find_call_for_signature_complete_call() {
1989 let source = r#"
1991contract Foo {
1992 function bar(uint x, uint y) public {}
1993 function test() public {
1994 bar(42, 99);
1995 }
1996}
1997"#;
1998 let tree = ts_parse(source).unwrap();
1999 let pos = source.find("42").unwrap();
2000 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
2001 assert_eq!(ctx.name, "bar");
2002 assert_eq!(ctx.arg_index, 0);
2003 }
2004
2005 #[test]
2006 fn test_ts_find_call_for_signature_member_call() {
2007 let source = r#"
2009contract Foo {
2010 function test() public {
2011 PRICE.addTax(
2012 }
2013}
2014"#;
2015 let tree = ts_parse(source).unwrap();
2016 let pos = source.find("addTax(").unwrap() + 7;
2017 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
2018 assert_eq!(ctx.name, "addTax");
2019 assert_eq!(ctx.arg_index, 0);
2020 }
2021
2022 #[test]
2023 fn test_ts_find_call_for_signature_array_access() {
2024 let source = r#"
2026contract Foo {
2027 mapping(bytes32 => uint256) public orders;
2028 function test() public {
2029 orders[orderId];
2030 }
2031}
2032"#;
2033 let tree = ts_parse(source).unwrap();
2034 let pos = source.find("[orderId]").unwrap() + 1;
2036 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
2037 assert_eq!(ctx.name, "orders");
2038 assert_eq!(ctx.arg_index, 0);
2039 assert!(ctx.is_index_access);
2040 }
2041
2042 #[test]
2043 fn test_ts_find_call_for_signature_array_access_empty() {
2044 let source = r#"
2046contract Foo {
2047 mapping(bytes32 => uint256) public orders;
2048 function test() public {
2049 orders[
2050 }
2051}
2052"#;
2053 let tree = ts_parse(source).unwrap();
2054 let pos = source.find("orders[").unwrap() + 7;
2055 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
2056 assert_eq!(ctx.name, "orders");
2057 assert!(ctx.is_index_access);
2058 }
2059}