1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Location, Position, Range, Url};
4use tree_sitter::{Node, Parser};
5
6use crate::types::{NodeId, SourceLoc};
7
8#[derive(Debug, Clone)]
9pub struct NodeInfo {
10 pub src: String,
11 pub name_location: Option<String>,
12 pub name_locations: Vec<String>,
13 pub referenced_declaration: Option<NodeId>,
14 pub node_type: Option<String>,
15 pub member_location: Option<String>,
16 pub absolute_path: Option<String>,
17}
18
19pub const CHILD_KEYS: &[&str] = &[
21 "AST",
22 "arguments",
23 "baseContracts",
24 "baseExpression",
25 "baseName",
26 "baseType",
27 "block",
28 "body",
29 "components",
30 "condition",
31 "declarations",
32 "endExpression",
33 "errorCall",
34 "eventCall",
35 "expression",
36 "externalCall",
37 "falseBody",
38 "falseExpression",
39 "file",
40 "foreign",
41 "functionName",
42 "indexExpression",
43 "initialValue",
44 "initializationExpression",
45 "keyType",
46 "leftExpression",
47 "leftHandSide",
48 "libraryName",
49 "literals",
50 "loopExpression",
51 "members",
52 "modifierName",
53 "modifiers",
54 "name",
55 "names",
56 "nodes",
57 "options",
58 "overrides",
59 "parameters",
60 "pathNode",
61 "post",
62 "pre",
63 "returnParameters",
64 "rightExpression",
65 "rightHandSide",
66 "startExpression",
67 "statements",
68 "storageLayout",
69 "subExpression",
70 "subdenomination",
71 "symbolAliases",
72 "trueBody",
73 "trueExpression",
74 "typeName",
75 "unitAlias",
76 "value",
77 "valueType",
78 "variableNames",
79 "variables",
80];
81
82fn push_if_node_or_array<'a>(tree: &'a Value, key: &str, stack: &mut Vec<&'a Value>) {
83 if let Some(value) = tree.get(key) {
84 match value {
85 Value::Array(arr) => {
86 stack.extend(arr);
87 }
88 Value::Object(_) => {
89 stack.push(value);
90 }
91 _ => {}
92 }
93 }
94}
95
96pub type ExternalRefs = HashMap<String, NodeId>;
99
100#[derive(Debug, Clone)]
103pub struct CachedBuild {
104 pub ast: Value,
105 pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
106 pub path_to_abs: HashMap<String, String>,
107 pub external_refs: ExternalRefs,
108 pub id_to_path_map: HashMap<String, String>,
109 pub gas_index: crate::gas::GasIndex,
112 pub hint_index: crate::inlay_hints::HintIndex,
115 pub doc_index: crate::hover::DocIndex,
118 pub build_version: i32,
121}
122
123impl CachedBuild {
124 pub fn new(ast: Value, build_version: i32) -> Self {
131 let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
132 cache_ids(sources)
133 } else {
134 (HashMap::new(), HashMap::new(), HashMap::new())
135 };
136
137 let id_to_path_map = ast
138 .get("source_id_to_path")
139 .and_then(|v| v.as_object())
140 .map(|obj| {
141 obj.iter()
142 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
143 .collect()
144 })
145 .unwrap_or_default();
146
147 let gas_index = crate::gas::build_gas_index(&ast);
148
149 let hint_index = if let Some(sources) = ast.get("sources") {
150 crate::inlay_hints::build_hint_index(sources)
151 } else {
152 HashMap::new()
153 };
154
155 let doc_index = crate::hover::build_doc_index(&ast);
156
157 Self {
158 ast,
159 nodes,
160 path_to_abs,
161 external_refs,
162 id_to_path_map,
163 gas_index,
164 hint_index,
165 doc_index,
166 build_version,
167 }
168 }
169}
170
171type Type = (
172 HashMap<String, HashMap<NodeId, NodeInfo>>,
173 HashMap<String, String>,
174 ExternalRefs,
175);
176
177pub fn cache_ids(sources: &Value) -> Type {
178 let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> = HashMap::new();
179 let mut path_to_abs: HashMap<String, String> = HashMap::new();
180 let mut external_refs: ExternalRefs = HashMap::new();
181
182 if let Some(sources_obj) = sources.as_object() {
183 for (path, source_data) in sources_obj {
184 if let Some(ast) = source_data.get("ast") {
185 let abs_path = ast
187 .get("absolutePath")
188 .and_then(|v| v.as_str())
189 .unwrap_or(path)
190 .to_string();
191
192 path_to_abs.insert(path.clone(), abs_path.clone());
193
194 if !nodes.contains_key(&abs_path) {
196 nodes.insert(abs_path.clone(), HashMap::new());
197 }
198
199 if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
200 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
201 {
202 nodes.get_mut(&abs_path).unwrap().insert(
203 NodeId(id),
204 NodeInfo {
205 src: src.to_string(),
206 name_location: None,
207 name_locations: vec![],
208 referenced_declaration: None,
209 node_type: ast
210 .get("nodeType")
211 .and_then(|v| v.as_str())
212 .map(|s| s.to_string()),
213 member_location: None,
214 absolute_path: ast
215 .get("absolutePath")
216 .and_then(|v| v.as_str())
217 .map(|s| s.to_string()),
218 },
219 );
220 }
221
222 let mut stack = vec![ast];
223
224 while let Some(tree) = stack.pop() {
225 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
226 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
227 {
228 let id = NodeId(raw_id);
229 let mut name_location = tree
231 .get("nameLocation")
232 .and_then(|v| v.as_str())
233 .map(|s| s.to_string());
234
235 if name_location.is_none()
239 && let Some(name_locations) = tree.get("nameLocations")
240 && let Some(locations_array) = name_locations.as_array()
241 && !locations_array.is_empty()
242 {
243 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
244 if node_type == Some("IdentifierPath") {
245 name_location = locations_array
246 .last()
247 .and_then(|v| v.as_str())
248 .map(|s| s.to_string());
249 } else {
250 name_location = locations_array[0].as_str().map(|s| s.to_string());
251 }
252 }
253
254 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
255 && let Some(locations_array) = name_locations.as_array()
256 {
257 locations_array
258 .iter()
259 .filter_map(|v| v.as_str().map(|s| s.to_string()))
260 .collect()
261 } else {
262 vec![]
263 };
264
265 let mut final_name_location = name_location;
266 if final_name_location.is_none()
267 && let Some(member_loc) =
268 tree.get("memberLocation").and_then(|v| v.as_str())
269 {
270 final_name_location = Some(member_loc.to_string());
271 }
272
273 let node_info = NodeInfo {
274 src: src.to_string(),
275 name_location: final_name_location,
276 name_locations,
277 referenced_declaration: tree
278 .get("referencedDeclaration")
279 .and_then(|v| v.as_u64())
280 .map(NodeId),
281 node_type: tree
282 .get("nodeType")
283 .and_then(|v| v.as_str())
284 .map(|s| s.to_string()),
285 member_location: tree
286 .get("memberLocation")
287 .and_then(|v| v.as_str())
288 .map(|s| s.to_string()),
289 absolute_path: tree
290 .get("absolutePath")
291 .and_then(|v| v.as_str())
292 .map(|s| s.to_string()),
293 };
294
295 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
296
297 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
299 && let Some(ext_refs) =
300 tree.get("externalReferences").and_then(|v| v.as_array())
301 {
302 for ext_ref in ext_refs {
303 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
304 && let Some(decl_id) =
305 ext_ref.get("declaration").and_then(|v| v.as_u64())
306 {
307 external_refs.insert(src_str.to_string(), NodeId(decl_id));
308 }
309 }
310 }
311 }
312
313 for key in CHILD_KEYS {
314 push_if_node_or_array(tree, key, &mut stack);
315 }
316 }
317 }
318 }
319 }
320
321 (nodes, path_to_abs, external_refs)
322}
323
324pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
325 let text = String::from_utf8_lossy(source_bytes);
326 crate::utils::position_to_byte_offset(&text, position)
327}
328
329pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
330 let text = String::from_utf8_lossy(source_bytes);
331 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
332 Some(pos)
333}
334
335pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
337 let loc = SourceLoc::parse(src)?;
338 let file_path = id_to_path.get(&loc.file_id_str())?;
339
340 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
341 std::path::PathBuf::from(file_path)
342 } else {
343 std::env::current_dir().ok()?.join(file_path)
344 };
345
346 let source_bytes = std::fs::read(&absolute_path).ok()?;
347 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
348 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
349 let uri = Url::from_file_path(&absolute_path).ok()?;
350
351 Some(Location {
352 uri,
353 range: Range {
354 start: start_pos,
355 end: end_pos,
356 },
357 })
358}
359
360pub fn goto_bytes(
361 nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
362 path_to_abs: &HashMap<String, String>,
363 id_to_path: &HashMap<String, String>,
364 external_refs: &ExternalRefs,
365 uri: &str,
366 position: usize,
367) -> Option<(String, usize, usize)> {
368 let path = match uri.starts_with("file://") {
369 true => &uri[7..],
370 false => uri,
371 };
372
373 let abs_path = path_to_abs.get(path)?;
375
376 let current_file_nodes = nodes.get(abs_path)?;
378
379 let path_to_file_id: HashMap<&str, &str> = id_to_path
381 .iter()
382 .map(|(id, p)| (p.as_str(), id.as_str()))
383 .collect();
384
385 let current_file_id = path_to_file_id.get(abs_path.as_str());
389
390 for (src_str, decl_id) in external_refs {
392 let Some(src_loc) = SourceLoc::parse(src_str) else {
393 continue;
394 };
395
396 if let Some(file_id) = current_file_id {
398 if src_loc.file_id_str() != *file_id {
399 continue;
400 }
401 } else {
402 continue;
403 }
404
405 if src_loc.offset <= position && position < src_loc.end() {
406 let mut target_node: Option<&NodeInfo> = None;
408 for file_nodes in nodes.values() {
409 if let Some(node) = file_nodes.get(decl_id) {
410 target_node = Some(node);
411 break;
412 }
413 }
414 let node = target_node?;
415 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
416 let loc = SourceLoc::parse(loc_str)?;
417 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
418 return Some((file_path, loc.offset, loc.length));
419 }
420 }
421
422 let mut refs = HashMap::new();
423
424 for (id, content) in current_file_nodes {
426 if content.referenced_declaration.is_none() {
427 continue;
428 }
429
430 let Some(src_loc) = SourceLoc::parse(&content.src) else {
431 continue;
432 };
433
434 if src_loc.offset <= position && position < src_loc.end() {
435 let diff = src_loc.length;
436 if !refs.contains_key(&diff) || refs[&diff] <= *id {
437 refs.insert(diff, *id);
438 }
439 }
440 }
441
442 if refs.is_empty() {
443 let tmp = current_file_nodes.iter();
446 for (_id, content) in tmp {
447 if content.node_type == Some("ImportDirective".to_string()) {
448 let Some(src_loc) = SourceLoc::parse(&content.src) else {
449 continue;
450 };
451
452 if src_loc.offset <= position
453 && position < src_loc.end()
454 && let Some(import_path) = &content.absolute_path
455 {
456 return Some((import_path.clone(), 0, 0));
457 }
458 }
459 }
460 return None;
461 }
462
463 let min_diff = *refs.keys().min()?;
465 let chosen_id = refs[&min_diff];
466 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
467
468 let mut target_node: Option<&NodeInfo> = None;
470 for file_nodes in nodes.values() {
471 if let Some(node) = file_nodes.get(&ref_id) {
472 target_node = Some(node);
473 break;
474 }
475 }
476
477 let node = target_node?;
478
479 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
481 let loc = SourceLoc::parse(loc_str)?;
482 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
483
484 Some((file_path, loc.offset, loc.length))
485}
486
487pub fn goto_declaration(
488 ast_data: &Value,
489 file_uri: &Url,
490 position: Position,
491 source_bytes: &[u8],
492) -> Option<Location> {
493 let sources = ast_data.get("sources")?;
494 let id_to_path = ast_data.get("source_id_to_path")?.as_object()?;
495
496 let id_to_path_map: HashMap<String, String> = id_to_path
497 .iter()
498 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
499 .collect();
500
501 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
502 let byte_position = pos_to_bytes(source_bytes, position);
503
504 if let Some((file_path, location_bytes, length)) = goto_bytes(
505 &nodes,
506 &path_to_abs,
507 &id_to_path_map,
508 &external_refs,
509 file_uri.as_ref(),
510 byte_position,
511 ) {
512 let target_file_path = std::path::Path::new(&file_path);
513 let absolute_path = if target_file_path.is_absolute() {
514 target_file_path.to_path_buf()
515 } else {
516 std::env::current_dir().ok()?.join(target_file_path)
517 };
518
519 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
520 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
521 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
522 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
523 {
524 return Some(Location {
525 uri: target_uri,
526 range: Range {
527 start: start_pos,
528 end: end_pos,
529 },
530 });
531 }
532 };
533
534 None
535}
536
537pub fn goto_declaration_by_name(
548 cached_build: &CachedBuild,
549 file_uri: &Url,
550 name: &str,
551 byte_hint: usize,
552) -> Option<Location> {
553 let path = match file_uri.as_ref().starts_with("file://") {
554 true => &file_uri.as_ref()[7..],
555 false => file_uri.as_ref(),
556 };
557 let abs_path = cached_build.path_to_abs.get(path)?;
558 let built_source = std::fs::read_to_string(abs_path).ok()?;
560
561 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
563
564 let mut tmp = {
565 let this = cached_build.nodes.get(abs_path)?;
566 this.iter()
567 };
568 while let Some((_id, node)) = tmp.next() {
569 let ref_id = match node.referenced_declaration {
570 Some(id) => id,
571 None => continue,
572 };
573
574 let Some(src_loc) = SourceLoc::parse(&node.src) else {
576 continue;
577 };
578 let start = src_loc.offset;
579 let length = src_loc.length;
580
581 if start + length > built_source.len() {
582 continue;
583 }
584
585 let node_text = &built_source[start..start + length];
586
587 let matches = node_text == name
592 || node_text.contains(&format!(".{name}("))
593 || node_text.ends_with(&format!(".{name}"));
594
595 if matches {
596 let distance = if byte_hint >= start && byte_hint < start + length {
600 0 } else if byte_hint < start {
602 start - byte_hint
603 } else {
604 byte_hint - (start + length)
605 };
606 candidates.push((distance, length, ref_id));
607 }
608 }
609
610 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
612 let ref_id = candidates.first()?.2;
613
614 let mut target_node: Option<&NodeInfo> = None;
616 for file_nodes in cached_build.nodes.values() {
617 if let Some(node) = file_nodes.get(&ref_id) {
618 target_node = Some(node);
619 break;
620 }
621 }
622
623 let node = target_node?;
624
625 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
627 let loc = SourceLoc::parse(loc_str)?;
628
629 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
630 let location_bytes = loc.offset;
631 let length = loc.length;
632
633 let target_file_path = std::path::Path::new(file_path);
634 let absolute_path = if target_file_path.is_absolute() {
635 target_file_path.to_path_buf()
636 } else {
637 std::env::current_dir().ok()?.join(target_file_path)
638 };
639
640 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
641 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
642 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
643 let target_uri = Url::from_file_path(&absolute_path).ok()?;
644
645 Some(Location {
646 uri: target_uri,
647 range: Range {
648 start: start_pos,
649 end: end_pos,
650 },
651 })
652}
653
654#[derive(Debug, Clone)]
658pub struct CursorContext {
659 pub name: String,
661 pub function: Option<String>,
663 pub contract: Option<String>,
665 pub object: Option<String>,
669 pub arg_count: Option<usize>,
672 pub arg_types: Vec<Option<String>>,
675}
676
677fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
679 let mut parser = Parser::new();
680 parser
681 .set_language(&tree_sitter_solidity::LANGUAGE.into())
682 .expect("failed to load Solidity grammar");
683 parser.parse(source, None)
684}
685
686pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
692 let line = location.range.start.line as usize;
693 let start_col = location.range.start.character as usize;
694 let end_col = location.range.end.character as usize;
695
696 if let Some(line_text) = target_source.lines().nth(line)
697 && end_col <= line_text.len()
698 {
699 return &line_text[start_col..end_col] == expected_name;
700 }
701 true
703}
704
705fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
707 if byte < node.start_byte() || byte >= node.end_byte() {
708 return None;
709 }
710 let mut cursor = node.walk();
711 for child in node.children(&mut cursor) {
712 if child.start_byte() <= byte
713 && byte < child.end_byte()
714 && let Some(deeper) = ts_node_at_byte(child, byte)
715 {
716 return Some(deeper);
717 }
718 }
719 Some(node)
720}
721
722fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
724 let mut cursor = node.walk();
725 node.children(&mut cursor)
726 .find(|c| c.kind() == "identifier" && c.is_named())
727 .map(|c| &source[c.byte_range()])
728}
729
730fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
736 let expr = if arg_node.kind() == "call_argument" {
738 let mut c = arg_node.walk();
739 arg_node.children(&mut c).find(|ch| ch.is_named())?
740 } else {
741 arg_node
742 };
743
744 match expr.kind() {
745 "identifier" => {
746 let var_name = &source[expr.byte_range()];
747 find_variable_type(expr, source, var_name)
749 }
750 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
751 "boolean_literal" => Some("bool".into()),
752 "string_literal" | "hex_string_literal" => Some("string".into()),
753 _ => None,
754 }
755}
756
757fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
762 let mut scope = from.parent();
763 while let Some(node) = scope {
764 match node.kind() {
765 "function_definition" | "modifier_definition" | "constructor_definition" => {
766 let mut c = node.walk();
768 for child in node.children(&mut c) {
769 if child.kind() == "parameter"
770 && let Some(id) = ts_child_id_text(child, source)
771 && id == var_name
772 {
773 let mut pc = child.walk();
775 return child
776 .children(&mut pc)
777 .find(|c| {
778 matches!(
779 c.kind(),
780 "type_name"
781 | "primitive_type"
782 | "user_defined_type"
783 | "mapping"
784 )
785 })
786 .map(|t| source[t.byte_range()].trim().to_string());
787 }
788 }
789 }
790 "function_body" | "block_statement" | "unchecked_block" => {
791 let mut c = node.walk();
793 for child in node.children(&mut c) {
794 if child.kind() == "variable_declaration_statement"
795 || child.kind() == "variable_declaration"
796 {
797 if let Some(id) = ts_child_id_text(child, source)
798 && id == var_name
799 {
800 let mut pc = child.walk();
801 return child
802 .children(&mut pc)
803 .find(|c| {
804 matches!(
805 c.kind(),
806 "type_name"
807 | "primitive_type"
808 | "user_defined_type"
809 | "mapping"
810 )
811 })
812 .map(|t| source[t.byte_range()].trim().to_string());
813 }
814 }
815 }
816 }
817 "contract_declaration" | "library_declaration" | "interface_declaration" => {
818 if let Some(body) = ts_find_child(node, "contract_body") {
820 let mut c = body.walk();
821 for child in body.children(&mut c) {
822 if child.kind() == "state_variable_declaration"
823 && let Some(id) = ts_child_id_text(child, source)
824 && id == var_name
825 {
826 let mut pc = child.walk();
827 return child
828 .children(&mut pc)
829 .find(|c| {
830 matches!(
831 c.kind(),
832 "type_name"
833 | "primitive_type"
834 | "user_defined_type"
835 | "mapping"
836 )
837 })
838 .map(|t| source[t.byte_range()].trim().to_string());
839 }
840 }
841 }
842 }
843 _ => {}
844 }
845 scope = node.parent();
846 }
847 None
848}
849
850fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
852 let mut cursor = call_node.walk();
853 call_node
854 .children(&mut cursor)
855 .filter(|c| c.kind() == "call_argument")
856 .map(|arg| infer_argument_type(arg, source))
857 .collect()
858}
859
860fn best_overload<'a>(
868 decls: &'a [TsDeclaration],
869 arg_count: Option<usize>,
870 arg_types: &[Option<String>],
871) -> Option<&'a TsDeclaration> {
872 if decls.len() == 1 {
873 return decls.first();
874 }
875 if decls.is_empty() {
876 return None;
877 }
878
879 let func_decls: Vec<&TsDeclaration> =
881 decls.iter().filter(|d| d.param_count.is_some()).collect();
882
883 if func_decls.is_empty() {
884 return decls.first();
885 }
886
887 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
889 let matched: Vec<_> = func_decls
890 .iter()
891 .filter(|d| d.param_count == Some(ac))
892 .collect();
893 if matched.len() == 1 {
894 return Some(matched[0]);
895 }
896 if matched.is_empty() {
897 func_decls.iter().collect()
899 } else {
900 matched
901 }
902 } else {
903 func_decls.iter().collect()
904 };
905
906 if !arg_types.is_empty() {
908 let mut best: Option<(&TsDeclaration, usize)> = None;
909 for &&decl in &count_matched {
910 let score = arg_types
911 .iter()
912 .zip(decl.param_types.iter())
913 .filter(|(arg_ty, param_ty)| {
914 if let Some(at) = arg_ty {
915 at == param_ty.as_str()
916 } else {
917 false
918 }
919 })
920 .count();
921 if best.is_none() || score > best.unwrap().1 {
922 best = Some((decl, score));
923 }
924 }
925 if let Some((decl, _)) = best {
926 return Some(decl);
927 }
928 }
929
930 count_matched.first().map(|d| **d).or(decls.first())
932}
933
934pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
938 let tree = ts_parse(source)?;
939 let byte = pos_to_bytes(source.as_bytes(), position);
940 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
941
942 let id_node = if leaf.kind() == "identifier" {
944 leaf
945 } else {
946 let parent = leaf.parent()?;
948 if parent.kind() == "identifier" {
949 parent
950 } else {
951 return None;
952 }
953 };
954
955 let name = source[id_node.byte_range()].to_string();
956 let mut function = None;
957 let mut contract = None;
958
959 let object = id_node.parent().and_then(|parent| {
963 if parent.kind() == "member_expression" {
964 let prop = parent.child_by_field_name("property")?;
965 if prop.id() == id_node.id() {
967 let obj = parent.child_by_field_name("object")?;
968 Some(source[obj.byte_range()].to_string())
969 } else {
970 None
971 }
972 } else {
973 None
974 }
975 });
976
977 let (arg_count, arg_types) = {
981 let mut node = id_node.parent();
982 let mut result = (None, vec![]);
983 while let Some(n) = node {
984 if n.kind() == "call_expression" {
985 let types = infer_call_arg_types(n, source);
986 result = (Some(types.len()), types);
987 break;
988 }
989 node = n.parent();
990 }
991 result
992 };
993
994 let mut current = id_node.parent();
996 while let Some(node) = current {
997 match node.kind() {
998 "function_definition" | "modifier_definition" if function.is_none() => {
999 function = ts_child_id_text(node, source).map(String::from);
1000 }
1001 "constructor_definition" if function.is_none() => {
1002 function = Some("constructor".into());
1003 }
1004 "contract_declaration" | "interface_declaration" | "library_declaration"
1005 if contract.is_none() =>
1006 {
1007 contract = ts_child_id_text(node, source).map(String::from);
1008 }
1009 _ => {}
1010 }
1011 current = node.parent();
1012 }
1013
1014 Some(CursorContext {
1015 name,
1016 function,
1017 contract,
1018 object,
1019 arg_count,
1020 arg_types,
1021 })
1022}
1023
1024#[derive(Debug, Clone)]
1026pub struct TsDeclaration {
1027 pub range: Range,
1029 pub kind: &'static str,
1031 pub container: Option<String>,
1033 pub param_count: Option<usize>,
1035 pub param_types: Vec<String>,
1038}
1039
1040pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1045 let tree = match ts_parse(source) {
1046 Some(t) => t,
1047 None => return vec![],
1048 };
1049 let mut results = Vec::new();
1050 collect_declarations(tree.root_node(), source, name, None, &mut results);
1051 results
1052}
1053
1054fn collect_declarations(
1055 node: Node,
1056 source: &str,
1057 name: &str,
1058 container: Option<&str>,
1059 out: &mut Vec<TsDeclaration>,
1060) {
1061 let mut cursor = node.walk();
1062 for child in node.children(&mut cursor) {
1063 if !child.is_named() {
1064 continue;
1065 }
1066 match child.kind() {
1067 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1068 if let Some(id_name) = ts_child_id_text(child, source) {
1069 if id_name == name {
1070 out.push(TsDeclaration {
1071 range: id_range(child),
1072 kind: child.kind(),
1073 container: container.map(String::from),
1074 param_count: None,
1075 param_types: vec![],
1076 });
1077 }
1078 if let Some(body) = ts_find_child(child, "contract_body") {
1080 collect_declarations(body, source, name, Some(id_name), out);
1081 }
1082 }
1083 }
1084 "function_definition" | "modifier_definition" => {
1085 if let Some(id_name) = ts_child_id_text(child, source) {
1086 if id_name == name {
1087 let types = parameter_type_signature(child, source);
1088 out.push(TsDeclaration {
1089 range: id_range(child),
1090 kind: child.kind(),
1091 container: container.map(String::from),
1092 param_count: Some(types.len()),
1093 param_types: types.into_iter().map(String::from).collect(),
1094 });
1095 }
1096 collect_parameters(child, source, name, container, out);
1098 if let Some(body) = ts_find_child(child, "function_body") {
1100 collect_declarations(body, source, name, container, out);
1101 }
1102 }
1103 }
1104 "constructor_definition" => {
1105 if name == "constructor" {
1106 let types = parameter_type_signature(child, source);
1107 out.push(TsDeclaration {
1108 range: ts_range(child),
1109 kind: "constructor_definition",
1110 container: container.map(String::from),
1111 param_count: Some(types.len()),
1112 param_types: types.into_iter().map(String::from).collect(),
1113 });
1114 }
1115 collect_parameters(child, source, name, container, out);
1117 if let Some(body) = ts_find_child(child, "function_body") {
1118 collect_declarations(body, source, name, container, out);
1119 }
1120 }
1121 "state_variable_declaration" | "variable_declaration" => {
1122 if let Some(id_name) = ts_child_id_text(child, source)
1123 && id_name == name
1124 {
1125 out.push(TsDeclaration {
1126 range: id_range(child),
1127 kind: child.kind(),
1128 container: container.map(String::from),
1129 param_count: None,
1130 param_types: vec![],
1131 });
1132 }
1133 }
1134 "struct_declaration" => {
1135 if let Some(id_name) = ts_child_id_text(child, source) {
1136 if id_name == name {
1137 out.push(TsDeclaration {
1138 range: id_range(child),
1139 kind: "struct_declaration",
1140 container: container.map(String::from),
1141 param_count: None,
1142 param_types: vec![],
1143 });
1144 }
1145 if let Some(body) = ts_find_child(child, "struct_body") {
1146 collect_declarations(body, source, name, Some(id_name), out);
1147 }
1148 }
1149 }
1150 "enum_declaration" => {
1151 if let Some(id_name) = ts_child_id_text(child, source) {
1152 if id_name == name {
1153 out.push(TsDeclaration {
1154 range: id_range(child),
1155 kind: "enum_declaration",
1156 container: container.map(String::from),
1157 param_count: None,
1158 param_types: vec![],
1159 });
1160 }
1161 if let Some(body) = ts_find_child(child, "enum_body") {
1163 let mut ecur = body.walk();
1164 for val in body.children(&mut ecur) {
1165 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1166 out.push(TsDeclaration {
1167 range: ts_range(val),
1168 kind: "enum_value",
1169 container: Some(id_name.to_string()),
1170 param_count: None,
1171 param_types: vec![],
1172 });
1173 }
1174 }
1175 }
1176 }
1177 }
1178 "event_definition" | "error_declaration" => {
1179 if let Some(id_name) = ts_child_id_text(child, source)
1180 && id_name == name
1181 {
1182 out.push(TsDeclaration {
1183 range: id_range(child),
1184 kind: child.kind(),
1185 container: container.map(String::from),
1186 param_count: None,
1187 param_types: vec![],
1188 });
1189 }
1190 }
1191 "user_defined_type_definition" => {
1192 if let Some(id_name) = ts_child_id_text(child, source)
1193 && id_name == name
1194 {
1195 out.push(TsDeclaration {
1196 range: id_range(child),
1197 kind: "user_defined_type_definition",
1198 container: container.map(String::from),
1199 param_count: None,
1200 param_types: vec![],
1201 });
1202 }
1203 }
1204 _ => {
1206 collect_declarations(child, source, name, container, out);
1207 }
1208 }
1209 }
1210}
1211
1212fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1218 let mut cursor = node.walk();
1219 node.children(&mut cursor)
1220 .filter(|c| c.kind() == "parameter")
1221 .filter_map(|param| {
1222 let mut pc = param.walk();
1223 param
1224 .children(&mut pc)
1225 .find(|c| {
1226 matches!(
1227 c.kind(),
1228 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1229 )
1230 })
1231 .map(|t| source[t.byte_range()].trim())
1232 })
1233 .collect()
1234}
1235
1236fn collect_parameters(
1238 node: Node,
1239 source: &str,
1240 name: &str,
1241 container: Option<&str>,
1242 out: &mut Vec<TsDeclaration>,
1243) {
1244 let mut cursor = node.walk();
1245 for child in node.children(&mut cursor) {
1246 if child.kind() == "parameter"
1247 && let Some(id_name) = ts_child_id_text(child, source)
1248 && id_name == name
1249 {
1250 out.push(TsDeclaration {
1251 range: id_range(child),
1252 kind: "parameter",
1253 container: container.map(String::from),
1254 param_count: None,
1255 param_types: vec![],
1256 });
1257 }
1258 }
1259}
1260
1261fn ts_range(node: Node) -> Range {
1263 let s = node.start_position();
1264 let e = node.end_position();
1265 Range {
1266 start: Position::new(s.row as u32, s.column as u32),
1267 end: Position::new(e.row as u32, e.column as u32),
1268 }
1269}
1270
1271fn id_range(node: Node) -> Range {
1273 let mut cursor = node.walk();
1274 node.children(&mut cursor)
1275 .find(|c| c.kind() == "identifier" && c.is_named())
1276 .map(|c| ts_range(c))
1277 .unwrap_or_else(|| ts_range(node))
1278}
1279
1280fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1281 let mut cursor = node.walk();
1282 node.children(&mut cursor).find(|c| c.kind() == kind)
1283}
1284
1285pub fn goto_definition_ts(
1293 source: &str,
1294 position: Position,
1295 file_uri: &Url,
1296 completion_cache: &crate::completion::CompletionCache,
1297 text_cache: &HashMap<String, (i32, String)>,
1298) -> Option<Location> {
1299 let ctx = cursor_context(source, position)?;
1300
1301 if let Some(obj_name) = &ctx.object {
1306 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1307 let target_source = read_target_source(&path, text_cache)?;
1308 let target_uri = Url::from_file_path(&path).ok()?;
1309 let decls = find_declarations_by_name(&target_source, &ctx.name);
1310 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1311 return Some(Location {
1312 uri: target_uri,
1313 range: d.range,
1314 });
1315 }
1316 }
1317 let decls = find_declarations_by_name(source, &ctx.name);
1319 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1320 return Some(Location {
1321 uri: file_uri.clone(),
1322 range: d.range,
1323 });
1324 }
1325 }
1326
1327 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1330
1331 match resolved {
1332 Some(ResolvedTarget::SameFile) => {
1333 find_best_declaration(source, &ctx, file_uri)
1335 }
1336 Some(ResolvedTarget::OtherFile { path, name }) => {
1337 let target_source = read_target_source(&path, text_cache);
1339 let target_source = target_source?;
1340 let target_uri = Url::from_file_path(&path).ok()?;
1341 let decls = find_declarations_by_name(&target_source, &name);
1342 decls.first().map(|d| Location {
1343 uri: target_uri,
1344 range: d.range,
1345 })
1346 }
1347 None => {
1348 find_best_declaration(source, &ctx, file_uri)
1350 }
1351 }
1352}
1353
1354#[derive(Debug)]
1355enum ResolvedTarget {
1356 SameFile,
1358 OtherFile { path: String, name: String },
1360}
1361
1362fn resolve_via_cache(
1368 ctx: &CursorContext,
1369 file_uri: &Url,
1370 cache: &crate::completion::CompletionCache,
1371) -> Option<ResolvedTarget> {
1372 let contract_scope = ctx
1374 .contract
1375 .as_ref()
1376 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1377 .copied();
1378
1379 if let Some(contract_id) = contract_scope {
1381 if let Some(func_name) = &ctx.function {
1383 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1386 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1388 && decls.iter().any(|d| d.name == ctx.name)
1389 {
1390 return Some(ResolvedTarget::SameFile);
1391 }
1392 }
1393 }
1394
1395 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1397 && decls.iter().any(|d| d.name == ctx.name)
1398 {
1399 return Some(ResolvedTarget::SameFile);
1400 }
1401
1402 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1404 for &base_id in bases.iter().skip(1) {
1405 if let Some(decls) = cache.scope_declarations.get(&base_id)
1406 && decls.iter().any(|d| d.name == ctx.name)
1407 {
1408 let base_name = cache
1411 .name_to_node_id
1412 .iter()
1413 .find(|&(_, &id)| id == base_id)
1414 .map(|(name, _)| name.clone());
1415
1416 if let Some(base_name) = base_name
1417 && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1418 {
1419 return Some(ResolvedTarget::OtherFile {
1420 path,
1421 name: ctx.name.clone(),
1422 });
1423 }
1424 return Some(ResolvedTarget::SameFile);
1426 }
1427 }
1428 }
1429 }
1430
1431 if cache.name_to_node_id.contains_key(&ctx.name) {
1433 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1435 let current_path = file_uri.to_file_path().ok()?;
1436 let current_str = current_path.to_str()?;
1437 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1438 return Some(ResolvedTarget::SameFile);
1439 }
1440 return Some(ResolvedTarget::OtherFile {
1441 path,
1442 name: ctx.name.clone(),
1443 });
1444 }
1445 return Some(ResolvedTarget::SameFile);
1446 }
1447
1448 if cache.name_to_type.contains_key(&ctx.name) {
1450 return Some(ResolvedTarget::SameFile);
1451 }
1452
1453 None
1454}
1455
1456fn find_function_scope(
1458 cache: &crate::completion::CompletionCache,
1459 contract_id: NodeId,
1460 func_name: &str,
1461) -> Option<NodeId> {
1462 for (&scope_id, &parent_id) in &cache.scope_parent {
1466 if parent_id == contract_id {
1467 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1471 && contract_decls.iter().any(|d| d.name == func_name)
1472 {
1473 if cache.scope_declarations.contains_key(&scope_id)
1477 || cache.scope_parent.values().any(|&p| p == scope_id)
1478 {
1479 return Some(scope_id);
1480 }
1481 }
1482 }
1483 }
1484 None
1485}
1486
1487fn find_file_for_contract(
1489 cache: &crate::completion::CompletionCache,
1490 contract_name: &str,
1491 _file_uri: &Url,
1492) -> Option<String> {
1493 let node_id = cache.name_to_node_id.get(contract_name)?;
1498 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1499 let file_id = scope_range.file_id;
1500
1501 cache
1503 .path_to_file_id
1504 .iter()
1505 .find(|&(_, &fid)| fid == file_id)
1506 .map(|(path, _)| path.clone())
1507}
1508
1509fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1511 let uri = Url::from_file_path(path).ok()?;
1513 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1514 return Some(content.clone());
1515 }
1516 std::fs::read_to_string(path).ok()
1518}
1519
1520fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1522 let decls = find_declarations_by_name(source, &ctx.name);
1523 if decls.is_empty() {
1524 return None;
1525 }
1526
1527 if decls.len() == 1 {
1529 return Some(Location {
1530 uri: file_uri.clone(),
1531 range: decls[0].range,
1532 });
1533 }
1534
1535 if let Some(contract_name) = &ctx.contract
1537 && let Some(d) = decls
1538 .iter()
1539 .find(|d| d.container.as_deref() == Some(contract_name))
1540 {
1541 return Some(Location {
1542 uri: file_uri.clone(),
1543 range: d.range,
1544 });
1545 }
1546
1547 Some(Location {
1549 uri: file_uri.clone(),
1550 range: decls[0].range,
1551 })
1552}
1553
1554#[cfg(test)]
1555mod ts_tests {
1556 use super::*;
1557
1558 #[test]
1559 fn test_cursor_context_state_var() {
1560 let source = r#"
1561contract Token {
1562 uint256 public totalSupply;
1563 function mint(uint256 amount) public {
1564 totalSupply += amount;
1565 }
1566}
1567"#;
1568 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
1570 assert_eq!(ctx.name, "totalSupply");
1571 assert_eq!(ctx.function.as_deref(), Some("mint"));
1572 assert_eq!(ctx.contract.as_deref(), Some("Token"));
1573 }
1574
1575 #[test]
1576 fn test_cursor_context_top_level() {
1577 let source = r#"
1578contract Foo {}
1579contract Bar {}
1580"#;
1581 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
1583 assert_eq!(ctx.name, "Foo");
1584 assert!(ctx.function.is_none());
1585 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
1587 }
1588
1589 #[test]
1590 fn test_find_declarations() {
1591 let source = r#"
1592contract Token {
1593 uint256 public totalSupply;
1594 function mint(uint256 amount) public {
1595 totalSupply += amount;
1596 }
1597}
1598"#;
1599 let decls = find_declarations_by_name(source, "totalSupply");
1600 assert_eq!(decls.len(), 1);
1601 assert_eq!(decls[0].kind, "state_variable_declaration");
1602 assert_eq!(decls[0].container.as_deref(), Some("Token"));
1603 }
1604
1605 #[test]
1606 fn test_find_declarations_multiple_contracts() {
1607 let source = r#"
1608contract A {
1609 uint256 public value;
1610}
1611contract B {
1612 uint256 public value;
1613}
1614"#;
1615 let decls = find_declarations_by_name(source, "value");
1616 assert_eq!(decls.len(), 2);
1617 assert_eq!(decls[0].container.as_deref(), Some("A"));
1618 assert_eq!(decls[1].container.as_deref(), Some("B"));
1619 }
1620
1621 #[test]
1622 fn test_find_declarations_enum_value() {
1623 let source = "contract Foo { enum Status { Active, Paused } }";
1624 let decls = find_declarations_by_name(source, "Active");
1625 assert_eq!(decls.len(), 1);
1626 assert_eq!(decls[0].kind, "enum_value");
1627 assert_eq!(decls[0].container.as_deref(), Some("Status"));
1628 }
1629
1630 #[test]
1631 fn test_cursor_context_short_param() {
1632 let source = r#"
1633contract Shop {
1634 uint256 public TAX;
1635 constructor(uint256 price, uint16 tax, uint16 taxBase) {
1636 TAX = tax;
1637 }
1638}
1639"#;
1640 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
1642 assert_eq!(ctx.name, "tax");
1643 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
1644
1645 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
1647 assert_eq!(ctx2.name, "TAX");
1648
1649 let decls = find_declarations_by_name(source, "tax");
1651 assert_eq!(decls.len(), 1);
1652 assert_eq!(decls[0].kind, "parameter");
1653
1654 let decls_tax_base = find_declarations_by_name(source, "taxBase");
1655 assert_eq!(decls_tax_base.len(), 1);
1656 assert_eq!(decls_tax_base[0].kind, "parameter");
1657
1658 let decls_price = find_declarations_by_name(source, "price");
1659 assert_eq!(decls_price.len(), 1);
1660 assert_eq!(decls_price[0].kind, "parameter");
1661
1662 let decls_tax_upper = find_declarations_by_name(source, "TAX");
1664 assert_eq!(decls_tax_upper.len(), 1);
1665 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
1666 }
1667
1668 #[test]
1669 fn test_find_best_declaration_same_contract() {
1670 let source = r#"
1671contract A { uint256 public x; }
1672contract B { uint256 public x; }
1673"#;
1674 let ctx = CursorContext {
1675 name: "x".into(),
1676 function: None,
1677 contract: Some("B".into()),
1678 object: None,
1679 arg_count: None,
1680 arg_types: vec![],
1681 };
1682 let uri = Url::parse("file:///test.sol").unwrap();
1683 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
1684 assert_eq!(loc.range.start.line, 2);
1686 }
1687}