1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use tower_lsp::lsp_types::{Location, Position, Range, Url};
5use tree_sitter::{Node, Parser};
6
7use crate::types::{NodeId, SourceLoc};
8use crate::utils::push_if_node_or_array;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct NodeInfo {
12 pub src: String,
13 pub name_location: Option<String>,
14 pub name_locations: Vec<String>,
15 pub referenced_declaration: Option<NodeId>,
16 pub node_type: Option<String>,
17 pub member_location: Option<String>,
18 pub absolute_path: Option<String>,
19}
20
21pub const CHILD_KEYS: &[&str] = &[
23 "AST",
24 "arguments",
25 "baseContracts",
26 "baseExpression",
27 "baseName",
28 "baseType",
29 "block",
30 "body",
31 "components",
32 "condition",
33 "declarations",
34 "endExpression",
35 "errorCall",
36 "eventCall",
37 "expression",
38 "externalCall",
39 "falseBody",
40 "falseExpression",
41 "file",
42 "foreign",
43 "functionName",
44 "indexExpression",
45 "initialValue",
46 "initializationExpression",
47 "keyType",
48 "leftExpression",
49 "leftHandSide",
50 "libraryName",
51 "literals",
52 "loopExpression",
53 "members",
54 "modifierName",
55 "modifiers",
56 "name",
57 "names",
58 "nodes",
59 "options",
60 "overrides",
61 "parameters",
62 "pathNode",
63 "post",
64 "pre",
65 "returnParameters",
66 "rightExpression",
67 "rightHandSide",
68 "startExpression",
69 "statements",
70 "storageLayout",
71 "subExpression",
72 "subdenomination",
73 "symbolAliases",
74 "trueBody",
75 "trueExpression",
76 "typeName",
77 "unitAlias",
78 "value",
79 "valueType",
80 "variableNames",
81 "variables",
82];
83
84pub type ExternalRefs = HashMap<String, NodeId>;
87
88#[derive(Debug, Clone)]
94pub struct CachedBuild {
95 pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
96 pub path_to_abs: HashMap<String, String>,
97 pub external_refs: ExternalRefs,
98 pub id_to_path_map: HashMap<String, String>,
99 pub decl_index: HashMap<i64, crate::solc_ast::DeclNode>,
103 pub node_id_to_source_path: HashMap<i64, String>,
107 pub gas_index: crate::gas::GasIndex,
110 pub hint_index: crate::inlay_hints::HintIndex,
113 pub doc_index: crate::hover::DocIndex,
116 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
119 pub build_version: i32,
122}
123
124impl CachedBuild {
125 pub fn new(ast: Value, build_version: i32) -> Self {
132 let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
133 cache_ids(sources)
134 } else {
135 (HashMap::new(), HashMap::new(), HashMap::new())
136 };
137
138 let id_to_path_map = ast
139 .get("source_id_to_path")
140 .and_then(|v| v.as_object())
141 .map(|obj| {
142 obj.iter()
143 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
144 .collect()
145 })
146 .unwrap_or_default();
147
148 let gas_index = crate::gas::build_gas_index(&ast);
149
150 let doc_index = crate::hover::build_doc_index(&ast);
151
152 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
159 match crate::solc_ast::extract_decl_nodes(sources) {
160 Some(extracted) => (extracted.decl_index, extracted.node_id_to_source_path),
161 None => (HashMap::new(), HashMap::new()),
162 }
163 } else {
164 (HashMap::new(), HashMap::new())
165 };
166
167 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
169 let hint_index = if let Some(sources) = ast.get("sources") {
170 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
171 } else {
172 HashMap::new()
173 };
174
175 let completion_cache = {
177 let sources = ast.get("sources");
178 let contracts = ast.get("contracts");
179 let cc = if let Some(s) = sources {
180 crate::completion::build_completion_cache(s, contracts)
181 } else {
182 crate::completion::build_completion_cache(
183 &serde_json::Value::Object(Default::default()),
184 contracts,
185 )
186 };
187 std::sync::Arc::new(cc)
188 };
189
190 Self {
194 nodes,
195 path_to_abs,
196 external_refs,
197 id_to_path_map,
198 decl_index,
199 node_id_to_source_path,
200 gas_index,
201 hint_index,
202 doc_index,
203 completion_cache,
204 build_version,
205 }
206 }
207
208 pub fn from_reference_index(
213 nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
214 path_to_abs: HashMap<String, String>,
215 external_refs: ExternalRefs,
216 id_to_path_map: HashMap<String, String>,
217 build_version: i32,
218 ) -> Self {
219 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
220 &serde_json::Value::Object(Default::default()),
221 None,
222 ));
223
224 Self {
225 nodes,
226 path_to_abs,
227 external_refs,
228 id_to_path_map,
229 decl_index: HashMap::new(),
230 node_id_to_source_path: HashMap::new(),
231 gas_index: HashMap::new(),
232 hint_index: HashMap::new(),
233 doc_index: HashMap::new(),
234 completion_cache,
235 build_version,
236 }
237 }
238}
239
240type CachedIds = (
242 HashMap<String, HashMap<NodeId, NodeInfo>>,
243 HashMap<String, String>,
244 ExternalRefs,
245);
246
247pub fn cache_ids(sources: &Value) -> CachedIds {
248 let source_count = sources.as_object().map_or(0, |obj| obj.len());
249
250 let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> =
253 HashMap::with_capacity(source_count);
254 let mut path_to_abs: HashMap<String, String> = HashMap::with_capacity(source_count);
255 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
256
257 if let Some(sources_obj) = sources.as_object() {
258 for (path, source_data) in sources_obj {
259 if let Some(ast) = source_data.get("ast") {
260 let abs_path = ast
262 .get("absolutePath")
263 .and_then(|v| v.as_str())
264 .unwrap_or(path)
265 .to_string();
266
267 path_to_abs.insert(path.clone(), abs_path.clone());
268
269 let size_hint = ast
274 .get("nodes")
275 .and_then(|v| v.as_array())
276 .map_or(64, |arr| arr.len() * 8);
277 if !nodes.contains_key(&abs_path) {
278 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
279 }
280
281 if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
282 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
283 {
284 nodes.get_mut(&abs_path).unwrap().insert(
285 NodeId(id),
286 NodeInfo {
287 src: src.to_string(),
288 name_location: None,
289 name_locations: vec![],
290 referenced_declaration: None,
291 node_type: ast
292 .get("nodeType")
293 .and_then(|v| v.as_str())
294 .map(|s| s.to_string()),
295 member_location: None,
296 absolute_path: ast
297 .get("absolutePath")
298 .and_then(|v| v.as_str())
299 .map(|s| s.to_string()),
300 },
301 );
302 }
303
304 let mut stack = vec![ast];
305
306 while let Some(tree) = stack.pop() {
307 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
308 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
309 {
310 let id = NodeId(raw_id);
311 let mut name_location = tree
313 .get("nameLocation")
314 .and_then(|v| v.as_str())
315 .map(|s| s.to_string());
316
317 if name_location.is_none()
321 && let Some(name_locations) = tree.get("nameLocations")
322 && let Some(locations_array) = name_locations.as_array()
323 && !locations_array.is_empty()
324 {
325 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
326 if node_type == Some("IdentifierPath") {
327 name_location = locations_array
328 .last()
329 .and_then(|v| v.as_str())
330 .map(|s| s.to_string());
331 } else {
332 name_location = locations_array[0].as_str().map(|s| s.to_string());
333 }
334 }
335
336 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
337 && let Some(locations_array) = name_locations.as_array()
338 {
339 locations_array
340 .iter()
341 .filter_map(|v| v.as_str().map(|s| s.to_string()))
342 .collect()
343 } else {
344 vec![]
345 };
346
347 let mut final_name_location = name_location;
348 if final_name_location.is_none()
349 && let Some(member_loc) =
350 tree.get("memberLocation").and_then(|v| v.as_str())
351 {
352 final_name_location = Some(member_loc.to_string());
353 }
354
355 let node_info = NodeInfo {
356 src: src.to_string(),
357 name_location: final_name_location,
358 name_locations,
359 referenced_declaration: tree
360 .get("referencedDeclaration")
361 .and_then(|v| v.as_u64())
362 .map(NodeId),
363 node_type: tree
364 .get("nodeType")
365 .and_then(|v| v.as_str())
366 .map(|s| s.to_string()),
367 member_location: tree
368 .get("memberLocation")
369 .and_then(|v| v.as_str())
370 .map(|s| s.to_string()),
371 absolute_path: tree
372 .get("absolutePath")
373 .and_then(|v| v.as_str())
374 .map(|s| s.to_string()),
375 };
376
377 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
378
379 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
381 && let Some(ext_refs) =
382 tree.get("externalReferences").and_then(|v| v.as_array())
383 {
384 for ext_ref in ext_refs {
385 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
386 && let Some(decl_id) =
387 ext_ref.get("declaration").and_then(|v| v.as_u64())
388 {
389 external_refs.insert(src_str.to_string(), NodeId(decl_id));
390 }
391 }
392 }
393 }
394
395 for key in CHILD_KEYS {
396 push_if_node_or_array(tree, key, &mut stack);
397 }
398 }
399 }
400 }
401 }
402
403 (nodes, path_to_abs, external_refs)
404}
405
406pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
407 let text = String::from_utf8_lossy(source_bytes);
408 crate::utils::position_to_byte_offset(&text, position)
409}
410
411pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
412 let text = String::from_utf8_lossy(source_bytes);
413 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
414 Some(pos)
415}
416
417pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
419 let loc = SourceLoc::parse(src)?;
420 let file_path = id_to_path.get(&loc.file_id_str())?;
421
422 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
423 std::path::PathBuf::from(file_path)
424 } else {
425 std::env::current_dir().ok()?.join(file_path)
426 };
427
428 let source_bytes = std::fs::read(&absolute_path).ok()?;
429 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
430 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
431 let uri = Url::from_file_path(&absolute_path).ok()?;
432
433 Some(Location {
434 uri,
435 range: Range {
436 start: start_pos,
437 end: end_pos,
438 },
439 })
440}
441
442pub fn goto_bytes(
443 nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
444 path_to_abs: &HashMap<String, String>,
445 id_to_path: &HashMap<String, String>,
446 external_refs: &ExternalRefs,
447 uri: &str,
448 position: usize,
449) -> Option<(String, usize, usize)> {
450 let path = match uri.starts_with("file://") {
451 true => &uri[7..],
452 false => uri,
453 };
454
455 let abs_path = path_to_abs.get(path)?;
457
458 let current_file_nodes = nodes.get(abs_path)?;
460
461 let path_to_file_id: HashMap<&str, &str> = id_to_path
463 .iter()
464 .map(|(id, p)| (p.as_str(), id.as_str()))
465 .collect();
466
467 let current_file_id = path_to_file_id.get(abs_path.as_str());
471
472 for (src_str, decl_id) in external_refs {
474 let Some(src_loc) = SourceLoc::parse(src_str) else {
475 continue;
476 };
477
478 if let Some(file_id) = current_file_id {
480 if src_loc.file_id_str() != *file_id {
481 continue;
482 }
483 } else {
484 continue;
485 }
486
487 if src_loc.offset <= position && position < src_loc.end() {
488 let mut target_node: Option<&NodeInfo> = None;
490 for file_nodes in nodes.values() {
491 if let Some(node) = file_nodes.get(decl_id) {
492 target_node = Some(node);
493 break;
494 }
495 }
496 let node = target_node?;
497 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
498 let loc = SourceLoc::parse(loc_str)?;
499 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
500 return Some((file_path, loc.offset, loc.length));
501 }
502 }
503
504 let mut refs = HashMap::new();
505
506 for (id, content) in current_file_nodes {
508 if content.referenced_declaration.is_none() {
509 continue;
510 }
511
512 let Some(src_loc) = SourceLoc::parse(&content.src) else {
513 continue;
514 };
515
516 if src_loc.offset <= position && position < src_loc.end() {
517 let diff = src_loc.length;
518 if !refs.contains_key(&diff) || refs[&diff] <= *id {
519 refs.insert(diff, *id);
520 }
521 }
522 }
523
524 if refs.is_empty() {
525 let tmp = current_file_nodes.iter();
528 for (_id, content) in tmp {
529 if content.node_type == Some("ImportDirective".to_string()) {
530 let Some(src_loc) = SourceLoc::parse(&content.src) else {
531 continue;
532 };
533
534 if src_loc.offset <= position
535 && position < src_loc.end()
536 && let Some(import_path) = &content.absolute_path
537 {
538 return Some((import_path.clone(), 0, 0));
539 }
540 }
541 }
542 return None;
543 }
544
545 let min_diff = *refs.keys().min()?;
547 let chosen_id = refs[&min_diff];
548 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
549
550 let mut target_node: Option<&NodeInfo> = None;
552 for file_nodes in nodes.values() {
553 if let Some(node) = file_nodes.get(&ref_id) {
554 target_node = Some(node);
555 break;
556 }
557 }
558
559 let node = target_node?;
560
561 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
563 let loc = SourceLoc::parse(loc_str)?;
564 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
565
566 Some((file_path, loc.offset, loc.length))
567}
568
569pub fn goto_declaration_cached(
572 build: &CachedBuild,
573 file_uri: &Url,
574 position: Position,
575 source_bytes: &[u8],
576) -> Option<Location> {
577 let byte_position = pos_to_bytes(source_bytes, position);
578
579 if let Some((file_path, location_bytes, length)) = goto_bytes(
580 &build.nodes,
581 &build.path_to_abs,
582 &build.id_to_path_map,
583 &build.external_refs,
584 file_uri.as_ref(),
585 byte_position,
586 ) {
587 let target_file_path = std::path::Path::new(&file_path);
588 let absolute_path = if target_file_path.is_absolute() {
589 target_file_path.to_path_buf()
590 } else {
591 let base = file_uri
596 .to_file_path()
597 .ok()
598 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
599 .or_else(|| std::env::current_dir().ok())
600 .unwrap_or_default();
601 base.join(target_file_path)
602 };
603
604 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
605 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
606 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
607 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
608 {
609 return Some(Location {
610 uri: target_uri,
611 range: Range {
612 start: start_pos,
613 end: end_pos,
614 },
615 });
616 }
617 };
618
619 None
620}
621
622pub fn goto_declaration_by_name(
633 cached_build: &CachedBuild,
634 file_uri: &Url,
635 name: &str,
636 byte_hint: usize,
637) -> Option<Location> {
638 let path = match file_uri.as_ref().starts_with("file://") {
639 true => &file_uri.as_ref()[7..],
640 false => file_uri.as_ref(),
641 };
642 let abs_path = cached_build.path_to_abs.get(path)?;
643 let built_source = std::fs::read_to_string(abs_path).ok()?;
645
646 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
648
649 let tmp = {
650 let this = cached_build.nodes.get(abs_path)?;
651 this.iter()
652 };
653 for (_id, node) in tmp {
654 let ref_id = match node.referenced_declaration {
655 Some(id) => id,
656 None => continue,
657 };
658
659 let Some(src_loc) = SourceLoc::parse(&node.src) else {
661 continue;
662 };
663 let start = src_loc.offset;
664 let length = src_loc.length;
665
666 if start + length > built_source.len() {
667 continue;
668 }
669
670 let node_text = &built_source[start..start + length];
671
672 let matches = node_text == name
677 || node_text.contains(&format!(".{name}("))
678 || node_text.ends_with(&format!(".{name}"));
679
680 if matches {
681 let distance = if byte_hint >= start && byte_hint < start + length {
685 0 } else if byte_hint < start {
687 start - byte_hint
688 } else {
689 byte_hint - (start + length)
690 };
691 candidates.push((distance, length, ref_id));
692 }
693 }
694
695 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
697 let ref_id = candidates.first()?.2;
698
699 let mut target_node: Option<&NodeInfo> = None;
701 for file_nodes in cached_build.nodes.values() {
702 if let Some(node) = file_nodes.get(&ref_id) {
703 target_node = Some(node);
704 break;
705 }
706 }
707
708 let node = target_node?;
709
710 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
712 let loc = SourceLoc::parse(loc_str)?;
713
714 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
715 let location_bytes = loc.offset;
716 let length = loc.length;
717
718 let target_file_path = std::path::Path::new(file_path);
719 let absolute_path = if target_file_path.is_absolute() {
720 target_file_path.to_path_buf()
721 } else {
722 let base = file_uri
723 .to_file_path()
724 .ok()
725 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
726 .or_else(|| std::env::current_dir().ok())
727 .unwrap_or_default();
728 base.join(target_file_path)
729 };
730
731 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
732 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
733 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
734 let target_uri = Url::from_file_path(&absolute_path).ok()?;
735
736 Some(Location {
737 uri: target_uri,
738 range: Range {
739 start: start_pos,
740 end: end_pos,
741 },
742 })
743}
744
745#[derive(Debug, Clone)]
749pub struct CursorContext {
750 pub name: String,
752 pub function: Option<String>,
754 pub contract: Option<String>,
756 pub object: Option<String>,
760 pub arg_count: Option<usize>,
763 pub arg_types: Vec<Option<String>>,
766}
767
768fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
770 let mut parser = Parser::new();
771 parser
772 .set_language(&tree_sitter_solidity::LANGUAGE.into())
773 .expect("failed to load Solidity grammar");
774 parser.parse(source, None)
775}
776
777pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
783 let line = location.range.start.line as usize;
784 let start_col = location.range.start.character as usize;
785 let end_col = location.range.end.character as usize;
786
787 if let Some(line_text) = target_source.lines().nth(line)
788 && end_col <= line_text.len()
789 {
790 return &line_text[start_col..end_col] == expected_name;
791 }
792 true
794}
795
796fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
798 if byte < node.start_byte() || byte >= node.end_byte() {
799 return None;
800 }
801 let mut cursor = node.walk();
802 for child in node.children(&mut cursor) {
803 if child.start_byte() <= byte
804 && byte < child.end_byte()
805 && let Some(deeper) = ts_node_at_byte(child, byte)
806 {
807 return Some(deeper);
808 }
809 }
810 Some(node)
811}
812
813fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
815 let mut cursor = node.walk();
816 node.children(&mut cursor)
817 .find(|c| c.kind() == "identifier" && c.is_named())
818 .map(|c| &source[c.byte_range()])
819}
820
821fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
827 let expr = if arg_node.kind() == "call_argument" {
829 let mut c = arg_node.walk();
830 arg_node.children(&mut c).find(|ch| ch.is_named())?
831 } else {
832 arg_node
833 };
834
835 match expr.kind() {
836 "identifier" => {
837 let var_name = &source[expr.byte_range()];
838 find_variable_type(expr, source, var_name)
840 }
841 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
842 "boolean_literal" => Some("bool".into()),
843 "string_literal" | "hex_string_literal" => Some("string".into()),
844 _ => None,
845 }
846}
847
848fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
853 let mut scope = from.parent();
854 while let Some(node) = scope {
855 match node.kind() {
856 "function_definition" | "modifier_definition" | "constructor_definition" => {
857 let mut c = node.walk();
859 for child in node.children(&mut c) {
860 if child.kind() == "parameter"
861 && let Some(id) = ts_child_id_text(child, source)
862 && id == var_name
863 {
864 let mut pc = child.walk();
866 return child
867 .children(&mut pc)
868 .find(|c| {
869 matches!(
870 c.kind(),
871 "type_name"
872 | "primitive_type"
873 | "user_defined_type"
874 | "mapping"
875 )
876 })
877 .map(|t| source[t.byte_range()].trim().to_string());
878 }
879 }
880 }
881 "function_body" | "block_statement" | "unchecked_block" => {
882 let mut c = node.walk();
884 for child in node.children(&mut c) {
885 if (child.kind() == "variable_declaration_statement"
886 || child.kind() == "variable_declaration")
887 && let Some(id) = ts_child_id_text(child, source)
888 && id == var_name
889 {
890 let mut pc = child.walk();
891 return child
892 .children(&mut pc)
893 .find(|c| {
894 matches!(
895 c.kind(),
896 "type_name"
897 | "primitive_type"
898 | "user_defined_type"
899 | "mapping"
900 )
901 })
902 .map(|t| source[t.byte_range()].trim().to_string());
903 }
904 }
905 }
906 "contract_declaration" | "library_declaration" | "interface_declaration" => {
907 if let Some(body) = ts_find_child(node, "contract_body") {
909 let mut c = body.walk();
910 for child in body.children(&mut c) {
911 if child.kind() == "state_variable_declaration"
912 && let Some(id) = ts_child_id_text(child, source)
913 && id == var_name
914 {
915 let mut pc = child.walk();
916 return child
917 .children(&mut pc)
918 .find(|c| {
919 matches!(
920 c.kind(),
921 "type_name"
922 | "primitive_type"
923 | "user_defined_type"
924 | "mapping"
925 )
926 })
927 .map(|t| source[t.byte_range()].trim().to_string());
928 }
929 }
930 }
931 }
932 _ => {}
933 }
934 scope = node.parent();
935 }
936 None
937}
938
939fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
941 let mut cursor = call_node.walk();
942 call_node
943 .children(&mut cursor)
944 .filter(|c| c.kind() == "call_argument")
945 .map(|arg| infer_argument_type(arg, source))
946 .collect()
947}
948
949fn best_overload<'a>(
957 decls: &'a [TsDeclaration],
958 arg_count: Option<usize>,
959 arg_types: &[Option<String>],
960) -> Option<&'a TsDeclaration> {
961 if decls.len() == 1 {
962 return decls.first();
963 }
964 if decls.is_empty() {
965 return None;
966 }
967
968 let func_decls: Vec<&TsDeclaration> =
970 decls.iter().filter(|d| d.param_count.is_some()).collect();
971
972 if func_decls.is_empty() {
973 return decls.first();
974 }
975
976 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
978 let matched: Vec<_> = func_decls
979 .iter()
980 .filter(|d| d.param_count == Some(ac))
981 .collect();
982 if matched.len() == 1 {
983 return Some(matched[0]);
984 }
985 if matched.is_empty() {
986 func_decls.iter().collect()
988 } else {
989 matched
990 }
991 } else {
992 func_decls.iter().collect()
993 };
994
995 if !arg_types.is_empty() {
997 let mut best: Option<(&TsDeclaration, usize)> = None;
998 for &&decl in &count_matched {
999 let score = arg_types
1000 .iter()
1001 .zip(decl.param_types.iter())
1002 .filter(|(arg_ty, param_ty)| {
1003 if let Some(at) = arg_ty {
1004 at == param_ty.as_str()
1005 } else {
1006 false
1007 }
1008 })
1009 .count();
1010 if best.is_none() || score > best.unwrap().1 {
1011 best = Some((decl, score));
1012 }
1013 }
1014 if let Some((decl, _)) = best {
1015 return Some(decl);
1016 }
1017 }
1018
1019 count_matched.first().map(|d| **d).or(decls.first())
1021}
1022
1023pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1027 let tree = ts_parse(source)?;
1028 let byte = pos_to_bytes(source.as_bytes(), position);
1029 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1030
1031 let id_node = if leaf.kind() == "identifier" {
1033 leaf
1034 } else {
1035 let parent = leaf.parent()?;
1037 if parent.kind() == "identifier" {
1038 parent
1039 } else {
1040 return None;
1041 }
1042 };
1043
1044 let name = source[id_node.byte_range()].to_string();
1045 let mut function = None;
1046 let mut contract = None;
1047
1048 let object = id_node.parent().and_then(|parent| {
1052 if parent.kind() == "member_expression" {
1053 let prop = parent.child_by_field_name("property")?;
1054 if prop.id() == id_node.id() {
1056 let obj = parent.child_by_field_name("object")?;
1057 Some(source[obj.byte_range()].to_string())
1058 } else {
1059 None
1060 }
1061 } else {
1062 None
1063 }
1064 });
1065
1066 let (arg_count, arg_types) = {
1070 let mut node = id_node.parent();
1071 let mut result = (None, vec![]);
1072 while let Some(n) = node {
1073 if n.kind() == "call_expression" {
1074 let types = infer_call_arg_types(n, source);
1075 result = (Some(types.len()), types);
1076 break;
1077 }
1078 node = n.parent();
1079 }
1080 result
1081 };
1082
1083 let mut current = id_node.parent();
1085 while let Some(node) = current {
1086 match node.kind() {
1087 "function_definition" | "modifier_definition" if function.is_none() => {
1088 function = ts_child_id_text(node, source).map(String::from);
1089 }
1090 "constructor_definition" if function.is_none() => {
1091 function = Some("constructor".into());
1092 }
1093 "contract_declaration" | "interface_declaration" | "library_declaration"
1094 if contract.is_none() =>
1095 {
1096 contract = ts_child_id_text(node, source).map(String::from);
1097 }
1098 _ => {}
1099 }
1100 current = node.parent();
1101 }
1102
1103 Some(CursorContext {
1104 name,
1105 function,
1106 contract,
1107 object,
1108 arg_count,
1109 arg_types,
1110 })
1111}
1112
1113#[derive(Debug, Clone)]
1115pub struct TsDeclaration {
1116 pub range: Range,
1118 pub kind: &'static str,
1120 pub container: Option<String>,
1122 pub param_count: Option<usize>,
1124 pub param_types: Vec<String>,
1127}
1128
1129pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1134 let tree = match ts_parse(source) {
1135 Some(t) => t,
1136 None => return vec![],
1137 };
1138 let mut results = Vec::new();
1139 collect_declarations(tree.root_node(), source, name, None, &mut results);
1140 results
1141}
1142
1143fn collect_declarations(
1144 node: Node,
1145 source: &str,
1146 name: &str,
1147 container: Option<&str>,
1148 out: &mut Vec<TsDeclaration>,
1149) {
1150 let mut cursor = node.walk();
1151 for child in node.children(&mut cursor) {
1152 if !child.is_named() {
1153 continue;
1154 }
1155 match child.kind() {
1156 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1157 if let Some(id_name) = ts_child_id_text(child, source) {
1158 if id_name == name {
1159 out.push(TsDeclaration {
1160 range: id_range(child),
1161 kind: child.kind(),
1162 container: container.map(String::from),
1163 param_count: None,
1164 param_types: vec![],
1165 });
1166 }
1167 if let Some(body) = ts_find_child(child, "contract_body") {
1169 collect_declarations(body, source, name, Some(id_name), out);
1170 }
1171 }
1172 }
1173 "function_definition" | "modifier_definition" => {
1174 if let Some(id_name) = ts_child_id_text(child, source) {
1175 if id_name == name {
1176 let types = parameter_type_signature(child, source);
1177 out.push(TsDeclaration {
1178 range: id_range(child),
1179 kind: child.kind(),
1180 container: container.map(String::from),
1181 param_count: Some(types.len()),
1182 param_types: types.into_iter().map(String::from).collect(),
1183 });
1184 }
1185 collect_parameters(child, source, name, container, out);
1187 if let Some(body) = ts_find_child(child, "function_body") {
1189 collect_declarations(body, source, name, container, out);
1190 }
1191 }
1192 }
1193 "constructor_definition" => {
1194 if name == "constructor" {
1195 let types = parameter_type_signature(child, source);
1196 out.push(TsDeclaration {
1197 range: ts_range(child),
1198 kind: "constructor_definition",
1199 container: container.map(String::from),
1200 param_count: Some(types.len()),
1201 param_types: types.into_iter().map(String::from).collect(),
1202 });
1203 }
1204 collect_parameters(child, source, name, container, out);
1206 if let Some(body) = ts_find_child(child, "function_body") {
1207 collect_declarations(body, source, name, container, out);
1208 }
1209 }
1210 "state_variable_declaration" | "variable_declaration" => {
1211 if let Some(id_name) = ts_child_id_text(child, source)
1212 && id_name == name
1213 {
1214 out.push(TsDeclaration {
1215 range: id_range(child),
1216 kind: child.kind(),
1217 container: container.map(String::from),
1218 param_count: None,
1219 param_types: vec![],
1220 });
1221 }
1222 }
1223 "struct_declaration" => {
1224 if let Some(id_name) = ts_child_id_text(child, source) {
1225 if id_name == name {
1226 out.push(TsDeclaration {
1227 range: id_range(child),
1228 kind: "struct_declaration",
1229 container: container.map(String::from),
1230 param_count: None,
1231 param_types: vec![],
1232 });
1233 }
1234 if let Some(body) = ts_find_child(child, "struct_body") {
1235 collect_declarations(body, source, name, Some(id_name), out);
1236 }
1237 }
1238 }
1239 "enum_declaration" => {
1240 if let Some(id_name) = ts_child_id_text(child, source) {
1241 if id_name == name {
1242 out.push(TsDeclaration {
1243 range: id_range(child),
1244 kind: "enum_declaration",
1245 container: container.map(String::from),
1246 param_count: None,
1247 param_types: vec![],
1248 });
1249 }
1250 if let Some(body) = ts_find_child(child, "enum_body") {
1252 let mut ecur = body.walk();
1253 for val in body.children(&mut ecur) {
1254 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1255 out.push(TsDeclaration {
1256 range: ts_range(val),
1257 kind: "enum_value",
1258 container: Some(id_name.to_string()),
1259 param_count: None,
1260 param_types: vec![],
1261 });
1262 }
1263 }
1264 }
1265 }
1266 }
1267 "event_definition" | "error_declaration" => {
1268 if let Some(id_name) = ts_child_id_text(child, source)
1269 && id_name == name
1270 {
1271 out.push(TsDeclaration {
1272 range: id_range(child),
1273 kind: child.kind(),
1274 container: container.map(String::from),
1275 param_count: None,
1276 param_types: vec![],
1277 });
1278 }
1279 }
1280 "user_defined_type_definition" => {
1281 if let Some(id_name) = ts_child_id_text(child, source)
1282 && id_name == name
1283 {
1284 out.push(TsDeclaration {
1285 range: id_range(child),
1286 kind: "user_defined_type_definition",
1287 container: container.map(String::from),
1288 param_count: None,
1289 param_types: vec![],
1290 });
1291 }
1292 }
1293 _ => {
1295 collect_declarations(child, source, name, container, out);
1296 }
1297 }
1298 }
1299}
1300
1301fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1307 let mut cursor = node.walk();
1308 node.children(&mut cursor)
1309 .filter(|c| c.kind() == "parameter")
1310 .filter_map(|param| {
1311 let mut pc = param.walk();
1312 param
1313 .children(&mut pc)
1314 .find(|c| {
1315 matches!(
1316 c.kind(),
1317 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1318 )
1319 })
1320 .map(|t| source[t.byte_range()].trim())
1321 })
1322 .collect()
1323}
1324
1325fn collect_parameters(
1327 node: Node,
1328 source: &str,
1329 name: &str,
1330 container: Option<&str>,
1331 out: &mut Vec<TsDeclaration>,
1332) {
1333 let mut cursor = node.walk();
1334 for child in node.children(&mut cursor) {
1335 if child.kind() == "parameter"
1336 && let Some(id_name) = ts_child_id_text(child, source)
1337 && id_name == name
1338 {
1339 out.push(TsDeclaration {
1340 range: id_range(child),
1341 kind: "parameter",
1342 container: container.map(String::from),
1343 param_count: None,
1344 param_types: vec![],
1345 });
1346 }
1347 }
1348}
1349
1350fn ts_range(node: Node) -> Range {
1352 let s = node.start_position();
1353 let e = node.end_position();
1354 Range {
1355 start: Position::new(s.row as u32, s.column as u32),
1356 end: Position::new(e.row as u32, e.column as u32),
1357 }
1358}
1359
1360fn id_range(node: Node) -> Range {
1362 let mut cursor = node.walk();
1363 node.children(&mut cursor)
1364 .find(|c| c.kind() == "identifier" && c.is_named())
1365 .map(|c| ts_range(c))
1366 .unwrap_or_else(|| ts_range(node))
1367}
1368
1369fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1370 let mut cursor = node.walk();
1371 node.children(&mut cursor).find(|c| c.kind() == kind)
1372}
1373
1374pub fn goto_definition_ts(
1382 source: &str,
1383 position: Position,
1384 file_uri: &Url,
1385 completion_cache: &crate::completion::CompletionCache,
1386 text_cache: &HashMap<String, (i32, String)>,
1387) -> Option<Location> {
1388 let ctx = cursor_context(source, position)?;
1389
1390 if let Some(obj_name) = &ctx.object {
1395 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1396 let target_source = read_target_source(&path, text_cache)?;
1397 let target_uri = Url::from_file_path(&path).ok()?;
1398 let decls = find_declarations_by_name(&target_source, &ctx.name);
1399 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1400 return Some(Location {
1401 uri: target_uri,
1402 range: d.range,
1403 });
1404 }
1405 }
1406 let decls = find_declarations_by_name(source, &ctx.name);
1408 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1409 return Some(Location {
1410 uri: file_uri.clone(),
1411 range: d.range,
1412 });
1413 }
1414 }
1415
1416 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1419
1420 match resolved {
1421 Some(ResolvedTarget::SameFile) => {
1422 find_best_declaration(source, &ctx, file_uri)
1424 }
1425 Some(ResolvedTarget::OtherFile { path, name }) => {
1426 let target_source = read_target_source(&path, text_cache);
1428 let target_source = target_source?;
1429 let target_uri = Url::from_file_path(&path).ok()?;
1430 let decls = find_declarations_by_name(&target_source, &name);
1431 decls.first().map(|d| Location {
1432 uri: target_uri,
1433 range: d.range,
1434 })
1435 }
1436 None => {
1437 find_best_declaration(source, &ctx, file_uri)
1439 }
1440 }
1441}
1442
1443#[derive(Debug)]
1444enum ResolvedTarget {
1445 SameFile,
1447 OtherFile { path: String, name: String },
1449}
1450
1451fn resolve_via_cache(
1457 ctx: &CursorContext,
1458 file_uri: &Url,
1459 cache: &crate::completion::CompletionCache,
1460) -> Option<ResolvedTarget> {
1461 let contract_scope = ctx
1463 .contract
1464 .as_ref()
1465 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1466 .copied();
1467
1468 if let Some(contract_id) = contract_scope {
1470 if let Some(func_name) = &ctx.function {
1472 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1475 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1477 && decls.iter().any(|d| d.name == ctx.name)
1478 {
1479 return Some(ResolvedTarget::SameFile);
1480 }
1481 }
1482 }
1483
1484 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1486 && decls.iter().any(|d| d.name == ctx.name)
1487 {
1488 return Some(ResolvedTarget::SameFile);
1489 }
1490
1491 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1493 for &base_id in bases.iter().skip(1) {
1494 if let Some(decls) = cache.scope_declarations.get(&base_id)
1495 && decls.iter().any(|d| d.name == ctx.name)
1496 {
1497 let base_name = cache
1500 .name_to_node_id
1501 .iter()
1502 .find(|&(_, &id)| id == base_id)
1503 .map(|(name, _)| name.clone());
1504
1505 if let Some(base_name) = base_name
1506 && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1507 {
1508 return Some(ResolvedTarget::OtherFile {
1509 path,
1510 name: ctx.name.clone(),
1511 });
1512 }
1513 return Some(ResolvedTarget::SameFile);
1515 }
1516 }
1517 }
1518 }
1519
1520 if cache.name_to_node_id.contains_key(&ctx.name) {
1522 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1524 let current_path = file_uri.to_file_path().ok()?;
1525 let current_str = current_path.to_str()?;
1526 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1527 return Some(ResolvedTarget::SameFile);
1528 }
1529 return Some(ResolvedTarget::OtherFile {
1530 path,
1531 name: ctx.name.clone(),
1532 });
1533 }
1534 return Some(ResolvedTarget::SameFile);
1535 }
1536
1537 if cache.name_to_type.contains_key(&ctx.name) {
1539 return Some(ResolvedTarget::SameFile);
1540 }
1541
1542 None
1543}
1544
1545fn find_function_scope(
1547 cache: &crate::completion::CompletionCache,
1548 contract_id: NodeId,
1549 func_name: &str,
1550) -> Option<NodeId> {
1551 for (&scope_id, &parent_id) in &cache.scope_parent {
1555 if parent_id == contract_id {
1556 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1560 && contract_decls.iter().any(|d| d.name == func_name)
1561 {
1562 if cache.scope_declarations.contains_key(&scope_id)
1566 || cache.scope_parent.values().any(|&p| p == scope_id)
1567 {
1568 return Some(scope_id);
1569 }
1570 }
1571 }
1572 }
1573 None
1574}
1575
1576fn find_file_for_contract(
1578 cache: &crate::completion::CompletionCache,
1579 contract_name: &str,
1580 _file_uri: &Url,
1581) -> Option<String> {
1582 let node_id = cache.name_to_node_id.get(contract_name)?;
1587 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1588 let file_id = scope_range.file_id;
1589
1590 cache
1592 .path_to_file_id
1593 .iter()
1594 .find(|&(_, &fid)| fid == file_id)
1595 .map(|(path, _)| path.clone())
1596}
1597
1598fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1600 let uri = Url::from_file_path(path).ok()?;
1602 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1603 return Some(content.clone());
1604 }
1605 std::fs::read_to_string(path).ok()
1607}
1608
1609fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1611 let decls = find_declarations_by_name(source, &ctx.name);
1612 if decls.is_empty() {
1613 return None;
1614 }
1615
1616 if decls.len() == 1 {
1618 return Some(Location {
1619 uri: file_uri.clone(),
1620 range: decls[0].range,
1621 });
1622 }
1623
1624 if let Some(contract_name) = &ctx.contract
1626 && let Some(d) = decls
1627 .iter()
1628 .find(|d| d.container.as_deref() == Some(contract_name))
1629 {
1630 return Some(Location {
1631 uri: file_uri.clone(),
1632 range: d.range,
1633 });
1634 }
1635
1636 Some(Location {
1638 uri: file_uri.clone(),
1639 range: decls[0].range,
1640 })
1641}
1642
1643#[cfg(test)]
1644mod ts_tests {
1645 use super::*;
1646
1647 #[test]
1648 fn test_cursor_context_state_var() {
1649 let source = r#"
1650contract Token {
1651 uint256 public totalSupply;
1652 function mint(uint256 amount) public {
1653 totalSupply += amount;
1654 }
1655}
1656"#;
1657 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
1659 assert_eq!(ctx.name, "totalSupply");
1660 assert_eq!(ctx.function.as_deref(), Some("mint"));
1661 assert_eq!(ctx.contract.as_deref(), Some("Token"));
1662 }
1663
1664 #[test]
1665 fn test_cursor_context_top_level() {
1666 let source = r#"
1667contract Foo {}
1668contract Bar {}
1669"#;
1670 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
1672 assert_eq!(ctx.name, "Foo");
1673 assert!(ctx.function.is_none());
1674 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
1676 }
1677
1678 #[test]
1679 fn test_find_declarations() {
1680 let source = r#"
1681contract Token {
1682 uint256 public totalSupply;
1683 function mint(uint256 amount) public {
1684 totalSupply += amount;
1685 }
1686}
1687"#;
1688 let decls = find_declarations_by_name(source, "totalSupply");
1689 assert_eq!(decls.len(), 1);
1690 assert_eq!(decls[0].kind, "state_variable_declaration");
1691 assert_eq!(decls[0].container.as_deref(), Some("Token"));
1692 }
1693
1694 #[test]
1695 fn test_find_declarations_multiple_contracts() {
1696 let source = r#"
1697contract A {
1698 uint256 public value;
1699}
1700contract B {
1701 uint256 public value;
1702}
1703"#;
1704 let decls = find_declarations_by_name(source, "value");
1705 assert_eq!(decls.len(), 2);
1706 assert_eq!(decls[0].container.as_deref(), Some("A"));
1707 assert_eq!(decls[1].container.as_deref(), Some("B"));
1708 }
1709
1710 #[test]
1711 fn test_find_declarations_enum_value() {
1712 let source = "contract Foo { enum Status { Active, Paused } }";
1713 let decls = find_declarations_by_name(source, "Active");
1714 assert_eq!(decls.len(), 1);
1715 assert_eq!(decls[0].kind, "enum_value");
1716 assert_eq!(decls[0].container.as_deref(), Some("Status"));
1717 }
1718
1719 #[test]
1720 fn test_cursor_context_short_param() {
1721 let source = r#"
1722contract Shop {
1723 uint256 public TAX;
1724 constructor(uint256 price, uint16 tax, uint16 taxBase) {
1725 TAX = tax;
1726 }
1727}
1728"#;
1729 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
1731 assert_eq!(ctx.name, "tax");
1732 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
1733
1734 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
1736 assert_eq!(ctx2.name, "TAX");
1737
1738 let decls = find_declarations_by_name(source, "tax");
1740 assert_eq!(decls.len(), 1);
1741 assert_eq!(decls[0].kind, "parameter");
1742
1743 let decls_tax_base = find_declarations_by_name(source, "taxBase");
1744 assert_eq!(decls_tax_base.len(), 1);
1745 assert_eq!(decls_tax_base[0].kind, "parameter");
1746
1747 let decls_price = find_declarations_by_name(source, "price");
1748 assert_eq!(decls_price.len(), 1);
1749 assert_eq!(decls_price[0].kind, "parameter");
1750
1751 let decls_tax_upper = find_declarations_by_name(source, "TAX");
1753 assert_eq!(decls_tax_upper.len(), 1);
1754 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
1755 }
1756
1757 #[test]
1758 fn test_find_best_declaration_same_contract() {
1759 let source = r#"
1760contract A { uint256 public x; }
1761contract B { uint256 public x; }
1762"#;
1763 let ctx = CursorContext {
1764 name: "x".into(),
1765 function: None,
1766 contract: Some("B".into()),
1767 object: None,
1768 arg_count: None,
1769 arg_types: vec![],
1770 };
1771 let uri = Url::parse("file:///test.sol").unwrap();
1772 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
1773 assert_eq!(loc.range.start.line, 2);
1775 }
1776}