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};
7use crate::utils::push_if_node_or_array;
8
9#[derive(Debug, Clone)]
10pub struct NodeInfo {
11 pub src: String,
12 pub name_location: Option<String>,
13 pub name_locations: Vec<String>,
14 pub referenced_declaration: Option<NodeId>,
15 pub node_type: Option<String>,
16 pub member_location: Option<String>,
17 pub absolute_path: Option<String>,
18}
19
20pub const CHILD_KEYS: &[&str] = &[
22 "AST",
23 "arguments",
24 "baseContracts",
25 "baseExpression",
26 "baseName",
27 "baseType",
28 "block",
29 "body",
30 "components",
31 "condition",
32 "declarations",
33 "endExpression",
34 "errorCall",
35 "eventCall",
36 "expression",
37 "externalCall",
38 "falseBody",
39 "falseExpression",
40 "file",
41 "foreign",
42 "functionName",
43 "indexExpression",
44 "initialValue",
45 "initializationExpression",
46 "keyType",
47 "leftExpression",
48 "leftHandSide",
49 "libraryName",
50 "literals",
51 "loopExpression",
52 "members",
53 "modifierName",
54 "modifiers",
55 "name",
56 "names",
57 "nodes",
58 "options",
59 "overrides",
60 "parameters",
61 "pathNode",
62 "post",
63 "pre",
64 "returnParameters",
65 "rightExpression",
66 "rightHandSide",
67 "startExpression",
68 "statements",
69 "storageLayout",
70 "subExpression",
71 "subdenomination",
72 "symbolAliases",
73 "trueBody",
74 "trueExpression",
75 "typeName",
76 "unitAlias",
77 "value",
78 "valueType",
79 "variableNames",
80 "variables",
81];
82
83pub type ExternalRefs = HashMap<String, NodeId>;
86
87#[derive(Debug, Clone)]
93pub struct CachedBuild {
94 pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
95 pub path_to_abs: HashMap<String, String>,
96 pub external_refs: ExternalRefs,
97 pub id_to_path_map: HashMap<String, String>,
98 pub decl_index: HashMap<i64, crate::solc_ast::DeclNode>,
102 pub node_id_to_source_path: HashMap<i64, String>,
106 pub gas_index: crate::gas::GasIndex,
109 pub hint_index: crate::inlay_hints::HintIndex,
112 pub doc_index: crate::hover::DocIndex,
115 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
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 doc_index = crate::hover::build_doc_index(&ast);
150
151 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
158 match crate::solc_ast::extract_decl_nodes(sources) {
159 Some(extracted) => (extracted.decl_index, extracted.node_id_to_source_path),
160 None => (HashMap::new(), HashMap::new()),
161 }
162 } else {
163 (HashMap::new(), HashMap::new())
164 };
165
166 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
168 let hint_index = if let Some(sources) = ast.get("sources") {
169 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
170 } else {
171 HashMap::new()
172 };
173
174 let completion_cache = {
176 let sources = ast.get("sources");
177 let contracts = ast.get("contracts");
178 let cc = if let Some(s) = sources {
179 crate::completion::build_completion_cache(s, contracts)
180 } else {
181 crate::completion::build_completion_cache(
182 &serde_json::Value::Object(Default::default()),
183 contracts,
184 )
185 };
186 std::sync::Arc::new(cc)
187 };
188
189 Self {
193 nodes,
194 path_to_abs,
195 external_refs,
196 id_to_path_map,
197 decl_index,
198 node_id_to_source_path,
199 gas_index,
200 hint_index,
201 doc_index,
202 completion_cache,
203 build_version,
204 }
205 }
206}
207
208type CachedIds = (
210 HashMap<String, HashMap<NodeId, NodeInfo>>,
211 HashMap<String, String>,
212 ExternalRefs,
213);
214
215pub fn cache_ids(sources: &Value) -> CachedIds {
216 let source_count = sources.as_object().map_or(0, |obj| obj.len());
217
218 let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> =
221 HashMap::with_capacity(source_count);
222 let mut path_to_abs: HashMap<String, String> = HashMap::with_capacity(source_count);
223 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
224
225 if let Some(sources_obj) = sources.as_object() {
226 for (path, source_data) in sources_obj {
227 if let Some(ast) = source_data.get("ast") {
228 let abs_path = ast
230 .get("absolutePath")
231 .and_then(|v| v.as_str())
232 .unwrap_or(path)
233 .to_string();
234
235 path_to_abs.insert(path.clone(), abs_path.clone());
236
237 let size_hint = ast
242 .get("nodes")
243 .and_then(|v| v.as_array())
244 .map_or(64, |arr| arr.len() * 8);
245 if !nodes.contains_key(&abs_path) {
246 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
247 }
248
249 if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
250 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
251 {
252 nodes.get_mut(&abs_path).unwrap().insert(
253 NodeId(id),
254 NodeInfo {
255 src: src.to_string(),
256 name_location: None,
257 name_locations: vec![],
258 referenced_declaration: None,
259 node_type: ast
260 .get("nodeType")
261 .and_then(|v| v.as_str())
262 .map(|s| s.to_string()),
263 member_location: None,
264 absolute_path: ast
265 .get("absolutePath")
266 .and_then(|v| v.as_str())
267 .map(|s| s.to_string()),
268 },
269 );
270 }
271
272 let mut stack = vec![ast];
273
274 while let Some(tree) = stack.pop() {
275 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
276 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
277 {
278 let id = NodeId(raw_id);
279 let mut name_location = tree
281 .get("nameLocation")
282 .and_then(|v| v.as_str())
283 .map(|s| s.to_string());
284
285 if name_location.is_none()
289 && let Some(name_locations) = tree.get("nameLocations")
290 && let Some(locations_array) = name_locations.as_array()
291 && !locations_array.is_empty()
292 {
293 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
294 if node_type == Some("IdentifierPath") {
295 name_location = locations_array
296 .last()
297 .and_then(|v| v.as_str())
298 .map(|s| s.to_string());
299 } else {
300 name_location = locations_array[0].as_str().map(|s| s.to_string());
301 }
302 }
303
304 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
305 && let Some(locations_array) = name_locations.as_array()
306 {
307 locations_array
308 .iter()
309 .filter_map(|v| v.as_str().map(|s| s.to_string()))
310 .collect()
311 } else {
312 vec![]
313 };
314
315 let mut final_name_location = name_location;
316 if final_name_location.is_none()
317 && let Some(member_loc) =
318 tree.get("memberLocation").and_then(|v| v.as_str())
319 {
320 final_name_location = Some(member_loc.to_string());
321 }
322
323 let node_info = NodeInfo {
324 src: src.to_string(),
325 name_location: final_name_location,
326 name_locations,
327 referenced_declaration: tree
328 .get("referencedDeclaration")
329 .and_then(|v| v.as_u64())
330 .map(NodeId),
331 node_type: tree
332 .get("nodeType")
333 .and_then(|v| v.as_str())
334 .map(|s| s.to_string()),
335 member_location: tree
336 .get("memberLocation")
337 .and_then(|v| v.as_str())
338 .map(|s| s.to_string()),
339 absolute_path: tree
340 .get("absolutePath")
341 .and_then(|v| v.as_str())
342 .map(|s| s.to_string()),
343 };
344
345 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
346
347 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
349 && let Some(ext_refs) =
350 tree.get("externalReferences").and_then(|v| v.as_array())
351 {
352 for ext_ref in ext_refs {
353 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
354 && let Some(decl_id) =
355 ext_ref.get("declaration").and_then(|v| v.as_u64())
356 {
357 external_refs.insert(src_str.to_string(), NodeId(decl_id));
358 }
359 }
360 }
361 }
362
363 for key in CHILD_KEYS {
364 push_if_node_or_array(tree, key, &mut stack);
365 }
366 }
367 }
368 }
369 }
370
371 (nodes, path_to_abs, external_refs)
372}
373
374pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
375 let text = String::from_utf8_lossy(source_bytes);
376 crate::utils::position_to_byte_offset(&text, position)
377}
378
379pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
380 let text = String::from_utf8_lossy(source_bytes);
381 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
382 Some(pos)
383}
384
385pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
387 let loc = SourceLoc::parse(src)?;
388 let file_path = id_to_path.get(&loc.file_id_str())?;
389
390 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
391 std::path::PathBuf::from(file_path)
392 } else {
393 std::env::current_dir().ok()?.join(file_path)
394 };
395
396 let source_bytes = std::fs::read(&absolute_path).ok()?;
397 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
398 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
399 let uri = Url::from_file_path(&absolute_path).ok()?;
400
401 Some(Location {
402 uri,
403 range: Range {
404 start: start_pos,
405 end: end_pos,
406 },
407 })
408}
409
410pub fn goto_bytes(
411 nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
412 path_to_abs: &HashMap<String, String>,
413 id_to_path: &HashMap<String, String>,
414 external_refs: &ExternalRefs,
415 uri: &str,
416 position: usize,
417) -> Option<(String, usize, usize)> {
418 let path = match uri.starts_with("file://") {
419 true => &uri[7..],
420 false => uri,
421 };
422
423 let abs_path = path_to_abs.get(path)?;
425
426 let current_file_nodes = nodes.get(abs_path)?;
428
429 let path_to_file_id: HashMap<&str, &str> = id_to_path
431 .iter()
432 .map(|(id, p)| (p.as_str(), id.as_str()))
433 .collect();
434
435 let current_file_id = path_to_file_id.get(abs_path.as_str());
439
440 for (src_str, decl_id) in external_refs {
442 let Some(src_loc) = SourceLoc::parse(src_str) else {
443 continue;
444 };
445
446 if let Some(file_id) = current_file_id {
448 if src_loc.file_id_str() != *file_id {
449 continue;
450 }
451 } else {
452 continue;
453 }
454
455 if src_loc.offset <= position && position < src_loc.end() {
456 let mut target_node: Option<&NodeInfo> = None;
458 for file_nodes in nodes.values() {
459 if let Some(node) = file_nodes.get(decl_id) {
460 target_node = Some(node);
461 break;
462 }
463 }
464 let node = target_node?;
465 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
466 let loc = SourceLoc::parse(loc_str)?;
467 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
468 return Some((file_path, loc.offset, loc.length));
469 }
470 }
471
472 let mut refs = HashMap::new();
473
474 for (id, content) in current_file_nodes {
476 if content.referenced_declaration.is_none() {
477 continue;
478 }
479
480 let Some(src_loc) = SourceLoc::parse(&content.src) else {
481 continue;
482 };
483
484 if src_loc.offset <= position && position < src_loc.end() {
485 let diff = src_loc.length;
486 if !refs.contains_key(&diff) || refs[&diff] <= *id {
487 refs.insert(diff, *id);
488 }
489 }
490 }
491
492 if refs.is_empty() {
493 let tmp = current_file_nodes.iter();
496 for (_id, content) in tmp {
497 if content.node_type == Some("ImportDirective".to_string()) {
498 let Some(src_loc) = SourceLoc::parse(&content.src) else {
499 continue;
500 };
501
502 if src_loc.offset <= position
503 && position < src_loc.end()
504 && let Some(import_path) = &content.absolute_path
505 {
506 return Some((import_path.clone(), 0, 0));
507 }
508 }
509 }
510 return None;
511 }
512
513 let min_diff = *refs.keys().min()?;
515 let chosen_id = refs[&min_diff];
516 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
517
518 let mut target_node: Option<&NodeInfo> = None;
520 for file_nodes in nodes.values() {
521 if let Some(node) = file_nodes.get(&ref_id) {
522 target_node = Some(node);
523 break;
524 }
525 }
526
527 let node = target_node?;
528
529 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
531 let loc = SourceLoc::parse(loc_str)?;
532 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
533
534 Some((file_path, loc.offset, loc.length))
535}
536
537pub fn goto_declaration_cached(
540 build: &CachedBuild,
541 file_uri: &Url,
542 position: Position,
543 source_bytes: &[u8],
544) -> Option<Location> {
545 let byte_position = pos_to_bytes(source_bytes, position);
546
547 if let Some((file_path, location_bytes, length)) = goto_bytes(
548 &build.nodes,
549 &build.path_to_abs,
550 &build.id_to_path_map,
551 &build.external_refs,
552 file_uri.as_ref(),
553 byte_position,
554 ) {
555 let target_file_path = std::path::Path::new(&file_path);
556 let absolute_path = if target_file_path.is_absolute() {
557 target_file_path.to_path_buf()
558 } else {
559 let base = file_uri
564 .to_file_path()
565 .ok()
566 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
567 .or_else(|| std::env::current_dir().ok())
568 .unwrap_or_default();
569 base.join(target_file_path)
570 };
571
572 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
573 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
574 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
575 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
576 {
577 return Some(Location {
578 uri: target_uri,
579 range: Range {
580 start: start_pos,
581 end: end_pos,
582 },
583 });
584 }
585 };
586
587 None
588}
589
590pub fn goto_declaration_by_name(
601 cached_build: &CachedBuild,
602 file_uri: &Url,
603 name: &str,
604 byte_hint: usize,
605) -> Option<Location> {
606 let path = match file_uri.as_ref().starts_with("file://") {
607 true => &file_uri.as_ref()[7..],
608 false => file_uri.as_ref(),
609 };
610 let abs_path = cached_build.path_to_abs.get(path)?;
611 let built_source = std::fs::read_to_string(abs_path).ok()?;
613
614 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
616
617 let tmp = {
618 let this = cached_build.nodes.get(abs_path)?;
619 this.iter()
620 };
621 for (_id, node) in tmp {
622 let ref_id = match node.referenced_declaration {
623 Some(id) => id,
624 None => continue,
625 };
626
627 let Some(src_loc) = SourceLoc::parse(&node.src) else {
629 continue;
630 };
631 let start = src_loc.offset;
632 let length = src_loc.length;
633
634 if start + length > built_source.len() {
635 continue;
636 }
637
638 let node_text = &built_source[start..start + length];
639
640 let matches = node_text == name
645 || node_text.contains(&format!(".{name}("))
646 || node_text.ends_with(&format!(".{name}"));
647
648 if matches {
649 let distance = if byte_hint >= start && byte_hint < start + length {
653 0 } else if byte_hint < start {
655 start - byte_hint
656 } else {
657 byte_hint - (start + length)
658 };
659 candidates.push((distance, length, ref_id));
660 }
661 }
662
663 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
665 let ref_id = candidates.first()?.2;
666
667 let mut target_node: Option<&NodeInfo> = None;
669 for file_nodes in cached_build.nodes.values() {
670 if let Some(node) = file_nodes.get(&ref_id) {
671 target_node = Some(node);
672 break;
673 }
674 }
675
676 let node = target_node?;
677
678 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
680 let loc = SourceLoc::parse(loc_str)?;
681
682 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
683 let location_bytes = loc.offset;
684 let length = loc.length;
685
686 let target_file_path = std::path::Path::new(file_path);
687 let absolute_path = if target_file_path.is_absolute() {
688 target_file_path.to_path_buf()
689 } else {
690 let base = file_uri
691 .to_file_path()
692 .ok()
693 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
694 .or_else(|| std::env::current_dir().ok())
695 .unwrap_or_default();
696 base.join(target_file_path)
697 };
698
699 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
700 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
701 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
702 let target_uri = Url::from_file_path(&absolute_path).ok()?;
703
704 Some(Location {
705 uri: target_uri,
706 range: Range {
707 start: start_pos,
708 end: end_pos,
709 },
710 })
711}
712
713#[derive(Debug, Clone)]
717pub struct CursorContext {
718 pub name: String,
720 pub function: Option<String>,
722 pub contract: Option<String>,
724 pub object: Option<String>,
728 pub arg_count: Option<usize>,
731 pub arg_types: Vec<Option<String>>,
734}
735
736fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
738 let mut parser = Parser::new();
739 parser
740 .set_language(&tree_sitter_solidity::LANGUAGE.into())
741 .expect("failed to load Solidity grammar");
742 parser.parse(source, None)
743}
744
745pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
751 let line = location.range.start.line as usize;
752 let start_col = location.range.start.character as usize;
753 let end_col = location.range.end.character as usize;
754
755 if let Some(line_text) = target_source.lines().nth(line)
756 && end_col <= line_text.len()
757 {
758 return &line_text[start_col..end_col] == expected_name;
759 }
760 true
762}
763
764fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
766 if byte < node.start_byte() || byte >= node.end_byte() {
767 return None;
768 }
769 let mut cursor = node.walk();
770 for child in node.children(&mut cursor) {
771 if child.start_byte() <= byte
772 && byte < child.end_byte()
773 && let Some(deeper) = ts_node_at_byte(child, byte)
774 {
775 return Some(deeper);
776 }
777 }
778 Some(node)
779}
780
781fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
783 let mut cursor = node.walk();
784 node.children(&mut cursor)
785 .find(|c| c.kind() == "identifier" && c.is_named())
786 .map(|c| &source[c.byte_range()])
787}
788
789fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
795 let expr = if arg_node.kind() == "call_argument" {
797 let mut c = arg_node.walk();
798 arg_node.children(&mut c).find(|ch| ch.is_named())?
799 } else {
800 arg_node
801 };
802
803 match expr.kind() {
804 "identifier" => {
805 let var_name = &source[expr.byte_range()];
806 find_variable_type(expr, source, var_name)
808 }
809 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
810 "boolean_literal" => Some("bool".into()),
811 "string_literal" | "hex_string_literal" => Some("string".into()),
812 _ => None,
813 }
814}
815
816fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
821 let mut scope = from.parent();
822 while let Some(node) = scope {
823 match node.kind() {
824 "function_definition" | "modifier_definition" | "constructor_definition" => {
825 let mut c = node.walk();
827 for child in node.children(&mut c) {
828 if child.kind() == "parameter"
829 && let Some(id) = ts_child_id_text(child, source)
830 && id == var_name
831 {
832 let mut pc = child.walk();
834 return child
835 .children(&mut pc)
836 .find(|c| {
837 matches!(
838 c.kind(),
839 "type_name"
840 | "primitive_type"
841 | "user_defined_type"
842 | "mapping"
843 )
844 })
845 .map(|t| source[t.byte_range()].trim().to_string());
846 }
847 }
848 }
849 "function_body" | "block_statement" | "unchecked_block" => {
850 let mut c = node.walk();
852 for child in node.children(&mut c) {
853 if (child.kind() == "variable_declaration_statement"
854 || child.kind() == "variable_declaration")
855 && let Some(id) = ts_child_id_text(child, source)
856 && id == var_name
857 {
858 let mut pc = child.walk();
859 return child
860 .children(&mut pc)
861 .find(|c| {
862 matches!(
863 c.kind(),
864 "type_name"
865 | "primitive_type"
866 | "user_defined_type"
867 | "mapping"
868 )
869 })
870 .map(|t| source[t.byte_range()].trim().to_string());
871 }
872 }
873 }
874 "contract_declaration" | "library_declaration" | "interface_declaration" => {
875 if let Some(body) = ts_find_child(node, "contract_body") {
877 let mut c = body.walk();
878 for child in body.children(&mut c) {
879 if child.kind() == "state_variable_declaration"
880 && let Some(id) = ts_child_id_text(child, source)
881 && id == var_name
882 {
883 let mut pc = child.walk();
884 return child
885 .children(&mut pc)
886 .find(|c| {
887 matches!(
888 c.kind(),
889 "type_name"
890 | "primitive_type"
891 | "user_defined_type"
892 | "mapping"
893 )
894 })
895 .map(|t| source[t.byte_range()].trim().to_string());
896 }
897 }
898 }
899 }
900 _ => {}
901 }
902 scope = node.parent();
903 }
904 None
905}
906
907fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
909 let mut cursor = call_node.walk();
910 call_node
911 .children(&mut cursor)
912 .filter(|c| c.kind() == "call_argument")
913 .map(|arg| infer_argument_type(arg, source))
914 .collect()
915}
916
917fn best_overload<'a>(
925 decls: &'a [TsDeclaration],
926 arg_count: Option<usize>,
927 arg_types: &[Option<String>],
928) -> Option<&'a TsDeclaration> {
929 if decls.len() == 1 {
930 return decls.first();
931 }
932 if decls.is_empty() {
933 return None;
934 }
935
936 let func_decls: Vec<&TsDeclaration> =
938 decls.iter().filter(|d| d.param_count.is_some()).collect();
939
940 if func_decls.is_empty() {
941 return decls.first();
942 }
943
944 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
946 let matched: Vec<_> = func_decls
947 .iter()
948 .filter(|d| d.param_count == Some(ac))
949 .collect();
950 if matched.len() == 1 {
951 return Some(matched[0]);
952 }
953 if matched.is_empty() {
954 func_decls.iter().collect()
956 } else {
957 matched
958 }
959 } else {
960 func_decls.iter().collect()
961 };
962
963 if !arg_types.is_empty() {
965 let mut best: Option<(&TsDeclaration, usize)> = None;
966 for &&decl in &count_matched {
967 let score = arg_types
968 .iter()
969 .zip(decl.param_types.iter())
970 .filter(|(arg_ty, param_ty)| {
971 if let Some(at) = arg_ty {
972 at == param_ty.as_str()
973 } else {
974 false
975 }
976 })
977 .count();
978 if best.is_none() || score > best.unwrap().1 {
979 best = Some((decl, score));
980 }
981 }
982 if let Some((decl, _)) = best {
983 return Some(decl);
984 }
985 }
986
987 count_matched.first().map(|d| **d).or(decls.first())
989}
990
991pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
995 let tree = ts_parse(source)?;
996 let byte = pos_to_bytes(source.as_bytes(), position);
997 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
998
999 let id_node = if leaf.kind() == "identifier" {
1001 leaf
1002 } else {
1003 let parent = leaf.parent()?;
1005 if parent.kind() == "identifier" {
1006 parent
1007 } else {
1008 return None;
1009 }
1010 };
1011
1012 let name = source[id_node.byte_range()].to_string();
1013 let mut function = None;
1014 let mut contract = None;
1015
1016 let object = id_node.parent().and_then(|parent| {
1020 if parent.kind() == "member_expression" {
1021 let prop = parent.child_by_field_name("property")?;
1022 if prop.id() == id_node.id() {
1024 let obj = parent.child_by_field_name("object")?;
1025 Some(source[obj.byte_range()].to_string())
1026 } else {
1027 None
1028 }
1029 } else {
1030 None
1031 }
1032 });
1033
1034 let (arg_count, arg_types) = {
1038 let mut node = id_node.parent();
1039 let mut result = (None, vec![]);
1040 while let Some(n) = node {
1041 if n.kind() == "call_expression" {
1042 let types = infer_call_arg_types(n, source);
1043 result = (Some(types.len()), types);
1044 break;
1045 }
1046 node = n.parent();
1047 }
1048 result
1049 };
1050
1051 let mut current = id_node.parent();
1053 while let Some(node) = current {
1054 match node.kind() {
1055 "function_definition" | "modifier_definition" if function.is_none() => {
1056 function = ts_child_id_text(node, source).map(String::from);
1057 }
1058 "constructor_definition" if function.is_none() => {
1059 function = Some("constructor".into());
1060 }
1061 "contract_declaration" | "interface_declaration" | "library_declaration"
1062 if contract.is_none() =>
1063 {
1064 contract = ts_child_id_text(node, source).map(String::from);
1065 }
1066 _ => {}
1067 }
1068 current = node.parent();
1069 }
1070
1071 Some(CursorContext {
1072 name,
1073 function,
1074 contract,
1075 object,
1076 arg_count,
1077 arg_types,
1078 })
1079}
1080
1081#[derive(Debug, Clone)]
1083pub struct TsDeclaration {
1084 pub range: Range,
1086 pub kind: &'static str,
1088 pub container: Option<String>,
1090 pub param_count: Option<usize>,
1092 pub param_types: Vec<String>,
1095}
1096
1097pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1102 let tree = match ts_parse(source) {
1103 Some(t) => t,
1104 None => return vec![],
1105 };
1106 let mut results = Vec::new();
1107 collect_declarations(tree.root_node(), source, name, None, &mut results);
1108 results
1109}
1110
1111fn collect_declarations(
1112 node: Node,
1113 source: &str,
1114 name: &str,
1115 container: Option<&str>,
1116 out: &mut Vec<TsDeclaration>,
1117) {
1118 let mut cursor = node.walk();
1119 for child in node.children(&mut cursor) {
1120 if !child.is_named() {
1121 continue;
1122 }
1123 match child.kind() {
1124 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1125 if let Some(id_name) = ts_child_id_text(child, source) {
1126 if id_name == name {
1127 out.push(TsDeclaration {
1128 range: id_range(child),
1129 kind: child.kind(),
1130 container: container.map(String::from),
1131 param_count: None,
1132 param_types: vec![],
1133 });
1134 }
1135 if let Some(body) = ts_find_child(child, "contract_body") {
1137 collect_declarations(body, source, name, Some(id_name), out);
1138 }
1139 }
1140 }
1141 "function_definition" | "modifier_definition" => {
1142 if let Some(id_name) = ts_child_id_text(child, source) {
1143 if id_name == name {
1144 let types = parameter_type_signature(child, source);
1145 out.push(TsDeclaration {
1146 range: id_range(child),
1147 kind: child.kind(),
1148 container: container.map(String::from),
1149 param_count: Some(types.len()),
1150 param_types: types.into_iter().map(String::from).collect(),
1151 });
1152 }
1153 collect_parameters(child, source, name, container, out);
1155 if let Some(body) = ts_find_child(child, "function_body") {
1157 collect_declarations(body, source, name, container, out);
1158 }
1159 }
1160 }
1161 "constructor_definition" => {
1162 if name == "constructor" {
1163 let types = parameter_type_signature(child, source);
1164 out.push(TsDeclaration {
1165 range: ts_range(child),
1166 kind: "constructor_definition",
1167 container: container.map(String::from),
1168 param_count: Some(types.len()),
1169 param_types: types.into_iter().map(String::from).collect(),
1170 });
1171 }
1172 collect_parameters(child, source, name, container, out);
1174 if let Some(body) = ts_find_child(child, "function_body") {
1175 collect_declarations(body, source, name, container, out);
1176 }
1177 }
1178 "state_variable_declaration" | "variable_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 "struct_declaration" => {
1192 if let Some(id_name) = ts_child_id_text(child, source) {
1193 if id_name == name {
1194 out.push(TsDeclaration {
1195 range: id_range(child),
1196 kind: "struct_declaration",
1197 container: container.map(String::from),
1198 param_count: None,
1199 param_types: vec![],
1200 });
1201 }
1202 if let Some(body) = ts_find_child(child, "struct_body") {
1203 collect_declarations(body, source, name, Some(id_name), out);
1204 }
1205 }
1206 }
1207 "enum_declaration" => {
1208 if let Some(id_name) = ts_child_id_text(child, source) {
1209 if id_name == name {
1210 out.push(TsDeclaration {
1211 range: id_range(child),
1212 kind: "enum_declaration",
1213 container: container.map(String::from),
1214 param_count: None,
1215 param_types: vec![],
1216 });
1217 }
1218 if let Some(body) = ts_find_child(child, "enum_body") {
1220 let mut ecur = body.walk();
1221 for val in body.children(&mut ecur) {
1222 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1223 out.push(TsDeclaration {
1224 range: ts_range(val),
1225 kind: "enum_value",
1226 container: Some(id_name.to_string()),
1227 param_count: None,
1228 param_types: vec![],
1229 });
1230 }
1231 }
1232 }
1233 }
1234 }
1235 "event_definition" | "error_declaration" => {
1236 if let Some(id_name) = ts_child_id_text(child, source)
1237 && id_name == name
1238 {
1239 out.push(TsDeclaration {
1240 range: id_range(child),
1241 kind: child.kind(),
1242 container: container.map(String::from),
1243 param_count: None,
1244 param_types: vec![],
1245 });
1246 }
1247 }
1248 "user_defined_type_definition" => {
1249 if let Some(id_name) = ts_child_id_text(child, source)
1250 && id_name == name
1251 {
1252 out.push(TsDeclaration {
1253 range: id_range(child),
1254 kind: "user_defined_type_definition",
1255 container: container.map(String::from),
1256 param_count: None,
1257 param_types: vec![],
1258 });
1259 }
1260 }
1261 _ => {
1263 collect_declarations(child, source, name, container, out);
1264 }
1265 }
1266 }
1267}
1268
1269fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1275 let mut cursor = node.walk();
1276 node.children(&mut cursor)
1277 .filter(|c| c.kind() == "parameter")
1278 .filter_map(|param| {
1279 let mut pc = param.walk();
1280 param
1281 .children(&mut pc)
1282 .find(|c| {
1283 matches!(
1284 c.kind(),
1285 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1286 )
1287 })
1288 .map(|t| source[t.byte_range()].trim())
1289 })
1290 .collect()
1291}
1292
1293fn collect_parameters(
1295 node: Node,
1296 source: &str,
1297 name: &str,
1298 container: Option<&str>,
1299 out: &mut Vec<TsDeclaration>,
1300) {
1301 let mut cursor = node.walk();
1302 for child in node.children(&mut cursor) {
1303 if child.kind() == "parameter"
1304 && let Some(id_name) = ts_child_id_text(child, source)
1305 && id_name == name
1306 {
1307 out.push(TsDeclaration {
1308 range: id_range(child),
1309 kind: "parameter",
1310 container: container.map(String::from),
1311 param_count: None,
1312 param_types: vec![],
1313 });
1314 }
1315 }
1316}
1317
1318fn ts_range(node: Node) -> Range {
1320 let s = node.start_position();
1321 let e = node.end_position();
1322 Range {
1323 start: Position::new(s.row as u32, s.column as u32),
1324 end: Position::new(e.row as u32, e.column as u32),
1325 }
1326}
1327
1328fn id_range(node: Node) -> Range {
1330 let mut cursor = node.walk();
1331 node.children(&mut cursor)
1332 .find(|c| c.kind() == "identifier" && c.is_named())
1333 .map(|c| ts_range(c))
1334 .unwrap_or_else(|| ts_range(node))
1335}
1336
1337fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1338 let mut cursor = node.walk();
1339 node.children(&mut cursor).find(|c| c.kind() == kind)
1340}
1341
1342pub fn goto_definition_ts(
1350 source: &str,
1351 position: Position,
1352 file_uri: &Url,
1353 completion_cache: &crate::completion::CompletionCache,
1354 text_cache: &HashMap<String, (i32, String)>,
1355) -> Option<Location> {
1356 let ctx = cursor_context(source, position)?;
1357
1358 if let Some(obj_name) = &ctx.object {
1363 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1364 let target_source = read_target_source(&path, text_cache)?;
1365 let target_uri = Url::from_file_path(&path).ok()?;
1366 let decls = find_declarations_by_name(&target_source, &ctx.name);
1367 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1368 return Some(Location {
1369 uri: target_uri,
1370 range: d.range,
1371 });
1372 }
1373 }
1374 let decls = find_declarations_by_name(source, &ctx.name);
1376 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1377 return Some(Location {
1378 uri: file_uri.clone(),
1379 range: d.range,
1380 });
1381 }
1382 }
1383
1384 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1387
1388 match resolved {
1389 Some(ResolvedTarget::SameFile) => {
1390 find_best_declaration(source, &ctx, file_uri)
1392 }
1393 Some(ResolvedTarget::OtherFile { path, name }) => {
1394 let target_source = read_target_source(&path, text_cache);
1396 let target_source = target_source?;
1397 let target_uri = Url::from_file_path(&path).ok()?;
1398 let decls = find_declarations_by_name(&target_source, &name);
1399 decls.first().map(|d| Location {
1400 uri: target_uri,
1401 range: d.range,
1402 })
1403 }
1404 None => {
1405 find_best_declaration(source, &ctx, file_uri)
1407 }
1408 }
1409}
1410
1411#[derive(Debug)]
1412enum ResolvedTarget {
1413 SameFile,
1415 OtherFile { path: String, name: String },
1417}
1418
1419fn resolve_via_cache(
1425 ctx: &CursorContext,
1426 file_uri: &Url,
1427 cache: &crate::completion::CompletionCache,
1428) -> Option<ResolvedTarget> {
1429 let contract_scope = ctx
1431 .contract
1432 .as_ref()
1433 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1434 .copied();
1435
1436 if let Some(contract_id) = contract_scope {
1438 if let Some(func_name) = &ctx.function {
1440 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1443 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1445 && decls.iter().any(|d| d.name == ctx.name)
1446 {
1447 return Some(ResolvedTarget::SameFile);
1448 }
1449 }
1450 }
1451
1452 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1454 && decls.iter().any(|d| d.name == ctx.name)
1455 {
1456 return Some(ResolvedTarget::SameFile);
1457 }
1458
1459 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1461 for &base_id in bases.iter().skip(1) {
1462 if let Some(decls) = cache.scope_declarations.get(&base_id)
1463 && decls.iter().any(|d| d.name == ctx.name)
1464 {
1465 let base_name = cache
1468 .name_to_node_id
1469 .iter()
1470 .find(|&(_, &id)| id == base_id)
1471 .map(|(name, _)| name.clone());
1472
1473 if let Some(base_name) = base_name
1474 && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1475 {
1476 return Some(ResolvedTarget::OtherFile {
1477 path,
1478 name: ctx.name.clone(),
1479 });
1480 }
1481 return Some(ResolvedTarget::SameFile);
1483 }
1484 }
1485 }
1486 }
1487
1488 if cache.name_to_node_id.contains_key(&ctx.name) {
1490 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1492 let current_path = file_uri.to_file_path().ok()?;
1493 let current_str = current_path.to_str()?;
1494 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1495 return Some(ResolvedTarget::SameFile);
1496 }
1497 return Some(ResolvedTarget::OtherFile {
1498 path,
1499 name: ctx.name.clone(),
1500 });
1501 }
1502 return Some(ResolvedTarget::SameFile);
1503 }
1504
1505 if cache.name_to_type.contains_key(&ctx.name) {
1507 return Some(ResolvedTarget::SameFile);
1508 }
1509
1510 None
1511}
1512
1513fn find_function_scope(
1515 cache: &crate::completion::CompletionCache,
1516 contract_id: NodeId,
1517 func_name: &str,
1518) -> Option<NodeId> {
1519 for (&scope_id, &parent_id) in &cache.scope_parent {
1523 if parent_id == contract_id {
1524 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1528 && contract_decls.iter().any(|d| d.name == func_name)
1529 {
1530 if cache.scope_declarations.contains_key(&scope_id)
1534 || cache.scope_parent.values().any(|&p| p == scope_id)
1535 {
1536 return Some(scope_id);
1537 }
1538 }
1539 }
1540 }
1541 None
1542}
1543
1544fn find_file_for_contract(
1546 cache: &crate::completion::CompletionCache,
1547 contract_name: &str,
1548 _file_uri: &Url,
1549) -> Option<String> {
1550 let node_id = cache.name_to_node_id.get(contract_name)?;
1555 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1556 let file_id = scope_range.file_id;
1557
1558 cache
1560 .path_to_file_id
1561 .iter()
1562 .find(|&(_, &fid)| fid == file_id)
1563 .map(|(path, _)| path.clone())
1564}
1565
1566fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1568 let uri = Url::from_file_path(path).ok()?;
1570 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1571 return Some(content.clone());
1572 }
1573 std::fs::read_to_string(path).ok()
1575}
1576
1577fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1579 let decls = find_declarations_by_name(source, &ctx.name);
1580 if decls.is_empty() {
1581 return None;
1582 }
1583
1584 if decls.len() == 1 {
1586 return Some(Location {
1587 uri: file_uri.clone(),
1588 range: decls[0].range,
1589 });
1590 }
1591
1592 if let Some(contract_name) = &ctx.contract
1594 && let Some(d) = decls
1595 .iter()
1596 .find(|d| d.container.as_deref() == Some(contract_name))
1597 {
1598 return Some(Location {
1599 uri: file_uri.clone(),
1600 range: d.range,
1601 });
1602 }
1603
1604 Some(Location {
1606 uri: file_uri.clone(),
1607 range: decls[0].range,
1608 })
1609}
1610
1611#[cfg(test)]
1612mod ts_tests {
1613 use super::*;
1614
1615 #[test]
1616 fn test_cursor_context_state_var() {
1617 let source = r#"
1618contract Token {
1619 uint256 public totalSupply;
1620 function mint(uint256 amount) public {
1621 totalSupply += amount;
1622 }
1623}
1624"#;
1625 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
1627 assert_eq!(ctx.name, "totalSupply");
1628 assert_eq!(ctx.function.as_deref(), Some("mint"));
1629 assert_eq!(ctx.contract.as_deref(), Some("Token"));
1630 }
1631
1632 #[test]
1633 fn test_cursor_context_top_level() {
1634 let source = r#"
1635contract Foo {}
1636contract Bar {}
1637"#;
1638 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
1640 assert_eq!(ctx.name, "Foo");
1641 assert!(ctx.function.is_none());
1642 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
1644 }
1645
1646 #[test]
1647 fn test_find_declarations() {
1648 let source = r#"
1649contract Token {
1650 uint256 public totalSupply;
1651 function mint(uint256 amount) public {
1652 totalSupply += amount;
1653 }
1654}
1655"#;
1656 let decls = find_declarations_by_name(source, "totalSupply");
1657 assert_eq!(decls.len(), 1);
1658 assert_eq!(decls[0].kind, "state_variable_declaration");
1659 assert_eq!(decls[0].container.as_deref(), Some("Token"));
1660 }
1661
1662 #[test]
1663 fn test_find_declarations_multiple_contracts() {
1664 let source = r#"
1665contract A {
1666 uint256 public value;
1667}
1668contract B {
1669 uint256 public value;
1670}
1671"#;
1672 let decls = find_declarations_by_name(source, "value");
1673 assert_eq!(decls.len(), 2);
1674 assert_eq!(decls[0].container.as_deref(), Some("A"));
1675 assert_eq!(decls[1].container.as_deref(), Some("B"));
1676 }
1677
1678 #[test]
1679 fn test_find_declarations_enum_value() {
1680 let source = "contract Foo { enum Status { Active, Paused } }";
1681 let decls = find_declarations_by_name(source, "Active");
1682 assert_eq!(decls.len(), 1);
1683 assert_eq!(decls[0].kind, "enum_value");
1684 assert_eq!(decls[0].container.as_deref(), Some("Status"));
1685 }
1686
1687 #[test]
1688 fn test_cursor_context_short_param() {
1689 let source = r#"
1690contract Shop {
1691 uint256 public TAX;
1692 constructor(uint256 price, uint16 tax, uint16 taxBase) {
1693 TAX = tax;
1694 }
1695}
1696"#;
1697 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
1699 assert_eq!(ctx.name, "tax");
1700 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
1701
1702 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
1704 assert_eq!(ctx2.name, "TAX");
1705
1706 let decls = find_declarations_by_name(source, "tax");
1708 assert_eq!(decls.len(), 1);
1709 assert_eq!(decls[0].kind, "parameter");
1710
1711 let decls_tax_base = find_declarations_by_name(source, "taxBase");
1712 assert_eq!(decls_tax_base.len(), 1);
1713 assert_eq!(decls_tax_base[0].kind, "parameter");
1714
1715 let decls_price = find_declarations_by_name(source, "price");
1716 assert_eq!(decls_price.len(), 1);
1717 assert_eq!(decls_price[0].kind, "parameter");
1718
1719 let decls_tax_upper = find_declarations_by_name(source, "TAX");
1721 assert_eq!(decls_tax_upper.len(), 1);
1722 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
1723 }
1724
1725 #[test]
1726 fn test_find_best_declaration_same_contract() {
1727 let source = r#"
1728contract A { uint256 public x; }
1729contract B { uint256 public x; }
1730"#;
1731 let ctx = CursorContext {
1732 name: "x".into(),
1733 function: None,
1734 contract: Some("B".into()),
1735 object: None,
1736 arg_count: None,
1737 arg_types: vec![],
1738 };
1739 let uri = Url::parse("file:///test.sol").unwrap();
1740 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
1741 assert_eq!(loc.range.start.line, 2);
1743 }
1744}