1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use tower_lsp::lsp_types::{Location, Position, Range, TextEdit, Url};
5use tree_sitter::{Node, Parser};
6
7use crate::types::{
8 AbsPath, FileId, NodeId, PathInterner, RelPath, SolcFileId, SourceLoc, SrcLocation,
9};
10use crate::utils::push_if_node_or_array;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NodeInfo {
14 pub src: SrcLocation,
15 pub name_location: Option<String>,
16 pub name_locations: Vec<String>,
17 pub referenced_declaration: Option<NodeId>,
18 pub node_type: Option<String>,
19 pub member_location: Option<String>,
20 pub absolute_path: Option<String>,
21 #[serde(default)]
26 pub scope: Option<NodeId>,
27 #[serde(default)]
34 pub base_functions: Vec<NodeId>,
35}
36
37pub const CHILD_KEYS: &[&str] = &[
39 "AST",
40 "arguments",
41 "baseContracts",
42 "baseExpression",
43 "baseName",
44 "baseType",
45 "block",
46 "body",
47 "components",
48 "condition",
49 "declarations",
50 "endExpression",
51 "errorCall",
52 "eventCall",
53 "expression",
54 "externalCall",
55 "falseBody",
56 "falseExpression",
57 "file",
58 "foreign",
59 "functionName",
60 "indexExpression",
61 "initialValue",
62 "initializationExpression",
63 "keyType",
64 "leftExpression",
65 "leftHandSide",
66 "libraryName",
67 "literals",
68 "loopExpression",
69 "members",
70 "modifierName",
71 "modifiers",
72 "name",
73 "names",
74 "nodes",
75 "options",
76 "overrides",
77 "parameters",
78 "pathNode",
79 "post",
80 "pre",
81 "returnParameters",
82 "rightExpression",
83 "rightHandSide",
84 "startExpression",
85 "statements",
86 "storageLayout",
87 "subExpression",
88 "subdenomination",
89 "symbolAliases",
90 "trueBody",
91 "trueExpression",
92 "typeName",
93 "unitAlias",
94 "value",
95 "valueType",
96 "variableNames",
97 "variables",
98];
99
100pub type ExternalRefs = HashMap<SrcLocation, NodeId>;
103
104#[derive(Debug, Clone)]
110pub struct CachedBuild {
111 pub nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
112 pub path_to_abs: HashMap<RelPath, AbsPath>,
113 pub external_refs: ExternalRefs,
114 pub id_to_path_map: HashMap<crate::types::SolcFileId, String>,
115 pub decl_index: HashMap<NodeId, crate::solc_ast::DeclNode>,
119 pub node_id_to_source_path: HashMap<NodeId, AbsPath>,
123 pub hint_index: crate::inlay_hints::HintIndex,
126 pub doc_index: crate::hover::DocIndex,
129 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
132 pub build_version: i32,
135 pub content_hash: u64,
139 pub qualifier_refs: HashMap<NodeId, Vec<NodeId>>,
147 pub base_function_implementation: HashMap<NodeId, Vec<NodeId>>,
158}
159
160impl CachedBuild {
161 pub fn new(ast: Value, build_version: i32, interner: Option<&mut PathInterner>) -> Self {
173 let (mut nodes, path_to_abs, mut external_refs) = if let Some(sources) = ast.get("sources")
174 {
175 cache_ids(sources)
176 } else {
177 (HashMap::new(), HashMap::new(), HashMap::new())
178 };
179
180 let solc_id_to_path: HashMap<SolcFileId, String> = ast
182 .get("source_id_to_path")
183 .and_then(|v| v.as_object())
184 .map(|obj| {
185 obj.iter()
186 .map(|(k, v)| {
187 (
188 SolcFileId::new(k.clone()),
189 v.as_str().unwrap_or("").to_string(),
190 )
191 })
192 .collect()
193 })
194 .unwrap_or_default();
195
196 let (id_to_path_map, canonical_remap) = if let Some(interner) = interner {
201 let remap = interner.build_remap(&solc_id_to_path);
202
203 for file_nodes in nodes.values_mut() {
205 for info in file_nodes.values_mut() {
206 canonicalize_node_info(info, &remap);
207 }
208 }
209
210 let old_refs = std::mem::take(&mut external_refs);
212 for (src, decl_id) in old_refs {
213 let new_src = SrcLocation::new(remap_src_canonical(src.as_str(), &remap));
214 external_refs.insert(new_src, decl_id);
215 }
216
217 (interner.to_id_to_path_map(), Some(remap))
219 } else {
220 (solc_id_to_path, None)
222 };
223
224 let doc_index = crate::hover::build_doc_index(&ast);
225
226 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
233 match crate::solc_ast::extract_decl_nodes(sources) {
234 Some(extracted) => (
235 extracted
236 .decl_index
237 .into_iter()
238 .map(|(id, decl)| (NodeId(id), decl))
239 .collect(),
240 extracted
241 .node_id_to_source_path
242 .into_iter()
243 .map(|(id, path)| (NodeId(id), AbsPath::new(path)))
244 .collect(),
245 ),
246 None => (HashMap::new(), HashMap::new()),
247 }
248 } else {
249 (HashMap::new(), HashMap::new())
250 };
251
252 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
254 let hint_index = if let Some(sources) = ast.get("sources") {
255 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
256 } else {
257 HashMap::new()
258 };
259
260 let completion_cache = {
262 let sources = ast.get("sources");
263 let contracts = ast.get("contracts");
264 let cc = if let Some(s) = sources {
265 crate::completion::build_completion_cache(s, contracts, canonical_remap.as_ref())
266 } else {
267 crate::completion::build_completion_cache(
268 &serde_json::Value::Object(Default::default()),
269 contracts,
270 canonical_remap.as_ref(),
271 )
272 };
273 std::sync::Arc::new(cc)
274 };
275
276 let qualifier_refs = build_qualifier_refs(&nodes);
281
282 let base_function_implementation = build_base_function_implementation(&nodes);
286
287 Self {
291 nodes,
292 path_to_abs,
293 external_refs,
294 id_to_path_map,
295 decl_index,
296 node_id_to_source_path,
297 hint_index,
298 doc_index,
299 completion_cache,
300 build_version,
301 content_hash: 0,
302 qualifier_refs,
303 base_function_implementation,
304 }
305 }
306
307 pub fn merge_missing_from(&mut self, other: &CachedBuild) {
314 for (abs_path, file_nodes) in &other.nodes {
315 if !self.nodes.contains_key(abs_path) {
316 self.nodes.insert(abs_path.clone(), file_nodes.clone());
317 }
318 }
319 for (k, v) in &other.path_to_abs {
320 self.path_to_abs
321 .entry(k.clone())
322 .or_insert_with(|| v.clone());
323 }
324 for (k, v) in &other.external_refs {
325 self.external_refs.entry(k.clone()).or_insert(*v);
326 }
327 for (k, v) in &other.id_to_path_map {
328 self.id_to_path_map
329 .entry(k.clone())
330 .or_insert_with(|| v.clone());
331 }
332 for (container_id, other_qrefs) in &other.qualifier_refs {
335 let entry = self.qualifier_refs.entry(*container_id).or_default();
336 for &qnode_id in other_qrefs {
337 if !entry.contains(&qnode_id) {
338 entry.push(qnode_id);
339 }
340 }
341 }
342 for (node_id, other_impls) in &other.base_function_implementation {
345 let entry = self
346 .base_function_implementation
347 .entry(*node_id)
348 .or_default();
349 for &impl_id in other_impls {
350 if !entry.contains(&impl_id) {
351 entry.push(impl_id);
352 }
353 }
354 }
355 }
356
357 pub fn from_reference_index(
366 nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
367 path_to_abs: HashMap<RelPath, AbsPath>,
368 external_refs: ExternalRefs,
369 id_to_path_map: HashMap<SolcFileId, String>,
370 build_version: i32,
371 interner: Option<&mut PathInterner>,
372 ) -> Self {
373 if let Some(interner) = interner {
376 for path in id_to_path_map.values() {
377 interner.intern(path);
378 }
379 }
380
381 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
382 &serde_json::Value::Object(Default::default()),
383 None,
384 None,
385 ));
386
387 let qualifier_refs = build_qualifier_refs(&nodes);
389
390 let base_function_implementation = build_base_function_implementation(&nodes);
393
394 Self {
395 nodes,
396 path_to_abs,
397 external_refs,
398 id_to_path_map,
399 decl_index: HashMap::new(),
400 node_id_to_source_path: HashMap::new(),
401 hint_index: HashMap::new(),
402 doc_index: HashMap::new(),
403 completion_cache,
404 build_version,
405 content_hash: 0,
406 qualifier_refs,
407 base_function_implementation,
408 }
409 }
410}
411
412fn build_qualifier_refs(
422 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
423) -> HashMap<NodeId, Vec<NodeId>> {
424 let mut node_scope: HashMap<NodeId, NodeId> = HashMap::new();
427 for file_nodes in nodes.values() {
428 for (id, info) in file_nodes {
429 if let Some(scope_id) = info.scope {
430 node_scope.insert(*id, scope_id);
431 }
432 }
433 }
434
435 let mut qualifier_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
438 for file_nodes in nodes.values() {
439 for (id, info) in file_nodes {
440 if info.name_locations.len() > 1 && info.node_type.as_deref() == Some("IdentifierPath")
441 {
442 if let Some(ref_decl) = info.referenced_declaration
443 && let Some(&scope_id) = node_scope.get(&ref_decl)
444 {
445 qualifier_refs.entry(scope_id).or_default().push(*id);
446 }
447 }
448 }
449 }
450
451 qualifier_refs
452}
453
454fn build_base_function_implementation(
464 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
465) -> HashMap<NodeId, Vec<NodeId>> {
466 let mut index: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
467
468 for file_nodes in nodes.values() {
469 for (id, info) in file_nodes {
470 if info.base_functions.is_empty() {
471 continue;
472 }
473 for &base_id in &info.base_functions {
475 index.entry(*id).or_default().push(base_id);
476 }
477 for &base_id in &info.base_functions {
479 index.entry(base_id).or_default().push(*id);
480 }
481 }
482 }
483
484 for ids in index.values_mut() {
486 ids.sort_unstable();
487 ids.dedup();
488 }
489
490 index
491}
492
493type CachedIds = (
495 HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
496 HashMap<RelPath, AbsPath>,
497 ExternalRefs,
498);
499
500pub fn remap_src_canonical(src: &str, remap: &HashMap<u64, FileId>) -> String {
504 let Some(last_colon) = src.rfind(':') else {
505 return src.to_owned();
506 };
507 let old_id_str = &src[last_colon + 1..];
508 let Ok(old_id) = old_id_str.parse::<u64>() else {
509 return src.to_owned();
510 };
511 let Some(canonical) = remap.get(&old_id) else {
512 return src.to_owned();
513 };
514 if canonical.0 == old_id {
515 return src.to_owned();
516 }
517 format!("{}{}", &src[..=last_colon], canonical.0)
518}
519
520fn canonicalize_node_info(info: &mut NodeInfo, remap: &HashMap<u64, FileId>) {
523 info.src = SrcLocation::new(remap_src_canonical(info.src.as_str(), remap));
524 if let Some(loc) = info.name_location.as_mut() {
525 *loc = remap_src_canonical(loc, remap);
526 }
527 for loc in &mut info.name_locations {
528 *loc = remap_src_canonical(loc, remap);
529 }
530 if let Some(loc) = info.member_location.as_mut() {
531 *loc = remap_src_canonical(loc, remap);
532 }
533}
534
535pub fn cache_ids(sources: &Value) -> CachedIds {
536 let source_count = sources.as_object().map_or(0, |obj| obj.len());
537
538 let mut nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>> =
541 HashMap::with_capacity(source_count);
542 let mut path_to_abs: HashMap<RelPath, AbsPath> = HashMap::with_capacity(source_count);
543 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
544
545 if let Some(sources_obj) = sources.as_object() {
546 for (path, source_data) in sources_obj {
547 if let Some(ast) = source_data.get("ast") {
548 let abs_path = AbsPath::new(
550 ast.get("absolutePath")
551 .and_then(|v| v.as_str())
552 .unwrap_or(path)
553 .to_string(),
554 );
555
556 path_to_abs.insert(RelPath::new(path), abs_path.clone());
557
558 let size_hint = ast
563 .get("nodes")
564 .and_then(|v| v.as_array())
565 .map_or(64, |arr| arr.len() * 8);
566 if !nodes.contains_key(&abs_path) {
567 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
568 }
569
570 if let Some(id) = ast.get("id").and_then(|v| v.as_i64())
571 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
572 {
573 nodes.get_mut(&abs_path).unwrap().insert(
574 NodeId(id),
575 NodeInfo {
576 src: SrcLocation::new(src),
577 name_location: None,
578 name_locations: vec![],
579 referenced_declaration: None,
580 node_type: ast
581 .get("nodeType")
582 .and_then(|v| v.as_str())
583 .map(|s| s.to_string()),
584 member_location: None,
585 absolute_path: ast
586 .get("absolutePath")
587 .and_then(|v| v.as_str())
588 .map(|s| s.to_string()),
589 scope: ast.get("scope").and_then(|v| v.as_i64()).map(NodeId),
590 base_functions: vec![],
591 },
592 );
593 }
594
595 let mut stack = vec![ast];
596
597 while let Some(tree) = stack.pop() {
598 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_i64())
599 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
600 {
601 let id = NodeId(raw_id);
602 let mut name_location = tree
604 .get("nameLocation")
605 .and_then(|v| v.as_str())
606 .map(|s| s.to_string());
607
608 if name_location.is_none()
612 && let Some(name_locations) = tree.get("nameLocations")
613 && let Some(locations_array) = name_locations.as_array()
614 && !locations_array.is_empty()
615 {
616 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
617 if node_type == Some("IdentifierPath") {
618 name_location = locations_array
619 .last()
620 .and_then(|v| v.as_str())
621 .map(|s| s.to_string());
622 } else {
623 name_location = locations_array[0].as_str().map(|s| s.to_string());
624 }
625 }
626
627 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
628 && let Some(locations_array) = name_locations.as_array()
629 {
630 locations_array
631 .iter()
632 .filter_map(|v| v.as_str().map(|s| s.to_string()))
633 .collect()
634 } else {
635 vec![]
636 };
637
638 let mut final_name_location = name_location;
639 if final_name_location.is_none()
640 && let Some(member_loc) =
641 tree.get("memberLocation").and_then(|v| v.as_str())
642 {
643 final_name_location = Some(member_loc.to_string());
644 }
645
646 let node_info = NodeInfo {
647 src: SrcLocation::new(src),
648 name_location: final_name_location,
649 name_locations,
650 referenced_declaration: tree
651 .get("referencedDeclaration")
652 .and_then(|v| v.as_i64())
653 .map(NodeId),
654 node_type: tree
655 .get("nodeType")
656 .and_then(|v| v.as_str())
657 .map(|s| s.to_string()),
658 member_location: tree
659 .get("memberLocation")
660 .and_then(|v| v.as_str())
661 .map(|s| s.to_string()),
662 absolute_path: tree
663 .get("absolutePath")
664 .and_then(|v| v.as_str())
665 .map(|s| s.to_string()),
666 scope: tree.get("scope").and_then(|v| v.as_i64()).map(NodeId),
667 base_functions: tree
668 .get("baseFunctions")
669 .and_then(|v| v.as_array())
670 .map(|arr| {
671 arr.iter().filter_map(|v| v.as_i64().map(NodeId)).collect()
672 })
673 .unwrap_or_default(),
674 };
675
676 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
677
678 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
680 && let Some(ext_refs) =
681 tree.get("externalReferences").and_then(|v| v.as_array())
682 {
683 for ext_ref in ext_refs {
684 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
685 && let Some(decl_id) =
686 ext_ref.get("declaration").and_then(|v| v.as_i64())
687 {
688 external_refs
689 .insert(SrcLocation::new(src_str), NodeId(decl_id));
690 }
691 }
692 }
693 }
694
695 for key in CHILD_KEYS {
696 push_if_node_or_array(tree, key, &mut stack);
697 }
698 }
699 }
700 }
701 }
702
703 (nodes, path_to_abs, external_refs)
704}
705
706pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
707 let text = String::from_utf8_lossy(source_bytes);
708 crate::utils::position_to_byte_offset(&text, position)
709}
710
711pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
712 let text = String::from_utf8_lossy(source_bytes);
713 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
714 Some(pos)
715}
716
717pub fn src_to_location(
719 src: &str,
720 id_to_path: &HashMap<crate::types::SolcFileId, String>,
721) -> Option<Location> {
722 let loc = SourceLoc::parse(src)?;
723 let file_path = id_to_path.get(&loc.file_id_str())?;
724
725 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
726 std::path::PathBuf::from(file_path)
727 } else {
728 std::env::current_dir().ok()?.join(file_path)
729 };
730
731 let source_bytes = std::fs::read(&absolute_path).ok()?;
732 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
733 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
734 let uri = Url::from_file_path(&absolute_path).ok()?;
735
736 Some(Location {
737 uri,
738 range: Range {
739 start: start_pos,
740 end: end_pos,
741 },
742 })
743}
744
745pub fn goto_bytes(
746 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
747 path_to_abs: &HashMap<RelPath, AbsPath>,
748 id_to_path: &HashMap<crate::types::SolcFileId, String>,
749 external_refs: &ExternalRefs,
750 uri: &str,
751 position: usize,
752) -> Option<(String, usize, usize)> {
753 let path = match uri.starts_with("file://") {
754 true => &uri[7..],
755 false => uri,
756 };
757
758 let abs_path = path_to_abs.get(path)?;
760
761 let current_file_nodes = nodes.get(abs_path)?;
763
764 let path_to_file_id: HashMap<&str, &crate::types::SolcFileId> =
766 id_to_path.iter().map(|(id, p)| (p.as_str(), id)).collect();
767
768 let current_file_id = path_to_file_id.get(abs_path.as_str());
772
773 for (src_str, decl_id) in external_refs {
775 let Some(src_loc) = SourceLoc::parse(src_str.as_str()) else {
776 continue;
777 };
778
779 if let Some(file_id) = current_file_id {
781 if src_loc.file_id_str() != **file_id {
782 continue;
783 }
784 } else {
785 continue;
786 }
787
788 if src_loc.offset <= position && position < src_loc.end() {
789 let mut target_node: Option<&NodeInfo> = None;
791 for file_nodes in nodes.values() {
792 if let Some(node) = file_nodes.get(decl_id) {
793 target_node = Some(node);
794 break;
795 }
796 }
797 let node = target_node?;
798 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
799 let loc = SourceLoc::parse(loc_str)?;
800 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
801 return Some((file_path, loc.offset, loc.length));
802 }
803 }
804
805 let mut refs = HashMap::new();
806
807 for (id, content) in current_file_nodes {
809 if content.referenced_declaration.is_none() {
810 continue;
811 }
812
813 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
814 continue;
815 };
816
817 if src_loc.offset <= position && position < src_loc.end() {
818 let diff = src_loc.length;
819 if !refs.contains_key(&diff) || refs[&diff] <= *id {
820 refs.insert(diff, *id);
821 }
822 }
823 }
824
825 if refs.is_empty() {
826 let tmp = current_file_nodes.iter();
829 for (_id, content) in tmp {
830 if content.node_type == Some("ImportDirective".to_string()) {
831 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
832 continue;
833 };
834
835 if src_loc.offset <= position
836 && position < src_loc.end()
837 && let Some(import_path) = &content.absolute_path
838 {
839 return Some((import_path.clone(), 0, 0));
840 }
841 }
842 }
843 return None;
844 }
845
846 let min_diff = *refs.keys().min()?;
848 let chosen_id = refs[&min_diff];
849 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
850
851 let mut target_node: Option<&NodeInfo> = None;
853 for file_nodes in nodes.values() {
854 if let Some(node) = file_nodes.get(&ref_id) {
855 target_node = Some(node);
856 break;
857 }
858 }
859
860 let node = target_node?;
861
862 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
864 let loc = SourceLoc::parse(loc_str)?;
865 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
866
867 Some((file_path, loc.offset, loc.length))
868}
869
870fn resolve_qualifier_goto(
875 build: &CachedBuild,
876 file_uri: &Url,
877 byte_position: usize,
878) -> Option<Location> {
879 let path = file_uri.to_file_path().ok()?;
880 let path_str = path.to_str()?;
881 let abs_path = build.path_to_abs.get(path_str)?;
882 let file_nodes = build.nodes.get(abs_path)?;
883
884 let node_id = crate::references::byte_to_id(&build.nodes, abs_path, byte_position)?;
886 let node_info = file_nodes.get(&node_id)?;
887
888 if node_info.node_type.as_deref() != Some("IdentifierPath")
890 || node_info.name_locations.len() <= 1
891 {
892 return None;
893 }
894
895 let first_loc = SourceLoc::parse(&node_info.name_locations[0])?;
897 if byte_position < first_loc.offset || byte_position >= first_loc.end() {
898 return None;
899 }
900
901 let ref_decl_id = node_info.referenced_declaration?;
903 let decl_node = find_node_info(&build.nodes, ref_decl_id)?;
905 let scope_id = decl_node.scope?;
906
907 let container_node = find_node_info(&build.nodes, scope_id)?;
909 let loc_str = container_node
910 .name_location
911 .as_deref()
912 .unwrap_or(container_node.src.as_str());
913 let loc = SourceLoc::parse(loc_str)?;
914 let file_path = build.id_to_path_map.get(&loc.file_id_str())?;
915
916 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
917 std::path::PathBuf::from(file_path)
918 } else {
919 std::env::current_dir().ok()?.join(file_path)
920 };
921
922 let target_bytes = std::fs::read(&absolute_path).ok()?;
923 let start_pos = bytes_to_pos(&target_bytes, loc.offset)?;
924 let end_pos = bytes_to_pos(&target_bytes, loc.end())?;
925 let target_uri = Url::from_file_path(&absolute_path).ok()?;
926
927 Some(Location {
928 uri: target_uri,
929 range: Range {
930 start: start_pos,
931 end: end_pos,
932 },
933 })
934}
935
936fn find_node_info<'a>(
938 nodes: &'a HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
939 node_id: NodeId,
940) -> Option<&'a NodeInfo> {
941 for file_nodes in nodes.values() {
942 if let Some(node) = file_nodes.get(&node_id) {
943 return Some(node);
944 }
945 }
946 None
947}
948
949pub fn goto_declaration_cached(
952 build: &CachedBuild,
953 file_uri: &Url,
954 position: Position,
955 source_bytes: &[u8],
956) -> Option<Location> {
957 let byte_position = pos_to_bytes(source_bytes, position);
958
959 if let Some(location) = resolve_qualifier_goto(build, file_uri, byte_position) {
964 return Some(location);
965 }
966
967 if let Some((file_path, location_bytes, length)) = goto_bytes(
968 &build.nodes,
969 &build.path_to_abs,
970 &build.id_to_path_map,
971 &build.external_refs,
972 file_uri.as_ref(),
973 byte_position,
974 ) {
975 let target_file_path = std::path::Path::new(&file_path);
976 let absolute_path = if target_file_path.is_absolute() {
977 target_file_path.to_path_buf()
978 } else {
979 let base = file_uri
984 .to_file_path()
985 .ok()
986 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
987 .or_else(|| std::env::current_dir().ok())
988 .unwrap_or_default();
989 base.join(target_file_path)
990 };
991
992 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
993 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
994 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
995 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
996 {
997 return Some(Location {
998 uri: target_uri,
999 range: Range {
1000 start: start_pos,
1001 end: end_pos,
1002 },
1003 });
1004 }
1005 };
1006
1007 None
1008}
1009
1010pub fn goto_declaration_by_name(
1021 cached_build: &CachedBuild,
1022 file_uri: &Url,
1023 name: &str,
1024 byte_hint: usize,
1025) -> Option<Location> {
1026 let path = match file_uri.as_ref().starts_with("file://") {
1027 true => &file_uri.as_ref()[7..],
1028 false => file_uri.as_ref(),
1029 };
1030 let abs_path = cached_build.path_to_abs.get(path)?;
1031 let built_source = std::fs::read_to_string(abs_path).ok()?;
1033
1034 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
1036
1037 let tmp = {
1038 let this = cached_build.nodes.get(abs_path)?;
1039 this.iter()
1040 };
1041 for (_id, node) in tmp {
1042 let ref_id = match node.referenced_declaration {
1043 Some(id) => id,
1044 None => continue,
1045 };
1046
1047 let Some(src_loc) = SourceLoc::parse(node.src.as_str()) else {
1049 continue;
1050 };
1051 let start = src_loc.offset;
1052 let length = src_loc.length;
1053
1054 if start + length > built_source.len() {
1055 continue;
1056 }
1057
1058 let node_text = &built_source[start..start + length];
1059
1060 let matches = node_text == name
1065 || node_text.contains(&format!(".{name}("))
1066 || node_text.ends_with(&format!(".{name}"));
1067
1068 if matches {
1069 let distance = if byte_hint >= start && byte_hint < start + length {
1073 0 } else if byte_hint < start {
1075 start - byte_hint
1076 } else {
1077 byte_hint - (start + length)
1078 };
1079 candidates.push((distance, length, ref_id));
1080 }
1081 }
1082
1083 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1085 let ref_id = candidates.first()?.2;
1086
1087 let mut target_node: Option<&NodeInfo> = None;
1089 for file_nodes in cached_build.nodes.values() {
1090 if let Some(node) = file_nodes.get(&ref_id) {
1091 target_node = Some(node);
1092 break;
1093 }
1094 }
1095
1096 let node = target_node?;
1097
1098 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1100 let loc = SourceLoc::parse(loc_str)?;
1101
1102 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1103 let location_bytes = loc.offset;
1104 let length = loc.length;
1105
1106 let target_file_path = std::path::Path::new(file_path);
1107 let absolute_path = if target_file_path.is_absolute() {
1108 target_file_path.to_path_buf()
1109 } else {
1110 let base = file_uri
1111 .to_file_path()
1112 .ok()
1113 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1114 .or_else(|| std::env::current_dir().ok())
1115 .unwrap_or_default();
1116 base.join(target_file_path)
1117 };
1118
1119 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1120 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1121 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1122 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1123
1124 Some(Location {
1125 uri: target_uri,
1126 range: Range {
1127 start: start_pos,
1128 end: end_pos,
1129 },
1130 })
1131}
1132
1133#[derive(Debug, Clone)]
1137pub struct CursorContext {
1138 pub name: String,
1140 pub function: Option<String>,
1142 pub contract: Option<String>,
1144 pub object: Option<String>,
1148 pub arg_count: Option<usize>,
1151 pub arg_types: Vec<Option<String>>,
1154}
1155
1156fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1158 let mut parser = Parser::new();
1159 parser
1160 .set_language(&tree_sitter_solidity::LANGUAGE.into())
1161 .expect("failed to load Solidity grammar");
1162 parser.parse(source, None)
1163}
1164
1165pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1171 let line = location.range.start.line as usize;
1172 let start_col = location.range.start.character as usize;
1173 let end_col = location.range.end.character as usize;
1174
1175 if let Some(line_text) = target_source.lines().nth(line)
1176 && end_col <= line_text.len()
1177 {
1178 return &line_text[start_col..end_col] == expected_name;
1179 }
1180 true
1182}
1183
1184fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1186 if byte < node.start_byte() || byte >= node.end_byte() {
1187 return None;
1188 }
1189 let mut cursor = node.walk();
1190 for child in node.children(&mut cursor) {
1191 if child.start_byte() <= byte
1192 && byte < child.end_byte()
1193 && let Some(deeper) = ts_node_at_byte(child, byte)
1194 {
1195 return Some(deeper);
1196 }
1197 }
1198 Some(node)
1199}
1200
1201fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1203 let mut cursor = node.walk();
1204 node.children(&mut cursor)
1205 .find(|c| c.kind() == "identifier" && c.is_named())
1206 .map(|c| &source[c.byte_range()])
1207}
1208
1209fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1215 let expr = if arg_node.kind() == "call_argument" {
1217 let mut c = arg_node.walk();
1218 arg_node.children(&mut c).find(|ch| ch.is_named())?
1219 } else {
1220 arg_node
1221 };
1222
1223 match expr.kind() {
1224 "identifier" => {
1225 let var_name = &source[expr.byte_range()];
1226 find_variable_type(expr, source, var_name)
1228 }
1229 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1230 "boolean_literal" => Some("bool".into()),
1231 "string_literal" | "hex_string_literal" => Some("string".into()),
1232 _ => None,
1233 }
1234}
1235
1236fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1241 let mut scope = from.parent();
1242 while let Some(node) = scope {
1243 match node.kind() {
1244 "function_definition" | "modifier_definition" | "constructor_definition" => {
1245 let mut c = node.walk();
1247 for child in node.children(&mut c) {
1248 if child.kind() == "parameter"
1249 && let Some(id) = ts_child_id_text(child, source)
1250 && id == var_name
1251 {
1252 let mut pc = child.walk();
1254 return child
1255 .children(&mut pc)
1256 .find(|c| {
1257 matches!(
1258 c.kind(),
1259 "type_name"
1260 | "primitive_type"
1261 | "user_defined_type"
1262 | "mapping"
1263 )
1264 })
1265 .map(|t| source[t.byte_range()].trim().to_string());
1266 }
1267 }
1268 }
1269 "function_body" | "block_statement" | "unchecked_block" => {
1270 let mut c = node.walk();
1272 for child in node.children(&mut c) {
1273 if (child.kind() == "variable_declaration_statement"
1274 || child.kind() == "variable_declaration")
1275 && let Some(id) = ts_child_id_text(child, source)
1276 && id == var_name
1277 {
1278 let mut pc = child.walk();
1279 return child
1280 .children(&mut pc)
1281 .find(|c| {
1282 matches!(
1283 c.kind(),
1284 "type_name"
1285 | "primitive_type"
1286 | "user_defined_type"
1287 | "mapping"
1288 )
1289 })
1290 .map(|t| source[t.byte_range()].trim().to_string());
1291 }
1292 }
1293 }
1294 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1295 if let Some(body) = ts_find_child(node, "contract_body") {
1297 let mut c = body.walk();
1298 for child in body.children(&mut c) {
1299 if child.kind() == "state_variable_declaration"
1300 && let Some(id) = ts_child_id_text(child, source)
1301 && id == var_name
1302 {
1303 let mut pc = child.walk();
1304 return child
1305 .children(&mut pc)
1306 .find(|c| {
1307 matches!(
1308 c.kind(),
1309 "type_name"
1310 | "primitive_type"
1311 | "user_defined_type"
1312 | "mapping"
1313 )
1314 })
1315 .map(|t| source[t.byte_range()].trim().to_string());
1316 }
1317 }
1318 }
1319 }
1320 _ => {}
1321 }
1322 scope = node.parent();
1323 }
1324 None
1325}
1326
1327fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1329 let mut cursor = call_node.walk();
1330 call_node
1331 .children(&mut cursor)
1332 .filter(|c| c.kind() == "call_argument")
1333 .map(|arg| infer_argument_type(arg, source))
1334 .collect()
1335}
1336
1337fn best_overload<'a>(
1345 decls: &'a [TsDeclaration],
1346 arg_count: Option<usize>,
1347 arg_types: &[Option<String>],
1348) -> Option<&'a TsDeclaration> {
1349 if decls.len() == 1 {
1350 return decls.first();
1351 }
1352 if decls.is_empty() {
1353 return None;
1354 }
1355
1356 let func_decls: Vec<&TsDeclaration> =
1358 decls.iter().filter(|d| d.param_count.is_some()).collect();
1359
1360 if func_decls.is_empty() {
1361 return decls.first();
1362 }
1363
1364 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1366 let matched: Vec<_> = func_decls
1367 .iter()
1368 .filter(|d| d.param_count == Some(ac))
1369 .collect();
1370 if matched.len() == 1 {
1371 return Some(matched[0]);
1372 }
1373 if matched.is_empty() {
1374 func_decls.iter().collect()
1376 } else {
1377 matched
1378 }
1379 } else {
1380 func_decls.iter().collect()
1381 };
1382
1383 if !arg_types.is_empty() {
1385 let mut best: Option<(&TsDeclaration, usize)> = None;
1386 for &&decl in &count_matched {
1387 let score = arg_types
1388 .iter()
1389 .zip(decl.param_types.iter())
1390 .filter(|(arg_ty, param_ty)| {
1391 if let Some(at) = arg_ty {
1392 at == param_ty.as_str()
1393 } else {
1394 false
1395 }
1396 })
1397 .count();
1398 if best.is_none() || score > best.unwrap().1 {
1399 best = Some((decl, score));
1400 }
1401 }
1402 if let Some((decl, _)) = best {
1403 return Some(decl);
1404 }
1405 }
1406
1407 count_matched.first().map(|d| **d).or(decls.first())
1409}
1410
1411pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1415 let tree = ts_parse(source)?;
1416 let byte = pos_to_bytes(source.as_bytes(), position);
1417 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1418
1419 let id_node = if leaf.kind() == "identifier" {
1421 leaf
1422 } else {
1423 let parent = leaf.parent()?;
1425 if parent.kind() == "identifier" {
1426 parent
1427 } else {
1428 return None;
1429 }
1430 };
1431
1432 let name = source[id_node.byte_range()].to_string();
1433 let mut function = None;
1434 let mut contract = None;
1435
1436 let object = id_node.parent().and_then(|parent| {
1440 if parent.kind() == "member_expression" {
1441 let prop = parent.child_by_field_name("property")?;
1442 if prop.id() == id_node.id() {
1444 let obj = parent.child_by_field_name("object")?;
1445 Some(source[obj.byte_range()].to_string())
1446 } else {
1447 None
1448 }
1449 } else {
1450 None
1451 }
1452 });
1453
1454 let (arg_count, arg_types) = {
1458 let mut node = id_node.parent();
1459 let mut result = (None, vec![]);
1460 while let Some(n) = node {
1461 if n.kind() == "call_expression" {
1462 let types = infer_call_arg_types(n, source);
1463 result = (Some(types.len()), types);
1464 break;
1465 }
1466 node = n.parent();
1467 }
1468 result
1469 };
1470
1471 let mut current = id_node.parent();
1473 while let Some(node) = current {
1474 match node.kind() {
1475 "function_definition" | "modifier_definition" if function.is_none() => {
1476 function = ts_child_id_text(node, source).map(String::from);
1477 }
1478 "constructor_definition" if function.is_none() => {
1479 function = Some("constructor".into());
1480 }
1481 "contract_declaration" | "interface_declaration" | "library_declaration"
1482 if contract.is_none() =>
1483 {
1484 contract = ts_child_id_text(node, source).map(String::from);
1485 }
1486 _ => {}
1487 }
1488 current = node.parent();
1489 }
1490
1491 Some(CursorContext {
1492 name,
1493 function,
1494 contract,
1495 object,
1496 arg_count,
1497 arg_types,
1498 })
1499}
1500
1501#[derive(Debug, Clone)]
1503pub struct TsDeclaration {
1504 pub range: Range,
1506 pub kind: &'static str,
1508 pub container: Option<String>,
1510 pub param_count: Option<usize>,
1512 pub param_types: Vec<String>,
1515}
1516
1517pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1522 let tree = match ts_parse(source) {
1523 Some(t) => t,
1524 None => return vec![],
1525 };
1526 let mut results = Vec::new();
1527 collect_declarations(tree.root_node(), source, name, None, &mut results);
1528 results
1529}
1530
1531fn collect_declarations(
1532 node: Node,
1533 source: &str,
1534 name: &str,
1535 container: Option<&str>,
1536 out: &mut Vec<TsDeclaration>,
1537) {
1538 let mut cursor = node.walk();
1539 for child in node.children(&mut cursor) {
1540 if !child.is_named() {
1541 continue;
1542 }
1543 match child.kind() {
1544 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1545 if let Some(id_name) = ts_child_id_text(child, source) {
1546 if id_name == name {
1547 out.push(TsDeclaration {
1548 range: id_range(child),
1549 kind: child.kind(),
1550 container: container.map(String::from),
1551 param_count: None,
1552 param_types: vec![],
1553 });
1554 }
1555 if let Some(body) = ts_find_child(child, "contract_body") {
1557 collect_declarations(body, source, name, Some(id_name), out);
1558 }
1559 }
1560 }
1561 "function_definition" | "modifier_definition" => {
1562 if let Some(id_name) = ts_child_id_text(child, source) {
1563 if id_name == name {
1564 let types = parameter_type_signature(child, source);
1565 out.push(TsDeclaration {
1566 range: id_range(child),
1567 kind: child.kind(),
1568 container: container.map(String::from),
1569 param_count: Some(types.len()),
1570 param_types: types.into_iter().map(String::from).collect(),
1571 });
1572 }
1573 collect_parameters(child, source, name, container, out);
1575 if let Some(body) = ts_find_child(child, "function_body") {
1577 collect_declarations(body, source, name, container, out);
1578 }
1579 }
1580 }
1581 "constructor_definition" => {
1582 if name == "constructor" {
1583 let types = parameter_type_signature(child, source);
1584 out.push(TsDeclaration {
1585 range: ts_range(child),
1586 kind: "constructor_definition",
1587 container: container.map(String::from),
1588 param_count: Some(types.len()),
1589 param_types: types.into_iter().map(String::from).collect(),
1590 });
1591 }
1592 collect_parameters(child, source, name, container, out);
1594 if let Some(body) = ts_find_child(child, "function_body") {
1595 collect_declarations(body, source, name, container, out);
1596 }
1597 }
1598 "state_variable_declaration" | "variable_declaration" => {
1599 if let Some(id_name) = ts_child_id_text(child, source)
1600 && id_name == name
1601 {
1602 out.push(TsDeclaration {
1603 range: id_range(child),
1604 kind: child.kind(),
1605 container: container.map(String::from),
1606 param_count: None,
1607 param_types: vec![],
1608 });
1609 }
1610 }
1611 "struct_declaration" => {
1612 if let Some(id_name) = ts_child_id_text(child, source) {
1613 if id_name == name {
1614 out.push(TsDeclaration {
1615 range: id_range(child),
1616 kind: "struct_declaration",
1617 container: container.map(String::from),
1618 param_count: None,
1619 param_types: vec![],
1620 });
1621 }
1622 if let Some(body) = ts_find_child(child, "struct_body") {
1623 collect_declarations(body, source, name, Some(id_name), out);
1624 }
1625 }
1626 }
1627 "enum_declaration" => {
1628 if let Some(id_name) = ts_child_id_text(child, source) {
1629 if id_name == name {
1630 out.push(TsDeclaration {
1631 range: id_range(child),
1632 kind: "enum_declaration",
1633 container: container.map(String::from),
1634 param_count: None,
1635 param_types: vec![],
1636 });
1637 }
1638 if let Some(body) = ts_find_child(child, "enum_body") {
1640 let mut ecur = body.walk();
1641 for val in body.children(&mut ecur) {
1642 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1643 out.push(TsDeclaration {
1644 range: ts_range(val),
1645 kind: "enum_value",
1646 container: Some(id_name.to_string()),
1647 param_count: None,
1648 param_types: vec![],
1649 });
1650 }
1651 }
1652 }
1653 }
1654 }
1655 "event_definition" | "error_declaration" => {
1656 if let Some(id_name) = ts_child_id_text(child, source)
1657 && id_name == name
1658 {
1659 out.push(TsDeclaration {
1660 range: id_range(child),
1661 kind: child.kind(),
1662 container: container.map(String::from),
1663 param_count: None,
1664 param_types: vec![],
1665 });
1666 }
1667 }
1668 "user_defined_type_definition" => {
1669 if let Some(id_name) = ts_child_id_text(child, source)
1670 && id_name == name
1671 {
1672 out.push(TsDeclaration {
1673 range: id_range(child),
1674 kind: "user_defined_type_definition",
1675 container: container.map(String::from),
1676 param_count: None,
1677 param_types: vec![],
1678 });
1679 }
1680 }
1681 _ => {
1683 collect_declarations(child, source, name, container, out);
1684 }
1685 }
1686 }
1687}
1688
1689fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1695 let mut cursor = node.walk();
1696 node.children(&mut cursor)
1697 .filter(|c| c.kind() == "parameter")
1698 .filter_map(|param| {
1699 let mut pc = param.walk();
1700 param
1701 .children(&mut pc)
1702 .find(|c| {
1703 matches!(
1704 c.kind(),
1705 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1706 )
1707 })
1708 .map(|t| source[t.byte_range()].trim())
1709 })
1710 .collect()
1711}
1712
1713fn collect_parameters(
1715 node: Node,
1716 source: &str,
1717 name: &str,
1718 container: Option<&str>,
1719 out: &mut Vec<TsDeclaration>,
1720) {
1721 let mut cursor = node.walk();
1722 for child in node.children(&mut cursor) {
1723 if child.kind() == "parameter"
1724 && let Some(id_name) = ts_child_id_text(child, source)
1725 && id_name == name
1726 {
1727 out.push(TsDeclaration {
1728 range: id_range(child),
1729 kind: "parameter",
1730 container: container.map(String::from),
1731 param_count: None,
1732 param_types: vec![],
1733 });
1734 }
1735 }
1736}
1737
1738fn ts_range(node: Node) -> Range {
1740 let s = node.start_position();
1741 let e = node.end_position();
1742 Range {
1743 start: Position::new(s.row as u32, s.column as u32),
1744 end: Position::new(e.row as u32, e.column as u32),
1745 }
1746}
1747
1748fn id_range(node: Node) -> Range {
1750 let mut cursor = node.walk();
1751 node.children(&mut cursor)
1752 .find(|c| c.kind() == "identifier" && c.is_named())
1753 .map(|c| ts_range(c))
1754 .unwrap_or_else(|| ts_range(node))
1755}
1756
1757fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1758 let mut cursor = node.walk();
1759 node.children(&mut cursor).find(|c| c.kind() == kind)
1760}
1761
1762pub fn goto_definition_ts(
1770 source: &str,
1771 position: Position,
1772 file_uri: &Url,
1773 completion_cache: &crate::completion::CompletionCache,
1774 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1775) -> Option<Location> {
1776 let ctx = cursor_context(source, position)?;
1777
1778 if let Some(obj_name) = &ctx.object {
1783 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1784 let target_source = read_target_source(&path, text_cache)?;
1785 let target_uri = Url::from_file_path(&path).ok()?;
1786 let decls = find_declarations_by_name(&target_source, &ctx.name);
1787 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1788 return Some(Location {
1789 uri: target_uri,
1790 range: d.range,
1791 });
1792 }
1793 }
1794 let decls = find_declarations_by_name(source, &ctx.name);
1796 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1797 return Some(Location {
1798 uri: file_uri.clone(),
1799 range: d.range,
1800 });
1801 }
1802 }
1803
1804 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1807
1808 match resolved {
1809 Some(ResolvedTarget::SameFile) => {
1810 find_best_declaration(source, &ctx, file_uri)
1812 }
1813 Some(ResolvedTarget::OtherFile { path, name }) => {
1814 let target_source = read_target_source(&path, text_cache);
1816 let target_source = target_source?;
1817 let target_uri = Url::from_file_path(&path).ok()?;
1818 let decls = find_declarations_by_name(&target_source, &name);
1819 decls.first().map(|d| Location {
1820 uri: target_uri,
1821 range: d.range,
1822 })
1823 }
1824 None => {
1825 find_best_declaration(source, &ctx, file_uri)
1827 }
1828 }
1829}
1830
1831#[derive(Debug)]
1832enum ResolvedTarget {
1833 SameFile,
1835 OtherFile { path: String, name: String },
1837}
1838
1839fn resolve_via_cache(
1845 ctx: &CursorContext,
1846 file_uri: &Url,
1847 cache: &crate::completion::CompletionCache,
1848) -> Option<ResolvedTarget> {
1849 let contract_scope = ctx
1851 .contract
1852 .as_ref()
1853 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1854 .copied();
1855
1856 if let Some(contract_id) = contract_scope {
1858 if let Some(func_name) = &ctx.function {
1860 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1863 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1865 && decls.iter().any(|d| d.name == ctx.name)
1866 {
1867 return Some(ResolvedTarget::SameFile);
1868 }
1869 }
1870 }
1871
1872 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1874 && decls.iter().any(|d| d.name == ctx.name)
1875 {
1876 return Some(ResolvedTarget::SameFile);
1877 }
1878
1879 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1881 for &base_id in bases.iter().skip(1) {
1882 if let Some(decls) = cache.scope_declarations.get(&base_id)
1883 && decls.iter().any(|d| d.name == ctx.name)
1884 {
1885 let base_name = cache
1888 .name_to_node_id
1889 .iter()
1890 .find(|&(_, &id)| id == base_id)
1891 .map(|(name, _)| name.clone());
1892
1893 if let Some(base_name) = base_name
1894 && let Some(path) =
1895 find_file_for_contract(cache, base_name.as_str(), file_uri)
1896 {
1897 return Some(ResolvedTarget::OtherFile {
1898 path,
1899 name: ctx.name.clone(),
1900 });
1901 }
1902 return Some(ResolvedTarget::SameFile);
1904 }
1905 }
1906 }
1907 }
1908
1909 if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
1911 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1913 let current_path = file_uri.to_file_path().ok()?;
1914 let current_str = current_path.to_str()?;
1915 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1916 return Some(ResolvedTarget::SameFile);
1917 }
1918 return Some(ResolvedTarget::OtherFile {
1919 path,
1920 name: ctx.name.clone(),
1921 });
1922 }
1923 return Some(ResolvedTarget::SameFile);
1924 }
1925
1926 if cache.name_to_type.contains_key(ctx.name.as_str()) {
1928 return Some(ResolvedTarget::SameFile);
1929 }
1930
1931 None
1932}
1933
1934fn find_function_scope(
1936 cache: &crate::completion::CompletionCache,
1937 contract_id: NodeId,
1938 func_name: &str,
1939) -> Option<NodeId> {
1940 for (&scope_id, &parent_id) in &cache.scope_parent {
1944 if parent_id == contract_id {
1945 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1949 && contract_decls.iter().any(|d| d.name == func_name)
1950 {
1951 if cache.scope_declarations.contains_key(&scope_id)
1955 || cache.scope_parent.values().any(|&p| p == scope_id)
1956 {
1957 return Some(scope_id);
1958 }
1959 }
1960 }
1961 }
1962 None
1963}
1964
1965fn find_file_for_contract(
1967 cache: &crate::completion::CompletionCache,
1968 contract_name: &str,
1969 _file_uri: &Url,
1970) -> Option<String> {
1971 let node_id = cache.name_to_node_id.get(contract_name)?;
1976 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1977 let file_id = scope_range.file_id;
1978
1979 cache
1981 .path_to_file_id
1982 .iter()
1983 .find(|&(_, &fid)| fid == file_id)
1984 .map(|(path, _)| path.to_string())
1985}
1986
1987fn read_target_source(
1989 path: &str,
1990 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1991) -> Option<String> {
1992 let uri = Url::from_file_path(path).ok()?;
1994 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1995 return Some(content.clone());
1996 }
1997 std::fs::read_to_string(path).ok()
1999}
2000
2001fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
2003 let decls = find_declarations_by_name(source, &ctx.name);
2004 if decls.is_empty() {
2005 return None;
2006 }
2007
2008 if decls.len() == 1 {
2010 return Some(Location {
2011 uri: file_uri.clone(),
2012 range: decls[0].range,
2013 });
2014 }
2015
2016 if let Some(contract_name) = &ctx.contract
2018 && let Some(d) = decls
2019 .iter()
2020 .find(|d| d.container.as_deref() == Some(contract_name))
2021 {
2022 return Some(Location {
2023 uri: file_uri.clone(),
2024 range: d.range,
2025 });
2026 }
2027
2028 Some(Location {
2030 uri: file_uri.clone(),
2031 range: decls[0].range,
2032 })
2033}
2034
2035#[derive(Debug, Clone, Copy)]
2041pub(crate) enum CodeActionKind<'a> {
2042 InsertAtFileStart { text: &'a str },
2044
2045 ReplaceToken {
2050 replacement: &'a str,
2051 walk_to: Option<&'a str>,
2052 },
2053
2054 DeleteToken,
2057
2058 DeleteLocalVar,
2061
2062 DeleteNodeByKind { node_kind: &'a str },
2066
2067 DeleteChildNode {
2070 walk_to: &'a str,
2071 child_kinds: &'a [&'a str],
2072 },
2073
2074 ReplaceChildNode {
2077 walk_to: &'a str,
2078 child_kind: &'a str,
2079 replacement: &'a str,
2080 },
2081
2082 InsertBeforeNode {
2086 walk_to: &'a str,
2087 before_child: &'a [&'a str],
2088 text: &'a str,
2089 },
2090}
2091
2092pub(crate) fn code_action_edit(
2097 source: &str,
2098 diag_range: Range,
2099 kind: CodeActionKind<'_>,
2100) -> Option<TextEdit> {
2101 let source_bytes = source.as_bytes();
2102
2103 match kind {
2104 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2106 range: Range {
2107 start: Position {
2108 line: 0,
2109 character: 0,
2110 },
2111 end: Position {
2112 line: 0,
2113 character: 0,
2114 },
2115 },
2116 new_text: text.to_string(),
2117 }),
2118
2119 CodeActionKind::ReplaceToken {
2121 replacement,
2122 walk_to,
2123 } => {
2124 let tree = ts_parse(source)?;
2125 let byte = pos_to_bytes(source_bytes, diag_range.start);
2126 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2127 if let Some(target_kind) = walk_to {
2131 loop {
2132 if node.kind() == target_kind {
2133 break;
2134 }
2135 node = node.parent()?;
2136 }
2137 }
2138 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2139 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2140 Some(TextEdit {
2141 range: Range {
2142 start: start_pos,
2143 end: end_pos,
2144 },
2145 new_text: replacement.to_string(),
2146 })
2147 }
2148
2149 CodeActionKind::DeleteToken => {
2151 let tree = ts_parse(source)?;
2152 let byte = pos_to_bytes(source_bytes, diag_range.start);
2153 let node = ts_node_at_byte(tree.root_node(), byte)?;
2154 let start = node.start_byte();
2155 let end =
2156 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2157 node.end_byte() + 1
2158 } else {
2159 node.end_byte()
2160 };
2161 let start_pos = bytes_to_pos(source_bytes, start)?;
2162 let end_pos = bytes_to_pos(source_bytes, end)?;
2163 Some(TextEdit {
2164 range: Range {
2165 start: start_pos,
2166 end: end_pos,
2167 },
2168 new_text: String::new(),
2169 })
2170 }
2171
2172 CodeActionKind::DeleteLocalVar => {
2174 let tree = ts_parse(source)?;
2175 let byte = pos_to_bytes(source_bytes, diag_range.start);
2176 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2177
2178 loop {
2179 if node.kind() == "variable_declaration_statement" {
2180 break;
2181 }
2182 node = node.parent()?;
2183 }
2184
2185 let stmt_start = node.start_byte();
2187 let delete_from = if stmt_start > 0 {
2188 let mut i = stmt_start - 1;
2189 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2190 i -= 1;
2191 }
2192 if source_bytes[i] == b'\n' {
2193 i
2194 } else {
2195 stmt_start
2196 }
2197 } else {
2198 stmt_start
2199 };
2200
2201 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2202 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2203 Some(TextEdit {
2204 range: Range {
2205 start: start_pos,
2206 end: end_pos,
2207 },
2208 new_text: String::new(),
2209 })
2210 }
2211
2212 CodeActionKind::DeleteNodeByKind { node_kind } => {
2214 let tree = ts_parse(source)?;
2215 let byte = pos_to_bytes(source_bytes, diag_range.start);
2216 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2217 loop {
2218 if node.kind() == node_kind {
2219 break;
2220 }
2221 node = node.parent()?;
2222 }
2223 let node_start = node.start_byte();
2225 let delete_from = if node_start > 0 {
2226 let mut i = node_start - 1;
2227 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2228 i -= 1;
2229 }
2230 if source_bytes[i] == b'\n' {
2231 i
2232 } else {
2233 node_start
2234 }
2235 } else {
2236 node_start
2237 };
2238 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2239 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2240 Some(TextEdit {
2241 range: Range {
2242 start: start_pos,
2243 end: end_pos,
2244 },
2245 new_text: String::new(),
2246 })
2247 }
2248
2249 CodeActionKind::DeleteChildNode {
2254 walk_to,
2255 child_kinds,
2256 } => {
2257 let tree = ts_parse(source)?;
2258 let byte = pos_to_bytes(source_bytes, diag_range.start);
2259 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2260 loop {
2261 if node.kind() == walk_to {
2262 break;
2263 }
2264 node = node.parent()?;
2265 }
2266 let mut cursor = node.walk();
2267 let children: Vec<_> = node.children(&mut cursor).collect();
2268 let child = child_kinds
2269 .iter()
2270 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2271 let start = child.start_byte();
2272 let end = if child.end_byte() < source_bytes.len()
2273 && source_bytes[child.end_byte()] == b' '
2274 {
2275 child.end_byte() + 1
2276 } else {
2277 child.end_byte()
2278 };
2279 let start_pos = bytes_to_pos(source_bytes, start)?;
2280 let end_pos = bytes_to_pos(source_bytes, end)?;
2281 Some(TextEdit {
2282 range: Range {
2283 start: start_pos,
2284 end: end_pos,
2285 },
2286 new_text: String::new(),
2287 })
2288 }
2289
2290 CodeActionKind::ReplaceChildNode {
2294 walk_to,
2295 child_kind,
2296 replacement,
2297 } => {
2298 let tree = ts_parse(source)?;
2299 let byte = pos_to_bytes(source_bytes, diag_range.start);
2300 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2301 loop {
2302 if node.kind() == walk_to {
2303 break;
2304 }
2305 node = node.parent()?;
2306 }
2307 let mut cursor = node.walk();
2308 let child = node
2309 .children(&mut cursor)
2310 .find(|c| c.kind() == child_kind)?;
2311 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2312 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2313 Some(TextEdit {
2314 range: Range {
2315 start: start_pos,
2316 end: end_pos,
2317 },
2318 new_text: replacement.to_string(),
2319 })
2320 }
2321
2322 CodeActionKind::InsertBeforeNode {
2329 walk_to,
2330 before_child,
2331 text,
2332 } => {
2333 let tree = ts_parse(source)?;
2334 let byte = pos_to_bytes(source_bytes, diag_range.start);
2335 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2336
2337 loop {
2338 if node.kind() == walk_to {
2339 break;
2340 }
2341 node = node.parent()?;
2342 }
2343
2344 let mut cursor = node.walk();
2345 let children: Vec<_> = node.children(&mut cursor).collect();
2346
2347 for target_kind in before_child {
2349 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2350 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2351 return Some(TextEdit {
2352 range: Range {
2353 start: insert_pos,
2354 end: insert_pos,
2355 },
2356 new_text: text.to_string(),
2357 });
2358 }
2359 }
2360 None
2361 }
2362 }
2363}
2364
2365#[cfg(test)]
2366mod ts_tests {
2367 use super::*;
2368
2369 #[test]
2370 fn test_cursor_context_state_var() {
2371 let source = r#"
2372contract Token {
2373 uint256 public totalSupply;
2374 function mint(uint256 amount) public {
2375 totalSupply += amount;
2376 }
2377}
2378"#;
2379 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2381 assert_eq!(ctx.name, "totalSupply");
2382 assert_eq!(ctx.function.as_deref(), Some("mint"));
2383 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2384 }
2385
2386 #[test]
2387 fn test_cursor_context_top_level() {
2388 let source = r#"
2389contract Foo {}
2390contract Bar {}
2391"#;
2392 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2394 assert_eq!(ctx.name, "Foo");
2395 assert!(ctx.function.is_none());
2396 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2398 }
2399
2400 #[test]
2401 fn test_find_declarations() {
2402 let source = r#"
2403contract Token {
2404 uint256 public totalSupply;
2405 function mint(uint256 amount) public {
2406 totalSupply += amount;
2407 }
2408}
2409"#;
2410 let decls = find_declarations_by_name(source, "totalSupply");
2411 assert_eq!(decls.len(), 1);
2412 assert_eq!(decls[0].kind, "state_variable_declaration");
2413 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2414 }
2415
2416 #[test]
2417 fn test_find_declarations_multiple_contracts() {
2418 let source = r#"
2419contract A {
2420 uint256 public value;
2421}
2422contract B {
2423 uint256 public value;
2424}
2425"#;
2426 let decls = find_declarations_by_name(source, "value");
2427 assert_eq!(decls.len(), 2);
2428 assert_eq!(decls[0].container.as_deref(), Some("A"));
2429 assert_eq!(decls[1].container.as_deref(), Some("B"));
2430 }
2431
2432 #[test]
2433 fn test_find_declarations_enum_value() {
2434 let source = "contract Foo { enum Status { Active, Paused } }";
2435 let decls = find_declarations_by_name(source, "Active");
2436 assert_eq!(decls.len(), 1);
2437 assert_eq!(decls[0].kind, "enum_value");
2438 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2439 }
2440
2441 #[test]
2442 fn test_cursor_context_short_param() {
2443 let source = r#"
2444contract Shop {
2445 uint256 public TAX;
2446 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2447 TAX = tax;
2448 }
2449}
2450"#;
2451 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2453 assert_eq!(ctx.name, "tax");
2454 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2455
2456 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2458 assert_eq!(ctx2.name, "TAX");
2459
2460 let decls = find_declarations_by_name(source, "tax");
2462 assert_eq!(decls.len(), 1);
2463 assert_eq!(decls[0].kind, "parameter");
2464
2465 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2466 assert_eq!(decls_tax_base.len(), 1);
2467 assert_eq!(decls_tax_base[0].kind, "parameter");
2468
2469 let decls_price = find_declarations_by_name(source, "price");
2470 assert_eq!(decls_price.len(), 1);
2471 assert_eq!(decls_price[0].kind, "parameter");
2472
2473 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2475 assert_eq!(decls_tax_upper.len(), 1);
2476 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2477 }
2478
2479 #[test]
2480 fn test_delete_child_node_2462_constructor_public() {
2481 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Warning 2462\n\ncontract ConstructorVisibility {\n uint256 public value;\n\n constructor() public {\n value = 1;\n }\n}\n";
2484 let diag_range = Range {
2485 start: Position {
2486 line: 8,
2487 character: 4,
2488 },
2489 end: Position {
2490 line: 10,
2491 character: 5,
2492 },
2493 };
2494 let source_bytes = source.as_bytes();
2495 let tree = ts_parse(source).expect("parse failed");
2496 let byte = pos_to_bytes(source_bytes, diag_range.start);
2497 eprintln!("2462 byte offset: {byte}");
2498 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2499 loop {
2500 eprintln!(
2501 " ancestor: kind={} start={} end={}",
2502 n.kind(),
2503 n.start_byte(),
2504 n.end_byte()
2505 );
2506 if n.kind() == "constructor_definition" {
2507 let mut cursor = n.walk();
2508 for child in n.children(&mut cursor) {
2509 eprintln!(
2510 " child: kind={:?} text={:?}",
2511 child.kind(),
2512 &source[child.start_byte()..child.end_byte()]
2513 );
2514 }
2515 break;
2516 }
2517 match n.parent() {
2518 Some(p) => n = p,
2519 None => break,
2520 }
2521 }
2522 }
2523 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2524 let edit = code_action_edit(
2525 source,
2526 diag_range,
2527 CodeActionKind::DeleteChildNode {
2528 walk_to: "constructor_definition",
2529 child_kinds: &ck,
2530 },
2531 );
2532 assert!(edit.is_some(), "2462 fix returned None");
2533 let edit = edit.unwrap();
2534 assert_eq!(edit.new_text, "");
2535 let lines: Vec<&str> = source.lines().collect();
2536 let deleted = &lines[edit.range.start.line as usize]
2537 [edit.range.start.character as usize..edit.range.end.character as usize];
2538 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2539 }
2540
2541 #[test]
2542 fn test_delete_child_node_9239_constructor_private() {
2543 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Error 9239: Constructor cannot have visibility.\n// Fix: remove the visibility specifier (private/external) from the constructor.\n\ncontract ConstructorPrivateVisibility {\n uint256 public value;\n\n constructor() private {\n value = 1;\n }\n}\n";
2546 let diag_range = Range {
2547 start: Position {
2548 line: 9,
2549 character: 4,
2550 },
2551 end: Position {
2552 line: 11,
2553 character: 5,
2554 },
2555 };
2556 let ck: Vec<&str> = vec!["modifier_invocation"];
2557 let edit = code_action_edit(
2558 source,
2559 diag_range,
2560 CodeActionKind::DeleteChildNode {
2561 walk_to: "constructor_definition",
2562 child_kinds: &ck,
2563 },
2564 );
2565 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2566 let edit = edit.unwrap();
2567 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2569 let lines: Vec<&str> = source.lines().collect();
2571 let line_text = lines[edit.range.start.line as usize];
2572 let deleted =
2573 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2574 assert!(
2575 deleted.contains("private"),
2576 "expected deleted text to contain 'private', got: {:?}",
2577 deleted
2578 );
2579 }
2580
2581 #[test]
2582 fn test_find_best_declaration_same_contract() {
2583 let source = r#"
2584contract A { uint256 public x; }
2585contract B { uint256 public x; }
2586"#;
2587 let ctx = CursorContext {
2588 name: "x".into(),
2589 function: None,
2590 contract: Some("B".into()),
2591 object: None,
2592 arg_count: None,
2593 arg_types: vec![],
2594 };
2595 let uri = Url::parse("file:///test.sol").unwrap();
2596 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2597 assert_eq!(loc.range.start.line, 2);
2599 }
2600}
2601