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> {
537 let func_expr = node.child_by_field_name("function")?;
538 let inner = first_named_child(func_expr)?;
540 match inner.kind() {
541 "identifier" => Some(&source[inner.byte_range()]),
542 "member_expression" => {
543 let prop = inner.child_by_field_name("property")?;
544 Some(&source[prop.byte_range()])
545 }
546 _ => None,
547 }
548}
549
550fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
552 let name_expr = node.child_by_field_name("name")?;
553 let inner = first_named_child(name_expr)?;
554 match inner.kind() {
555 "identifier" => Some(&source[inner.byte_range()]),
556 "member_expression" => {
557 let prop = inner.child_by_field_name("property")?;
558 Some(&source[prop.byte_range()])
559 }
560 _ => None,
561 }
562}
563
564fn ts_call_arguments(node: Node) -> Vec<Node> {
567 let mut args = Vec::new();
568 let mut cursor = node.walk();
569 for child in node.children(&mut cursor) {
570 if child.kind() == "call_argument" {
571 args.push(child);
572 }
573 }
574 args
575}
576
577fn first_named_child(node: Node) -> Option<Node> {
579 let mut cursor = node.walk();
580 node.children(&mut cursor).find(|c| c.is_named())
581}
582
583pub struct TsCallContext<'a> {
585 pub name: &'a str,
587 pub arg_index: usize,
589 pub arg_count: usize,
591 pub call_start_byte: usize,
593 pub is_index_access: bool,
596}
597
598pub fn ts_find_call_at_byte<'a>(
603 root: tree_sitter::Node<'a>,
604 source: &'a str,
605 byte_pos: usize,
606) -> Option<TsCallContext<'a>> {
607 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
609
610 loop {
612 if node.kind() == "call_argument" {
613 break;
614 }
615 node = node.parent()?;
616 }
617
618 let call_node = node.parent()?;
620 let args = ts_call_arguments(call_node);
621 let arg_index = args.iter().position(|a| a.id() == node.id())?;
622
623 match call_node.kind() {
624 "call_expression" => {
625 let name = ts_call_function_name(call_node, source)?;
626 Some(TsCallContext {
627 name,
628 arg_index,
629 arg_count: args.len(),
630 call_start_byte: call_node.start_byte(),
631 is_index_access: false,
632 })
633 }
634 "emit_statement" => {
635 let name = ts_emit_event_name(call_node, source)?;
636 Some(TsCallContext {
637 name,
638 arg_index,
639 arg_count: args.len(),
640 call_start_byte: call_node.start_byte(),
641 is_index_access: false,
642 })
643 }
644 _ => None,
645 }
646}
647
648pub fn ts_find_call_for_signature<'a>(
658 root: tree_sitter::Node<'a>,
659 source: &'a str,
660 byte_pos: usize,
661) -> Option<TsCallContext<'a>> {
662 if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
664 return Some(ctx);
665 }
666
667 let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
669 loop {
670 match node.kind() {
671 "call_expression" => {
672 let name = ts_call_function_name(node, source)?;
673 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
674 let args = ts_call_arguments(node);
675 let arg_count = args.len().max(arg_index + 1);
676 return Some(TsCallContext {
677 name,
678 arg_index,
679 arg_count,
680 call_start_byte: node.start_byte(),
681 is_index_access: false,
682 });
683 }
684 "emit_statement" => {
685 let name = ts_emit_event_name(node, source)?;
686 let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
687 let args = ts_call_arguments(node);
688 let arg_count = args.len().max(arg_index + 1);
689 return Some(TsCallContext {
690 name,
691 arg_index,
692 arg_count,
693 call_start_byte: node.start_byte(),
694 is_index_access: false,
695 });
696 }
697 "array_access" => {
698 let base_node = node.child_by_field_name("base")?;
700 let name_node = if base_node.kind() == "member_expression" {
703 base_node
704 .child_by_field_name("property")
705 .unwrap_or(base_node)
706 } else {
707 base_node
708 };
709 let name = &source[name_node.byte_range()];
710 return Some(TsCallContext {
711 name,
712 arg_index: 0,
713 arg_count: 1,
714 call_start_byte: node.start_byte(),
715 is_index_access: true,
716 });
717 }
718 "source_file" => break,
719 _ => {
720 node = node.parent()?;
721 }
722 }
723 }
724
725 if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
727 return Some(ctx);
728 }
729
730 find_index_by_text_scan(source, byte_pos)
732}
733
734fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
740 let before = &source[..byte_pos.min(source.len())];
741
742 let mut depth: i32 = 0;
744 let mut paren_pos = None;
745 for (i, ch) in before.char_indices().rev() {
746 match ch {
747 ')' => depth += 1,
748 '(' => {
749 if depth == 0 {
750 paren_pos = Some(i);
751 break;
752 }
753 depth -= 1;
754 }
755 _ => {}
756 }
757 }
758 let paren_pos = paren_pos?;
759
760 let before_paren = &source[..paren_pos];
763 let name_end = before_paren.trim_end().len();
764 let name_start = before_paren[..name_end]
765 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
766 .map(|i| i + 1)
767 .unwrap_or(0);
768 let name = &source[name_start..name_end];
769
770 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
771 return None;
772 }
773
774 let arg_index = count_commas_before(source, paren_pos, byte_pos);
776
777 Some(TsCallContext {
778 name,
779 arg_index,
780 arg_count: arg_index + 1,
781 call_start_byte: name_start,
782 is_index_access: false,
783 })
784}
785
786fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
791 let before = &source[..byte_pos.min(source.len())];
792
793 let mut depth: i32 = 0;
795 let mut bracket_pos = None;
796 for (i, c) in before.char_indices().rev() {
797 match c {
798 ']' => depth += 1,
799 '[' => {
800 if depth == 0 {
801 bracket_pos = Some(i);
802 break;
803 }
804 depth -= 1;
805 }
806 _ => {}
807 }
808 }
809 let bracket_pos = bracket_pos?;
810
811 let before_bracket = &source[..bracket_pos];
813 let name_end = before_bracket.trim_end().len();
814 let name_start = before_bracket[..name_end]
815 .rfind(|c: char| !c.is_alphanumeric() && c != '_')
816 .map(|i| i + 1)
817 .unwrap_or(0);
818 let name = &source[name_start..name_end];
819
820 if name.is_empty() || !name.chars().next()?.is_alphabetic() {
821 return None;
822 }
823
824 Some(TsCallContext {
825 name,
826 arg_index: 0,
827 arg_count: 1,
828 call_start_byte: name_start,
829 is_index_access: true,
830 })
831}
832
833fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
835 let end = byte_pos.min(source.len());
836 let text = &source[start..end];
837
838 let mut count = 0;
839 let mut depth = 0;
840 let mut found_open = false;
841 for ch in text.chars() {
842 match ch {
843 '(' if !found_open => {
844 found_open = true;
845 depth = 1;
846 }
847 '(' => depth += 1,
848 ')' => depth -= 1,
849 ',' if found_open && depth == 1 => count += 1,
850 _ => {}
851 }
852 }
853 count
854}
855
856fn collect_ts_gas_hints(
861 node: Node,
862 source: &str,
863 range: &Range,
864 gas_index: &gas::GasIndex,
865 abs_path: &str,
866 hints: &mut Vec<InlayHint>,
867) {
868 let node_start = node.start_position();
869 let node_end = node.end_position();
870 if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
871 return;
872 }
873
874 match node.kind() {
875 "function_definition" => {
876 if let Some(hint) = ts_gas_hint_for_function(node, source, range, gas_index, abs_path) {
877 hints.push(hint);
878 }
879 }
880 "contract_declaration" | "library_declaration" | "interface_declaration" => {
881 if let Some(hint) = ts_gas_hint_for_contract(node, source, range, gas_index, abs_path) {
882 hints.push(hint);
883 }
884 }
885 _ => {}
886 }
887
888 let mut cursor = node.walk();
889 for child in node.children(&mut cursor) {
890 collect_ts_gas_hints(child, source, range, gas_index, abs_path, hints);
891 }
892}
893
894fn ts_node_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
896 let mut cursor = node.walk();
897 node.children(&mut cursor)
898 .find(|c| c.kind() == "identifier" && c.is_named())
899 .map(|c| &source[c.byte_range()])
900}
901
902fn ts_body_open_brace(node: Node, body_kind: &str) -> Option<Position> {
904 let mut cursor = node.walk();
905 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
906 let start = body.start_position();
907 Some(Position::new(start.row as u32, start.column as u32))
908}
909
910fn ts_body_close_brace(node: Node, body_kind: &str) -> Option<Position> {
912 let mut cursor = node.walk();
913 let body = node.children(&mut cursor).find(|c| c.kind() == body_kind)?;
914 let end = body.end_position();
915 Some(Position::new(
917 end.row as u32,
918 end.column.saturating_sub(1) as u32,
919 ))
920}
921
922fn ts_enclosing_contract_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
924 let mut parent = node.parent();
925 while let Some(p) = parent {
926 if p.kind() == "contract_declaration"
927 || p.kind() == "library_declaration"
928 || p.kind() == "interface_declaration"
929 {
930 return ts_node_name(p, source);
931 }
932 parent = p.parent();
933 }
934 None
935}
936
937fn find_gas_key<'a>(
939 gas_index: &'a gas::GasIndex,
940 abs_path: &str,
941 contract_name: &str,
942) -> Option<&'a str> {
943 let exact = format!("{abs_path}:{contract_name}");
944 if gas_index.contains_key(&exact) {
945 return Some(gas_index.get_key_value(&exact)?.0.as_str());
946 }
947 let file_name = std::path::Path::new(abs_path).file_name()?.to_str()?;
948 let suffix = format!("{file_name}:{contract_name}");
949 gas_index
950 .keys()
951 .find(|k| k.ends_with(&suffix))
952 .map(|k| k.as_str())
953}
954
955fn has_gas_sentinel(node: Node, source: &str) -> bool {
960 let mut prev = node.prev_named_sibling();
961 while let Some(sibling) = prev {
962 if sibling.kind() == "comment" {
963 let text = &source[sibling.byte_range()];
964 if text.contains(gas::GAS_SENTINEL) {
965 return true;
966 }
967 } else {
968 break;
969 }
970 prev = sibling.prev_named_sibling();
971 }
972 false
973}
974
975fn ts_gas_hint_for_function(
977 node: Node,
978 source: &str,
979 range: &Range,
980 gas_index: &gas::GasIndex,
981 abs_path: &str,
982) -> Option<InlayHint> {
983 if !has_gas_sentinel(node, source) {
985 return None;
986 }
987 let fn_name = ts_node_name(node, source)?;
988 let contract_name = ts_enclosing_contract_name(node, source)?;
989 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
990 let contract_gas = gas_index.get(gas_key)?;
991
992 let prefix = format!("{fn_name}(");
993 let cost = contract_gas
994 .external_by_sig
995 .iter()
996 .find(|(sig, _)| sig.as_str().starts_with(&prefix))
997 .map(|(_, c)| c.as_str())
998 .or_else(|| {
999 contract_gas
1000 .internal
1001 .iter()
1002 .find(|(sig, _)| sig.starts_with(&prefix))
1003 .map(|(_, c)| c.as_str())
1004 })?;
1005
1006 let (brace_pos, offset) = match FN_GAS_HINT_POSITION {
1008 FnGasHintPosition::Opening => (ts_body_open_brace(node, "function_body")?, 1),
1009 FnGasHintPosition::Closing => (ts_body_close_brace(node, "function_body")?, 1),
1010 };
1011 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1012 return None;
1013 }
1014
1015 Some(InlayHint {
1016 position: Position::new(brace_pos.line, brace_pos.character + offset),
1017 kind: Some(InlayHintKind::TYPE),
1018 label: InlayHintLabel::String(format!("🔥 gas: {}", gas::format_gas(cost))),
1019 text_edits: None,
1020 tooltip: Some(InlayHintTooltip::String("Estimated gas cost".to_string())),
1021 padding_left: Some(true),
1022 padding_right: None,
1023 data: None,
1024 })
1025}
1026
1027fn ts_gas_hint_for_contract(
1030 node: Node,
1031 source: &str,
1032 range: &Range,
1033 gas_index: &gas::GasIndex,
1034 abs_path: &str,
1035) -> Option<InlayHint> {
1036 if !has_gas_sentinel(node, source) {
1038 return None;
1039 }
1040 let contract_name = ts_node_name(node, source)?;
1041 let gas_key = find_gas_key(gas_index, abs_path, contract_name)?;
1042 let contract_gas = gas_index.get(gas_key)?;
1043
1044 let display_cost = match contract_gas.creation.get("totalCost").map(|s| s.as_str()) {
1046 Some("infinite") | None => contract_gas
1047 .creation
1048 .get("codeDepositCost")
1049 .map(|s| s.as_str())?,
1050 Some(total) => total,
1051 };
1052
1053 let brace_pos = ts_body_open_brace(node, "contract_body")?;
1054 if brace_pos.line < range.start.line || brace_pos.line > range.end.line {
1055 return None;
1056 }
1057
1058 Some(InlayHint {
1059 position: Position::new(brace_pos.line, brace_pos.character + 1),
1060 kind: Some(InlayHintKind::TYPE),
1061 label: InlayHintLabel::String(format!("🔥 deploy: {} ", gas::format_gas(display_cost))),
1062 text_edits: None,
1063 tooltip: Some(InlayHintTooltip::String(format!(
1064 "Deploy cost — code deposit: {}, execution: {}",
1065 gas::format_gas(
1066 contract_gas
1067 .creation
1068 .get("codeDepositCost")
1069 .map(|s| s.as_str())
1070 .unwrap_or("?")
1071 ),
1072 gas::format_gas(
1073 contract_gas
1074 .creation
1075 .get("executionCost")
1076 .map(|s| s.as_str())
1077 .unwrap_or("?")
1078 )
1079 ))),
1080 padding_left: Some(true),
1081 padding_right: None,
1082 data: None,
1083 })
1084}
1085
1086fn get_parameter_names(decl: &Value) -> Option<Vec<String>> {
1090 let items = decl
1093 .get("parameters")
1094 .and_then(|p| p.get("parameters"))
1095 .and_then(|v| v.as_array())
1096 .or_else(|| decl.get("members").and_then(|v| v.as_array()))?;
1097 Some(
1098 items
1099 .iter()
1100 .map(|p| {
1101 p.get("name")
1102 .and_then(|v| v.as_str())
1103 .unwrap_or("")
1104 .to_string()
1105 })
1106 .collect(),
1107 )
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113
1114 #[test]
1115 fn test_gas_sentinel_present() {
1116 let source = r#"
1117contract Foo {
1118 /// @custom:lsp-enable gas-estimates
1119 function bar() public {}
1120}
1121"#;
1122 let tree = ts_parse(source).unwrap();
1123 let root = tree.root_node();
1124 let contract = root.child(0).unwrap();
1126 let body = contract.child_by_field_name("body").unwrap();
1127 let mut cursor = body.walk();
1128 let fn_node = body
1129 .children(&mut cursor)
1130 .find(|c| c.kind() == "function_definition")
1131 .unwrap();
1132 assert!(has_gas_sentinel(fn_node, source));
1133 }
1134
1135 #[test]
1136 fn test_gas_sentinel_absent() {
1137 let source = r#"
1138contract Foo {
1139 function bar() public {}
1140}
1141"#;
1142 let tree = ts_parse(source).unwrap();
1143 let root = tree.root_node();
1144 let contract = root.child(0).unwrap();
1145 let body = contract.child_by_field_name("body").unwrap();
1146 let mut cursor = body.walk();
1147 let fn_node = body
1148 .children(&mut cursor)
1149 .find(|c| c.kind() == "function_definition")
1150 .unwrap();
1151 assert!(!has_gas_sentinel(fn_node, source));
1152 }
1153
1154 #[test]
1155 fn test_gas_sentinel_with_other_natspec() {
1156 let source = r#"
1157contract Foo {
1158 /// @notice Does something
1159 /// @custom:lsp-enable gas-estimates
1160 function bar() public {}
1161}
1162"#;
1163 let tree = ts_parse(source).unwrap();
1164 let root = tree.root_node();
1165 let contract = root.child(0).unwrap();
1166 let body = contract.child_by_field_name("body").unwrap();
1167 let mut cursor = body.walk();
1168 let fn_node = body
1169 .children(&mut cursor)
1170 .find(|c| c.kind() == "function_definition")
1171 .unwrap();
1172 assert!(has_gas_sentinel(fn_node, source));
1173 }
1174
1175 #[test]
1176 fn test_get_parameter_names() {
1177 let decl: Value = serde_json::json!({
1178 "parameters": {
1179 "parameters": [
1180 {"name": "to", "nodeType": "VariableDeclaration"},
1181 {"name": "amount", "nodeType": "VariableDeclaration"},
1182 ]
1183 }
1184 });
1185 let names = get_parameter_names(&decl).unwrap();
1186 assert_eq!(names, vec!["to", "amount"]);
1187 }
1188
1189 #[test]
1190 fn test_ts_call_function_name() {
1191 let source = r#"
1192contract Foo {
1193 function bar(uint x) public {}
1194 function test() public {
1195 bar(42);
1196 }
1197}
1198"#;
1199 let tree = ts_parse(source).unwrap();
1200 let mut found = Vec::new();
1201 find_calls(tree.root_node(), source, &mut found);
1202 assert_eq!(found.len(), 1);
1203 assert_eq!(found[0], "bar");
1204 }
1205
1206 #[test]
1207 fn test_ts_member_call_name() {
1208 let source = r#"
1209contract Foo {
1210 function test() public {
1211 PRICE.addTax(TAX, TAX_BASE);
1212 }
1213}
1214"#;
1215 let tree = ts_parse(source).unwrap();
1216 let mut found = Vec::new();
1217 find_calls(tree.root_node(), source, &mut found);
1218 assert_eq!(found.len(), 1);
1219 assert_eq!(found[0], "addTax");
1220 }
1221
1222 #[test]
1223 fn test_ts_emit_event_name() {
1224 let source = r#"
1225contract Foo {
1226 event Purchase(address buyer, uint256 price);
1227 function test() public {
1228 emit Purchase(msg.sender, 100);
1229 }
1230}
1231"#;
1232 let tree = ts_parse(source).unwrap();
1233 let mut found = Vec::new();
1234 find_emits(tree.root_node(), source, &mut found);
1235 assert_eq!(found.len(), 1);
1236 assert_eq!(found[0], "Purchase");
1237 }
1238
1239 #[test]
1240 fn test_ts_call_arguments_count() {
1241 let source = r#"
1242contract Foo {
1243 function bar(uint x, uint y) public {}
1244 function test() public {
1245 bar(1, 2);
1246 }
1247}
1248"#;
1249 let tree = ts_parse(source).unwrap();
1250 let mut arg_counts = Vec::new();
1251 find_call_arg_counts(tree.root_node(), &mut arg_counts);
1252 assert_eq!(arg_counts, vec![2]);
1253 }
1254
1255 #[test]
1256 fn test_ts_argument_positions_follow_live_buffer() {
1257 let source = r#"
1259contract Foo {
1260 function bar(uint x, uint y) public {}
1261 function test() public {
1262 bar(
1263 1,
1264 2
1265 );
1266 }
1267}
1268"#;
1269 let tree = ts_parse(source).unwrap();
1270 let mut positions = Vec::new();
1271 find_arg_positions(tree.root_node(), &mut positions);
1272 assert_eq!(positions.len(), 2);
1274 assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
1277
1278 fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1281 if node.kind() == "call_expression"
1282 && let Some(name) = ts_call_function_name(node, source)
1283 {
1284 out.push(name);
1285 }
1286 let mut cursor = node.walk();
1287 for child in node.children(&mut cursor) {
1288 find_calls(child, source, out);
1289 }
1290 }
1291
1292 fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
1293 if node.kind() == "emit_statement"
1294 && let Some(name) = ts_emit_event_name(node, source)
1295 {
1296 out.push(name);
1297 }
1298 let mut cursor = node.walk();
1299 for child in node.children(&mut cursor) {
1300 find_emits(child, source, out);
1301 }
1302 }
1303
1304 fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
1305 if node.kind() == "call_expression" {
1306 out.push(ts_call_arguments(node).len());
1307 }
1308 let mut cursor = node.walk();
1309 for child in node.children(&mut cursor) {
1310 find_call_arg_counts(child, out);
1311 }
1312 }
1313
1314 fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
1315 if node.kind() == "call_expression" {
1316 for arg in ts_call_arguments(node) {
1317 let p = arg.start_position();
1318 out.push((p.row, p.column));
1319 }
1320 }
1321 let mut cursor = node.walk();
1322 for child in node.children(&mut cursor) {
1323 find_arg_positions(child, out);
1324 }
1325 }
1326
1327 #[test]
1328 fn test_ts_find_call_at_byte_first_arg() {
1329 let source = r#"
1330contract Foo {
1331 function bar(uint x, uint y) public {}
1332 function test() public {
1333 bar(42, 99);
1334 }
1335}
1336"#;
1337 let tree = ts_parse(source).unwrap();
1338 let pos_42 = source.find("42").unwrap();
1340 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1341 assert_eq!(ctx.name, "bar");
1342 assert_eq!(ctx.arg_index, 0);
1343 assert_eq!(ctx.arg_count, 2);
1344 }
1345
1346 #[test]
1347 fn test_ts_find_call_at_byte_second_arg() {
1348 let source = r#"
1349contract Foo {
1350 function bar(uint x, uint y) public {}
1351 function test() public {
1352 bar(42, 99);
1353 }
1354}
1355"#;
1356 let tree = ts_parse(source).unwrap();
1357 let pos_99 = source.find("99").unwrap();
1358 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1359 assert_eq!(ctx.name, "bar");
1360 assert_eq!(ctx.arg_index, 1);
1361 assert_eq!(ctx.arg_count, 2);
1362 }
1363
1364 #[test]
1365 fn test_ts_find_call_at_byte_outside_call_returns_none() {
1366 let source = r#"
1367contract Foo {
1368 function bar(uint x) public {}
1369 function test() public {
1370 uint z = 10;
1371 bar(42);
1372 }
1373}
1374"#;
1375 let tree = ts_parse(source).unwrap();
1376 let pos_10 = source.find("10").unwrap();
1378 assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
1379 }
1380
1381 #[test]
1382 fn test_ts_find_call_at_byte_member_call() {
1383 let source = r#"
1384contract Foo {
1385 function test() public {
1386 PRICE.addTax(TAX, TAX_BASE);
1387 }
1388}
1389"#;
1390 let tree = ts_parse(source).unwrap();
1391 let pos_tax = source.find("TAX,").unwrap();
1392 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
1393 assert_eq!(ctx.name, "addTax");
1394 assert_eq!(ctx.arg_index, 0);
1395 assert_eq!(ctx.arg_count, 2);
1396 }
1397
1398 #[test]
1399 fn test_ts_find_call_at_byte_emit_statement() {
1400 let source = r#"
1401contract Foo {
1402 event Purchase(address buyer, uint256 price);
1403 function test() public {
1404 emit Purchase(msg.sender, 100);
1405 }
1406}
1407"#;
1408 let tree = ts_parse(source).unwrap();
1409 let pos_100 = source.find("100").unwrap();
1410 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
1411 assert_eq!(ctx.name, "Purchase");
1412 assert_eq!(ctx.arg_index, 1);
1413 assert_eq!(ctx.arg_count, 2);
1414 }
1415
1416 #[test]
1417 fn test_ts_find_call_at_byte_multiline() {
1418 let source = r#"
1419contract Foo {
1420 function bar(uint x, uint y, uint z) public {}
1421 function test() public {
1422 bar(
1423 1,
1424 2,
1425 3
1426 );
1427 }
1428}
1429"#;
1430 let tree = ts_parse(source).unwrap();
1431 let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
1435 assert_eq!(ctx.name, "bar");
1436 assert_eq!(ctx.arg_index, 1);
1437 assert_eq!(ctx.arg_count, 3);
1438 }
1439
1440 #[test]
1441 fn test_resolve_callsite_param_basic() {
1442 let mut lookup = HintLookup {
1444 by_offset: HashMap::new(),
1445 by_name: HashMap::new(),
1446 };
1447 lookup.by_name.insert(
1448 ("transfer".to_string(), 2),
1449 CallSite {
1450 info: ParamInfo {
1451 names: vec!["to".to_string(), "amount".to_string()],
1452 skip: 0,
1453 },
1454 name: "transfer".to_string(),
1455 decl_id: 42,
1456 },
1457 );
1458
1459 let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
1461 assert_eq!(result.param_name, "to");
1462 assert_eq!(result.decl_id, 42);
1463
1464 let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
1466 assert_eq!(result.param_name, "amount");
1467 assert_eq!(result.decl_id, 42);
1468 }
1469
1470 #[test]
1471 fn test_resolve_callsite_param_with_skip() {
1472 let mut lookup = HintLookup {
1474 by_offset: HashMap::new(),
1475 by_name: HashMap::new(),
1476 };
1477 lookup.by_name.insert(
1478 ("addTax".to_string(), 2),
1479 CallSite {
1480 info: ParamInfo {
1481 names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
1482 skip: 1,
1483 },
1484 name: "addTax".to_string(),
1485 decl_id: 99,
1486 },
1487 );
1488
1489 let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
1491 assert_eq!(result.param_name, "tax");
1492
1493 let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
1495 assert_eq!(result.param_name, "base");
1496 }
1497
1498 #[test]
1499 fn test_resolve_callsite_param_out_of_bounds() {
1500 let mut lookup = HintLookup {
1501 by_offset: HashMap::new(),
1502 by_name: HashMap::new(),
1503 };
1504 lookup.by_name.insert(
1505 ("foo".to_string(), 1),
1506 CallSite {
1507 info: ParamInfo {
1508 names: vec!["x".to_string()],
1509 skip: 0,
1510 },
1511 name: "foo".to_string(),
1512 decl_id: 1,
1513 },
1514 );
1515
1516 assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
1518 }
1519
1520 #[test]
1521 fn test_resolve_callsite_param_unknown_function() {
1522 let lookup = HintLookup {
1523 by_offset: HashMap::new(),
1524 by_name: HashMap::new(),
1525 };
1526 assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
1527 }
1528
1529 #[test]
1530 fn test_ts_find_call_at_byte_emit_member_access() {
1531 let source = r#"
1535contract Foo {
1536 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1537 function test() public {
1538 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1539 }
1540}
1541"#;
1542 let tree = ts_parse(source).unwrap();
1543 let params_tick = source.find("params.tickLower,").unwrap();
1545 let tick_lower_pos = params_tick + "params.".len(); let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
1548 assert_eq!(ctx.name, "ModifyLiquidity");
1549 assert_eq!(
1550 ctx.arg_index, 2,
1551 "params.tickLower is the 3rd argument (index 2)"
1552 );
1553 assert_eq!(ctx.arg_count, 4);
1554 }
1555
1556 #[test]
1557 fn test_ts_find_call_at_byte_member_access_on_property() {
1558 let source = r#"
1560contract Foo {
1561 event Transfer(address from, address to);
1562 function test() public {
1563 emit Transfer(msg.sender, addr);
1564 }
1565}
1566"#;
1567 let tree = ts_parse(source).unwrap();
1568 let sender_pos = source.find("sender").unwrap();
1569 let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
1570 assert_eq!(ctx.name, "Transfer");
1571 assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
1572 }
1573
1574 #[test]
1575 fn test_ts_find_call_at_byte_emit_all_args() {
1576 let source = r#"
1578contract Foo {
1579 event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
1580 function test() public {
1581 emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
1582 }
1583}
1584"#;
1585 let tree = ts_parse(source).unwrap();
1586
1587 let pos_id = source.find("(id,").unwrap() + 1;
1589 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
1590 assert_eq!(ctx.name, "ModifyLiquidity");
1591 assert_eq!(ctx.arg_index, 0);
1592
1593 let pos_msg = source.find("msg.sender").unwrap();
1595 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
1596 assert_eq!(ctx.arg_index, 1);
1597
1598 let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
1600 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
1601 assert_eq!(ctx.arg_index, 2);
1602
1603 let pos_tu = source.find("params.tickUpper").unwrap();
1605 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
1606 assert_eq!(ctx.arg_index, 3);
1607 }
1608
1609 #[test]
1610 fn test_ts_find_call_at_byte_nested_call_arg() {
1611 let source = r#"
1614contract Foo {
1615 function inner(uint x) public returns (uint) {}
1616 function outer(uint a, uint b) public {}
1617 function test() public {
1618 outer(inner(42), 99);
1619 }
1620}
1621"#;
1622 let tree = ts_parse(source).unwrap();
1623
1624 let pos_42 = source.find("42").unwrap();
1626 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
1627 assert_eq!(ctx.name, "inner");
1628 assert_eq!(ctx.arg_index, 0);
1629
1630 let pos_99 = source.find("99").unwrap();
1632 let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
1633 assert_eq!(ctx.name, "outer");
1634 assert_eq!(ctx.arg_index, 1);
1635 }
1636
1637 #[test]
1638 fn test_ts_find_call_for_signature_incomplete_call() {
1639 let source = r#"
1641contract Foo {
1642 function bar(uint x, uint y) public {}
1643 function test() public {
1644 bar(
1645 }
1646}
1647"#;
1648 let tree = ts_parse(source).unwrap();
1649 let pos = source.find("bar(").unwrap() + 4;
1650 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1651 assert_eq!(ctx.name, "bar");
1652 assert_eq!(ctx.arg_index, 0);
1653 }
1654
1655 #[test]
1656 fn test_ts_find_call_for_signature_after_comma() {
1657 let source = r#"
1659contract Foo {
1660 function bar(uint x, uint y) public {}
1661 function test() public {
1662 bar(42,
1663 }
1664}
1665"#;
1666 let tree = ts_parse(source).unwrap();
1667 let pos = source.find("42,").unwrap() + 3;
1668 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1669 assert_eq!(ctx.name, "bar");
1670 assert_eq!(ctx.arg_index, 1);
1671 }
1672
1673 #[test]
1674 fn test_ts_find_call_for_signature_complete_call() {
1675 let source = r#"
1677contract Foo {
1678 function bar(uint x, uint y) public {}
1679 function test() public {
1680 bar(42, 99);
1681 }
1682}
1683"#;
1684 let tree = ts_parse(source).unwrap();
1685 let pos = source.find("42").unwrap();
1686 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1687 assert_eq!(ctx.name, "bar");
1688 assert_eq!(ctx.arg_index, 0);
1689 }
1690
1691 #[test]
1692 fn test_ts_find_call_for_signature_member_call() {
1693 let source = r#"
1695contract Foo {
1696 function test() public {
1697 PRICE.addTax(
1698 }
1699}
1700"#;
1701 let tree = ts_parse(source).unwrap();
1702 let pos = source.find("addTax(").unwrap() + 7;
1703 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1704 assert_eq!(ctx.name, "addTax");
1705 assert_eq!(ctx.arg_index, 0);
1706 }
1707
1708 #[test]
1709 fn test_ts_find_call_for_signature_array_access() {
1710 let source = r#"
1712contract Foo {
1713 mapping(bytes32 => uint256) public orders;
1714 function test() public {
1715 orders[orderId];
1716 }
1717}
1718"#;
1719 let tree = ts_parse(source).unwrap();
1720 let pos = source.find("[orderId]").unwrap() + 1;
1722 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1723 assert_eq!(ctx.name, "orders");
1724 assert_eq!(ctx.arg_index, 0);
1725 assert!(ctx.is_index_access);
1726 }
1727
1728 #[test]
1729 fn test_ts_find_call_for_signature_array_access_empty() {
1730 let source = r#"
1732contract Foo {
1733 mapping(bytes32 => uint256) public orders;
1734 function test() public {
1735 orders[
1736 }
1737}
1738"#;
1739 let tree = ts_parse(source).unwrap();
1740 let pos = source.find("orders[").unwrap() + 7;
1741 let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
1742 assert_eq!(ctx.name, "orders");
1743 assert!(ctx.is_index_access);
1744 }
1745}