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 qualifier_refs: HashMap<NodeId, Vec<NodeId>>,
143 pub base_function_implementation: HashMap<NodeId, Vec<NodeId>>,
154}
155
156impl CachedBuild {
157 pub fn new(ast: Value, build_version: i32, interner: Option<&mut PathInterner>) -> Self {
169 let (mut nodes, path_to_abs, mut external_refs) = if let Some(sources) = ast.get("sources")
170 {
171 cache_ids(sources)
172 } else {
173 (HashMap::new(), HashMap::new(), HashMap::new())
174 };
175
176 let solc_id_to_path: HashMap<SolcFileId, String> = ast
178 .get("source_id_to_path")
179 .and_then(|v| v.as_object())
180 .map(|obj| {
181 obj.iter()
182 .map(|(k, v)| {
183 (
184 SolcFileId::new(k.clone()),
185 v.as_str().unwrap_or("").to_string(),
186 )
187 })
188 .collect()
189 })
190 .unwrap_or_default();
191
192 let (id_to_path_map, canonical_remap) = if let Some(interner) = interner {
197 let remap = interner.build_remap(&solc_id_to_path);
198
199 for file_nodes in nodes.values_mut() {
201 for info in file_nodes.values_mut() {
202 canonicalize_node_info(info, &remap);
203 }
204 }
205
206 let old_refs = std::mem::take(&mut external_refs);
208 for (src, decl_id) in old_refs {
209 let new_src = SrcLocation::new(remap_src_canonical(src.as_str(), &remap));
210 external_refs.insert(new_src, decl_id);
211 }
212
213 (interner.to_id_to_path_map(), Some(remap))
215 } else {
216 (solc_id_to_path, None)
218 };
219
220 let doc_index = crate::hover::build_doc_index(&ast);
221
222 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
229 match crate::solc_ast::extract_decl_nodes(sources) {
230 Some(extracted) => (
231 extracted
232 .decl_index
233 .into_iter()
234 .map(|(id, decl)| (NodeId(id), decl))
235 .collect(),
236 extracted
237 .node_id_to_source_path
238 .into_iter()
239 .map(|(id, path)| (NodeId(id), AbsPath::new(path)))
240 .collect(),
241 ),
242 None => (HashMap::new(), HashMap::new()),
243 }
244 } else {
245 (HashMap::new(), HashMap::new())
246 };
247
248 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
250 let hint_index = if let Some(sources) = ast.get("sources") {
251 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
252 } else {
253 HashMap::new()
254 };
255
256 let completion_cache = {
258 let sources = ast.get("sources");
259 let contracts = ast.get("contracts");
260 let cc = if let Some(s) = sources {
261 crate::completion::build_completion_cache(s, contracts, canonical_remap.as_ref())
262 } else {
263 crate::completion::build_completion_cache(
264 &serde_json::Value::Object(Default::default()),
265 contracts,
266 canonical_remap.as_ref(),
267 )
268 };
269 std::sync::Arc::new(cc)
270 };
271
272 let qualifier_refs = build_qualifier_refs(&nodes);
277
278 let base_function_implementation = build_base_function_implementation(&nodes);
282
283 Self {
287 nodes,
288 path_to_abs,
289 external_refs,
290 id_to_path_map,
291 decl_index,
292 node_id_to_source_path,
293 hint_index,
294 doc_index,
295 completion_cache,
296 build_version,
297 qualifier_refs,
298 base_function_implementation,
299 }
300 }
301
302 pub fn merge_missing_from(&mut self, other: &CachedBuild) {
309 for (abs_path, file_nodes) in &other.nodes {
310 if !self.nodes.contains_key(abs_path) {
311 self.nodes.insert(abs_path.clone(), file_nodes.clone());
312 }
313 }
314 for (k, v) in &other.path_to_abs {
315 self.path_to_abs
316 .entry(k.clone())
317 .or_insert_with(|| v.clone());
318 }
319 for (k, v) in &other.external_refs {
320 self.external_refs.entry(k.clone()).or_insert(*v);
321 }
322 for (k, v) in &other.id_to_path_map {
323 self.id_to_path_map
324 .entry(k.clone())
325 .or_insert_with(|| v.clone());
326 }
327 for (container_id, other_qrefs) in &other.qualifier_refs {
330 let entry = self.qualifier_refs.entry(*container_id).or_default();
331 for &qnode_id in other_qrefs {
332 if !entry.contains(&qnode_id) {
333 entry.push(qnode_id);
334 }
335 }
336 }
337 for (node_id, other_impls) in &other.base_function_implementation {
340 let entry = self
341 .base_function_implementation
342 .entry(*node_id)
343 .or_default();
344 for &impl_id in other_impls {
345 if !entry.contains(&impl_id) {
346 entry.push(impl_id);
347 }
348 }
349 }
350 }
351
352 pub fn from_reference_index(
361 nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
362 path_to_abs: HashMap<RelPath, AbsPath>,
363 external_refs: ExternalRefs,
364 id_to_path_map: HashMap<SolcFileId, String>,
365 build_version: i32,
366 interner: Option<&mut PathInterner>,
367 ) -> Self {
368 if let Some(interner) = interner {
371 for path in id_to_path_map.values() {
372 interner.intern(path);
373 }
374 }
375
376 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
377 &serde_json::Value::Object(Default::default()),
378 None,
379 None,
380 ));
381
382 let qualifier_refs = build_qualifier_refs(&nodes);
384
385 let base_function_implementation = build_base_function_implementation(&nodes);
388
389 Self {
390 nodes,
391 path_to_abs,
392 external_refs,
393 id_to_path_map,
394 decl_index: HashMap::new(),
395 node_id_to_source_path: HashMap::new(),
396 hint_index: HashMap::new(),
397 doc_index: HashMap::new(),
398 completion_cache,
399 build_version,
400 qualifier_refs,
401 base_function_implementation,
402 }
403 }
404}
405
406fn build_qualifier_refs(
416 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
417) -> HashMap<NodeId, Vec<NodeId>> {
418 let mut node_scope: HashMap<NodeId, NodeId> = HashMap::new();
421 for file_nodes in nodes.values() {
422 for (id, info) in file_nodes {
423 if let Some(scope_id) = info.scope {
424 node_scope.insert(*id, scope_id);
425 }
426 }
427 }
428
429 let mut qualifier_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
432 for file_nodes in nodes.values() {
433 for (id, info) in file_nodes {
434 if info.name_locations.len() > 1 && info.node_type.as_deref() == Some("IdentifierPath")
435 {
436 if let Some(ref_decl) = info.referenced_declaration
437 && let Some(&scope_id) = node_scope.get(&ref_decl)
438 {
439 qualifier_refs.entry(scope_id).or_default().push(*id);
440 }
441 }
442 }
443 }
444
445 qualifier_refs
446}
447
448fn build_base_function_implementation(
458 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
459) -> HashMap<NodeId, Vec<NodeId>> {
460 let mut index: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
461
462 for file_nodes in nodes.values() {
463 for (id, info) in file_nodes {
464 if info.base_functions.is_empty() {
465 continue;
466 }
467 for &base_id in &info.base_functions {
469 index.entry(*id).or_default().push(base_id);
470 }
471 for &base_id in &info.base_functions {
473 index.entry(base_id).or_default().push(*id);
474 }
475 }
476 }
477
478 for ids in index.values_mut() {
480 ids.sort_unstable();
481 ids.dedup();
482 }
483
484 index
485}
486
487type CachedIds = (
489 HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
490 HashMap<RelPath, AbsPath>,
491 ExternalRefs,
492);
493
494pub fn remap_src_canonical(src: &str, remap: &HashMap<u64, FileId>) -> String {
498 let Some(last_colon) = src.rfind(':') else {
499 return src.to_owned();
500 };
501 let old_id_str = &src[last_colon + 1..];
502 let Ok(old_id) = old_id_str.parse::<u64>() else {
503 return src.to_owned();
504 };
505 let Some(canonical) = remap.get(&old_id) else {
506 return src.to_owned();
507 };
508 if canonical.0 == old_id {
509 return src.to_owned();
510 }
511 format!("{}{}", &src[..=last_colon], canonical.0)
512}
513
514fn canonicalize_node_info(info: &mut NodeInfo, remap: &HashMap<u64, FileId>) {
517 info.src = SrcLocation::new(remap_src_canonical(info.src.as_str(), remap));
518 if let Some(loc) = info.name_location.as_mut() {
519 *loc = remap_src_canonical(loc, remap);
520 }
521 for loc in &mut info.name_locations {
522 *loc = remap_src_canonical(loc, remap);
523 }
524 if let Some(loc) = info.member_location.as_mut() {
525 *loc = remap_src_canonical(loc, remap);
526 }
527}
528
529pub fn cache_ids(sources: &Value) -> CachedIds {
530 let source_count = sources.as_object().map_or(0, |obj| obj.len());
531
532 let mut nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>> =
535 HashMap::with_capacity(source_count);
536 let mut path_to_abs: HashMap<RelPath, AbsPath> = HashMap::with_capacity(source_count);
537 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
538
539 if let Some(sources_obj) = sources.as_object() {
540 for (path, source_data) in sources_obj {
541 if let Some(ast) = source_data.get("ast") {
542 let abs_path = AbsPath::new(
544 ast.get("absolutePath")
545 .and_then(|v| v.as_str())
546 .unwrap_or(path)
547 .to_string(),
548 );
549
550 path_to_abs.insert(RelPath::new(path), abs_path.clone());
551
552 let size_hint = ast
557 .get("nodes")
558 .and_then(|v| v.as_array())
559 .map_or(64, |arr| arr.len() * 8);
560 if !nodes.contains_key(&abs_path) {
561 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
562 }
563
564 if let Some(id) = ast.get("id").and_then(|v| v.as_i64())
565 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
566 {
567 nodes.get_mut(&abs_path).unwrap().insert(
568 NodeId(id),
569 NodeInfo {
570 src: SrcLocation::new(src),
571 name_location: None,
572 name_locations: vec![],
573 referenced_declaration: None,
574 node_type: ast
575 .get("nodeType")
576 .and_then(|v| v.as_str())
577 .map(|s| s.to_string()),
578 member_location: None,
579 absolute_path: ast
580 .get("absolutePath")
581 .and_then(|v| v.as_str())
582 .map(|s| s.to_string()),
583 scope: ast.get("scope").and_then(|v| v.as_i64()).map(NodeId),
584 base_functions: vec![],
585 },
586 );
587 }
588
589 let mut stack = vec![ast];
590
591 while let Some(tree) = stack.pop() {
592 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_i64())
593 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
594 {
595 let id = NodeId(raw_id);
596 let mut name_location = tree
598 .get("nameLocation")
599 .and_then(|v| v.as_str())
600 .map(|s| s.to_string());
601
602 if name_location.is_none()
606 && let Some(name_locations) = tree.get("nameLocations")
607 && let Some(locations_array) = name_locations.as_array()
608 && !locations_array.is_empty()
609 {
610 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
611 if node_type == Some("IdentifierPath") {
612 name_location = locations_array
613 .last()
614 .and_then(|v| v.as_str())
615 .map(|s| s.to_string());
616 } else {
617 name_location = locations_array[0].as_str().map(|s| s.to_string());
618 }
619 }
620
621 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
622 && let Some(locations_array) = name_locations.as_array()
623 {
624 locations_array
625 .iter()
626 .filter_map(|v| v.as_str().map(|s| s.to_string()))
627 .collect()
628 } else {
629 vec![]
630 };
631
632 let mut final_name_location = name_location;
633 if final_name_location.is_none()
634 && let Some(member_loc) =
635 tree.get("memberLocation").and_then(|v| v.as_str())
636 {
637 final_name_location = Some(member_loc.to_string());
638 }
639
640 let node_info = NodeInfo {
641 src: SrcLocation::new(src),
642 name_location: final_name_location,
643 name_locations,
644 referenced_declaration: tree
645 .get("referencedDeclaration")
646 .and_then(|v| v.as_i64())
647 .map(NodeId),
648 node_type: tree
649 .get("nodeType")
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string()),
652 member_location: tree
653 .get("memberLocation")
654 .and_then(|v| v.as_str())
655 .map(|s| s.to_string()),
656 absolute_path: tree
657 .get("absolutePath")
658 .and_then(|v| v.as_str())
659 .map(|s| s.to_string()),
660 scope: tree.get("scope").and_then(|v| v.as_i64()).map(NodeId),
661 base_functions: tree
662 .get("baseFunctions")
663 .and_then(|v| v.as_array())
664 .map(|arr| {
665 arr.iter().filter_map(|v| v.as_i64().map(NodeId)).collect()
666 })
667 .unwrap_or_default(),
668 };
669
670 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
671
672 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
674 && let Some(ext_refs) =
675 tree.get("externalReferences").and_then(|v| v.as_array())
676 {
677 for ext_ref in ext_refs {
678 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
679 && let Some(decl_id) =
680 ext_ref.get("declaration").and_then(|v| v.as_i64())
681 {
682 external_refs
683 .insert(SrcLocation::new(src_str), NodeId(decl_id));
684 }
685 }
686 }
687 }
688
689 for key in CHILD_KEYS {
690 push_if_node_or_array(tree, key, &mut stack);
691 }
692 }
693 }
694 }
695 }
696
697 (nodes, path_to_abs, external_refs)
698}
699
700pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
701 let text = String::from_utf8_lossy(source_bytes);
702 crate::utils::position_to_byte_offset(&text, position)
703}
704
705pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
706 let text = String::from_utf8_lossy(source_bytes);
707 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
708 Some(pos)
709}
710
711pub fn src_to_location(
713 src: &str,
714 id_to_path: &HashMap<crate::types::SolcFileId, String>,
715) -> Option<Location> {
716 let loc = SourceLoc::parse(src)?;
717 let file_path = id_to_path.get(&loc.file_id_str())?;
718
719 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
720 std::path::PathBuf::from(file_path)
721 } else {
722 std::env::current_dir().ok()?.join(file_path)
723 };
724
725 let source_bytes = std::fs::read(&absolute_path).ok()?;
726 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
727 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
728 let uri = Url::from_file_path(&absolute_path).ok()?;
729
730 Some(Location {
731 uri,
732 range: Range {
733 start: start_pos,
734 end: end_pos,
735 },
736 })
737}
738
739pub fn goto_bytes(
740 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
741 path_to_abs: &HashMap<RelPath, AbsPath>,
742 id_to_path: &HashMap<crate::types::SolcFileId, String>,
743 external_refs: &ExternalRefs,
744 uri: &str,
745 position: usize,
746) -> Option<(String, usize, usize)> {
747 let path = match uri.starts_with("file://") {
748 true => &uri[7..],
749 false => uri,
750 };
751
752 let abs_path = path_to_abs.get(path)?;
754
755 let current_file_nodes = nodes.get(abs_path)?;
757
758 let path_to_file_id: HashMap<&str, &crate::types::SolcFileId> =
760 id_to_path.iter().map(|(id, p)| (p.as_str(), id)).collect();
761
762 let current_file_id = path_to_file_id.get(abs_path.as_str());
766
767 for (src_str, decl_id) in external_refs {
769 let Some(src_loc) = SourceLoc::parse(src_str.as_str()) else {
770 continue;
771 };
772
773 if let Some(file_id) = current_file_id {
775 if src_loc.file_id_str() != **file_id {
776 continue;
777 }
778 } else {
779 continue;
780 }
781
782 if src_loc.offset <= position && position < src_loc.end() {
783 let mut target_node: Option<&NodeInfo> = None;
785 for file_nodes in nodes.values() {
786 if let Some(node) = file_nodes.get(decl_id) {
787 target_node = Some(node);
788 break;
789 }
790 }
791 let node = target_node?;
792 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
793 let loc = SourceLoc::parse(loc_str)?;
794 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
795 return Some((file_path, loc.offset, loc.length));
796 }
797 }
798
799 let mut refs = HashMap::new();
800
801 for (id, content) in current_file_nodes {
803 if content.referenced_declaration.is_none() {
804 continue;
805 }
806
807 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
808 continue;
809 };
810
811 if src_loc.offset <= position && position < src_loc.end() {
812 let diff = src_loc.length;
813 if !refs.contains_key(&diff) || refs[&diff] <= *id {
814 refs.insert(diff, *id);
815 }
816 }
817 }
818
819 if refs.is_empty() {
820 let tmp = current_file_nodes.iter();
823 for (_id, content) in tmp {
824 if content.node_type == Some("ImportDirective".to_string()) {
825 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
826 continue;
827 };
828
829 if src_loc.offset <= position
830 && position < src_loc.end()
831 && let Some(import_path) = &content.absolute_path
832 {
833 return Some((import_path.clone(), 0, 0));
834 }
835 }
836 }
837 return None;
838 }
839
840 let min_diff = *refs.keys().min()?;
842 let chosen_id = refs[&min_diff];
843 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
844
845 let mut target_node: Option<&NodeInfo> = None;
847 for file_nodes in nodes.values() {
848 if let Some(node) = file_nodes.get(&ref_id) {
849 target_node = Some(node);
850 break;
851 }
852 }
853
854 let node = target_node?;
855
856 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
858 let loc = SourceLoc::parse(loc_str)?;
859 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
860
861 Some((file_path, loc.offset, loc.length))
862}
863
864fn resolve_qualifier_goto(
869 build: &CachedBuild,
870 file_uri: &Url,
871 byte_position: usize,
872) -> Option<Location> {
873 let path = file_uri.to_file_path().ok()?;
874 let path_str = path.to_str()?;
875 let abs_path = build.path_to_abs.get(path_str)?;
876 let file_nodes = build.nodes.get(abs_path)?;
877
878 let node_id = crate::references::byte_to_id(&build.nodes, abs_path, byte_position)?;
880 let node_info = file_nodes.get(&node_id)?;
881
882 if node_info.node_type.as_deref() != Some("IdentifierPath")
884 || node_info.name_locations.len() <= 1
885 {
886 return None;
887 }
888
889 let first_loc = SourceLoc::parse(&node_info.name_locations[0])?;
891 if byte_position < first_loc.offset || byte_position >= first_loc.end() {
892 return None;
893 }
894
895 let ref_decl_id = node_info.referenced_declaration?;
897 let decl_node = find_node_info(&build.nodes, ref_decl_id)?;
899 let scope_id = decl_node.scope?;
900
901 let container_node = find_node_info(&build.nodes, scope_id)?;
903 let loc_str = container_node
904 .name_location
905 .as_deref()
906 .unwrap_or(container_node.src.as_str());
907 let loc = SourceLoc::parse(loc_str)?;
908 let file_path = build.id_to_path_map.get(&loc.file_id_str())?;
909
910 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
911 std::path::PathBuf::from(file_path)
912 } else {
913 std::env::current_dir().ok()?.join(file_path)
914 };
915
916 let target_bytes = std::fs::read(&absolute_path).ok()?;
917 let start_pos = bytes_to_pos(&target_bytes, loc.offset)?;
918 let end_pos = bytes_to_pos(&target_bytes, loc.end())?;
919 let target_uri = Url::from_file_path(&absolute_path).ok()?;
920
921 Some(Location {
922 uri: target_uri,
923 range: Range {
924 start: start_pos,
925 end: end_pos,
926 },
927 })
928}
929
930fn find_node_info<'a>(
932 nodes: &'a HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
933 node_id: NodeId,
934) -> Option<&'a NodeInfo> {
935 for file_nodes in nodes.values() {
936 if let Some(node) = file_nodes.get(&node_id) {
937 return Some(node);
938 }
939 }
940 None
941}
942
943pub fn goto_declaration_cached(
946 build: &CachedBuild,
947 file_uri: &Url,
948 position: Position,
949 source_bytes: &[u8],
950) -> Option<Location> {
951 let byte_position = pos_to_bytes(source_bytes, position);
952
953 if let Some(location) = resolve_qualifier_goto(build, file_uri, byte_position) {
958 return Some(location);
959 }
960
961 if let Some((file_path, location_bytes, length)) = goto_bytes(
962 &build.nodes,
963 &build.path_to_abs,
964 &build.id_to_path_map,
965 &build.external_refs,
966 file_uri.as_ref(),
967 byte_position,
968 ) {
969 let target_file_path = std::path::Path::new(&file_path);
970 let absolute_path = if target_file_path.is_absolute() {
971 target_file_path.to_path_buf()
972 } else {
973 let base = file_uri
978 .to_file_path()
979 .ok()
980 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
981 .or_else(|| std::env::current_dir().ok())
982 .unwrap_or_default();
983 base.join(target_file_path)
984 };
985
986 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
987 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
988 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
989 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
990 {
991 return Some(Location {
992 uri: target_uri,
993 range: Range {
994 start: start_pos,
995 end: end_pos,
996 },
997 });
998 }
999 };
1000
1001 None
1002}
1003
1004pub fn goto_declaration_by_name(
1015 cached_build: &CachedBuild,
1016 file_uri: &Url,
1017 name: &str,
1018 byte_hint: usize,
1019) -> Option<Location> {
1020 let path = match file_uri.as_ref().starts_with("file://") {
1021 true => &file_uri.as_ref()[7..],
1022 false => file_uri.as_ref(),
1023 };
1024 let abs_path = cached_build.path_to_abs.get(path)?;
1025 let built_source = std::fs::read_to_string(abs_path).ok()?;
1027
1028 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
1030
1031 let tmp = {
1032 let this = cached_build.nodes.get(abs_path)?;
1033 this.iter()
1034 };
1035 for (_id, node) in tmp {
1036 let ref_id = match node.referenced_declaration {
1037 Some(id) => id,
1038 None => continue,
1039 };
1040
1041 let Some(src_loc) = SourceLoc::parse(node.src.as_str()) else {
1043 continue;
1044 };
1045 let start = src_loc.offset;
1046 let length = src_loc.length;
1047
1048 if start + length > built_source.len() {
1049 continue;
1050 }
1051
1052 let node_text = &built_source[start..start + length];
1053
1054 let matches = node_text == name
1059 || node_text.contains(&format!(".{name}("))
1060 || node_text.ends_with(&format!(".{name}"));
1061
1062 if matches {
1063 let distance = if byte_hint >= start && byte_hint < start + length {
1067 0 } else if byte_hint < start {
1069 start - byte_hint
1070 } else {
1071 byte_hint - (start + length)
1072 };
1073 candidates.push((distance, length, ref_id));
1074 }
1075 }
1076
1077 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1079 let ref_id = candidates.first()?.2;
1080
1081 let mut target_node: Option<&NodeInfo> = None;
1083 for file_nodes in cached_build.nodes.values() {
1084 if let Some(node) = file_nodes.get(&ref_id) {
1085 target_node = Some(node);
1086 break;
1087 }
1088 }
1089
1090 let node = target_node?;
1091
1092 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1094 let loc = SourceLoc::parse(loc_str)?;
1095
1096 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1097 let location_bytes = loc.offset;
1098 let length = loc.length;
1099
1100 let target_file_path = std::path::Path::new(file_path);
1101 let absolute_path = if target_file_path.is_absolute() {
1102 target_file_path.to_path_buf()
1103 } else {
1104 let base = file_uri
1105 .to_file_path()
1106 .ok()
1107 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1108 .or_else(|| std::env::current_dir().ok())
1109 .unwrap_or_default();
1110 base.join(target_file_path)
1111 };
1112
1113 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1114 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1115 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1116 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1117
1118 Some(Location {
1119 uri: target_uri,
1120 range: Range {
1121 start: start_pos,
1122 end: end_pos,
1123 },
1124 })
1125}
1126
1127#[derive(Debug, Clone)]
1131pub struct CursorContext {
1132 pub name: String,
1134 pub function: Option<String>,
1136 pub contract: Option<String>,
1138 pub object: Option<String>,
1142 pub arg_count: Option<usize>,
1145 pub arg_types: Vec<Option<String>>,
1148}
1149
1150fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1152 let mut parser = Parser::new();
1153 parser
1154 .set_language(&tree_sitter_solidity::LANGUAGE.into())
1155 .expect("failed to load Solidity grammar");
1156 parser.parse(source, None)
1157}
1158
1159pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1165 let line = location.range.start.line as usize;
1166 let start_col = location.range.start.character as usize;
1167 let end_col = location.range.end.character as usize;
1168
1169 if let Some(line_text) = target_source.lines().nth(line)
1170 && end_col <= line_text.len()
1171 {
1172 return &line_text[start_col..end_col] == expected_name;
1173 }
1174 true
1176}
1177
1178fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1180 if byte < node.start_byte() || byte >= node.end_byte() {
1181 return None;
1182 }
1183 let mut cursor = node.walk();
1184 for child in node.children(&mut cursor) {
1185 if child.start_byte() <= byte
1186 && byte < child.end_byte()
1187 && let Some(deeper) = ts_node_at_byte(child, byte)
1188 {
1189 return Some(deeper);
1190 }
1191 }
1192 Some(node)
1193}
1194
1195fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1197 let mut cursor = node.walk();
1198 node.children(&mut cursor)
1199 .find(|c| c.kind() == "identifier" && c.is_named())
1200 .map(|c| &source[c.byte_range()])
1201}
1202
1203fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1209 let expr = if arg_node.kind() == "call_argument" {
1211 let mut c = arg_node.walk();
1212 arg_node.children(&mut c).find(|ch| ch.is_named())?
1213 } else {
1214 arg_node
1215 };
1216
1217 match expr.kind() {
1218 "identifier" => {
1219 let var_name = &source[expr.byte_range()];
1220 find_variable_type(expr, source, var_name)
1222 }
1223 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1224 "boolean_literal" => Some("bool".into()),
1225 "string_literal" | "hex_string_literal" => Some("string".into()),
1226 _ => None,
1227 }
1228}
1229
1230fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1235 let mut scope = from.parent();
1236 while let Some(node) = scope {
1237 match node.kind() {
1238 "function_definition" | "modifier_definition" | "constructor_definition" => {
1239 let mut c = node.walk();
1241 for child in node.children(&mut c) {
1242 if child.kind() == "parameter"
1243 && let Some(id) = ts_child_id_text(child, source)
1244 && id == var_name
1245 {
1246 let mut pc = child.walk();
1248 return child
1249 .children(&mut pc)
1250 .find(|c| {
1251 matches!(
1252 c.kind(),
1253 "type_name"
1254 | "primitive_type"
1255 | "user_defined_type"
1256 | "mapping"
1257 )
1258 })
1259 .map(|t| source[t.byte_range()].trim().to_string());
1260 }
1261 }
1262 }
1263 "function_body" | "block_statement" | "unchecked_block" => {
1264 let mut c = node.walk();
1266 for child in node.children(&mut c) {
1267 if (child.kind() == "variable_declaration_statement"
1268 || child.kind() == "variable_declaration")
1269 && let Some(id) = ts_child_id_text(child, source)
1270 && id == var_name
1271 {
1272 let mut pc = child.walk();
1273 return child
1274 .children(&mut pc)
1275 .find(|c| {
1276 matches!(
1277 c.kind(),
1278 "type_name"
1279 | "primitive_type"
1280 | "user_defined_type"
1281 | "mapping"
1282 )
1283 })
1284 .map(|t| source[t.byte_range()].trim().to_string());
1285 }
1286 }
1287 }
1288 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1289 if let Some(body) = ts_find_child(node, "contract_body") {
1291 let mut c = body.walk();
1292 for child in body.children(&mut c) {
1293 if child.kind() == "state_variable_declaration"
1294 && let Some(id) = ts_child_id_text(child, source)
1295 && id == var_name
1296 {
1297 let mut pc = child.walk();
1298 return child
1299 .children(&mut pc)
1300 .find(|c| {
1301 matches!(
1302 c.kind(),
1303 "type_name"
1304 | "primitive_type"
1305 | "user_defined_type"
1306 | "mapping"
1307 )
1308 })
1309 .map(|t| source[t.byte_range()].trim().to_string());
1310 }
1311 }
1312 }
1313 }
1314 _ => {}
1315 }
1316 scope = node.parent();
1317 }
1318 None
1319}
1320
1321fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1323 let mut cursor = call_node.walk();
1324 call_node
1325 .children(&mut cursor)
1326 .filter(|c| c.kind() == "call_argument")
1327 .map(|arg| infer_argument_type(arg, source))
1328 .collect()
1329}
1330
1331fn best_overload<'a>(
1339 decls: &'a [TsDeclaration],
1340 arg_count: Option<usize>,
1341 arg_types: &[Option<String>],
1342) -> Option<&'a TsDeclaration> {
1343 if decls.len() == 1 {
1344 return decls.first();
1345 }
1346 if decls.is_empty() {
1347 return None;
1348 }
1349
1350 let func_decls: Vec<&TsDeclaration> =
1352 decls.iter().filter(|d| d.param_count.is_some()).collect();
1353
1354 if func_decls.is_empty() {
1355 return decls.first();
1356 }
1357
1358 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1360 let matched: Vec<_> = func_decls
1361 .iter()
1362 .filter(|d| d.param_count == Some(ac))
1363 .collect();
1364 if matched.len() == 1 {
1365 return Some(matched[0]);
1366 }
1367 if matched.is_empty() {
1368 func_decls.iter().collect()
1370 } else {
1371 matched
1372 }
1373 } else {
1374 func_decls.iter().collect()
1375 };
1376
1377 if !arg_types.is_empty() {
1379 let mut best: Option<(&TsDeclaration, usize)> = None;
1380 for &&decl in &count_matched {
1381 let score = arg_types
1382 .iter()
1383 .zip(decl.param_types.iter())
1384 .filter(|(arg_ty, param_ty)| {
1385 if let Some(at) = arg_ty {
1386 at == param_ty.as_str()
1387 } else {
1388 false
1389 }
1390 })
1391 .count();
1392 if best.is_none() || score > best.unwrap().1 {
1393 best = Some((decl, score));
1394 }
1395 }
1396 if let Some((decl, _)) = best {
1397 return Some(decl);
1398 }
1399 }
1400
1401 count_matched.first().map(|d| **d).or(decls.first())
1403}
1404
1405pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1409 let tree = ts_parse(source)?;
1410 let byte = pos_to_bytes(source.as_bytes(), position);
1411 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1412
1413 let id_node = if leaf.kind() == "identifier" {
1415 leaf
1416 } else {
1417 let parent = leaf.parent()?;
1419 if parent.kind() == "identifier" {
1420 parent
1421 } else {
1422 return None;
1423 }
1424 };
1425
1426 let name = source[id_node.byte_range()].to_string();
1427 let mut function = None;
1428 let mut contract = None;
1429
1430 let object = id_node.parent().and_then(|parent| {
1434 if parent.kind() == "member_expression" {
1435 let prop = parent.child_by_field_name("property")?;
1436 if prop.id() == id_node.id() {
1438 let obj = parent.child_by_field_name("object")?;
1439 Some(source[obj.byte_range()].to_string())
1440 } else {
1441 None
1442 }
1443 } else {
1444 None
1445 }
1446 });
1447
1448 let (arg_count, arg_types) = {
1452 let mut node = id_node.parent();
1453 let mut result = (None, vec![]);
1454 while let Some(n) = node {
1455 if n.kind() == "call_expression" {
1456 let types = infer_call_arg_types(n, source);
1457 result = (Some(types.len()), types);
1458 break;
1459 }
1460 node = n.parent();
1461 }
1462 result
1463 };
1464
1465 let mut current = id_node.parent();
1467 while let Some(node) = current {
1468 match node.kind() {
1469 "function_definition" | "modifier_definition" if function.is_none() => {
1470 function = ts_child_id_text(node, source).map(String::from);
1471 }
1472 "constructor_definition" if function.is_none() => {
1473 function = Some("constructor".into());
1474 }
1475 "contract_declaration" | "interface_declaration" | "library_declaration"
1476 if contract.is_none() =>
1477 {
1478 contract = ts_child_id_text(node, source).map(String::from);
1479 }
1480 _ => {}
1481 }
1482 current = node.parent();
1483 }
1484
1485 Some(CursorContext {
1486 name,
1487 function,
1488 contract,
1489 object,
1490 arg_count,
1491 arg_types,
1492 })
1493}
1494
1495#[derive(Debug, Clone)]
1497pub struct TsDeclaration {
1498 pub range: Range,
1500 pub kind: &'static str,
1502 pub container: Option<String>,
1504 pub param_count: Option<usize>,
1506 pub param_types: Vec<String>,
1509}
1510
1511pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1516 let tree = match ts_parse(source) {
1517 Some(t) => t,
1518 None => return vec![],
1519 };
1520 let mut results = Vec::new();
1521 collect_declarations(tree.root_node(), source, name, None, &mut results);
1522 results
1523}
1524
1525fn collect_declarations(
1526 node: Node,
1527 source: &str,
1528 name: &str,
1529 container: Option<&str>,
1530 out: &mut Vec<TsDeclaration>,
1531) {
1532 let mut cursor = node.walk();
1533 for child in node.children(&mut cursor) {
1534 if !child.is_named() {
1535 continue;
1536 }
1537 match child.kind() {
1538 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1539 if let Some(id_name) = ts_child_id_text(child, source) {
1540 if id_name == name {
1541 out.push(TsDeclaration {
1542 range: id_range(child),
1543 kind: child.kind(),
1544 container: container.map(String::from),
1545 param_count: None,
1546 param_types: vec![],
1547 });
1548 }
1549 if let Some(body) = ts_find_child(child, "contract_body") {
1551 collect_declarations(body, source, name, Some(id_name), out);
1552 }
1553 }
1554 }
1555 "function_definition" | "modifier_definition" => {
1556 if let Some(id_name) = ts_child_id_text(child, source) {
1557 if id_name == name {
1558 let types = parameter_type_signature(child, source);
1559 out.push(TsDeclaration {
1560 range: id_range(child),
1561 kind: child.kind(),
1562 container: container.map(String::from),
1563 param_count: Some(types.len()),
1564 param_types: types.into_iter().map(String::from).collect(),
1565 });
1566 }
1567 collect_parameters(child, source, name, container, out);
1569 if let Some(body) = ts_find_child(child, "function_body") {
1571 collect_declarations(body, source, name, container, out);
1572 }
1573 }
1574 }
1575 "constructor_definition" => {
1576 if name == "constructor" {
1577 let types = parameter_type_signature(child, source);
1578 out.push(TsDeclaration {
1579 range: ts_range(child),
1580 kind: "constructor_definition",
1581 container: container.map(String::from),
1582 param_count: Some(types.len()),
1583 param_types: types.into_iter().map(String::from).collect(),
1584 });
1585 }
1586 collect_parameters(child, source, name, container, out);
1588 if let Some(body) = ts_find_child(child, "function_body") {
1589 collect_declarations(body, source, name, container, out);
1590 }
1591 }
1592 "state_variable_declaration" | "variable_declaration" => {
1593 if let Some(id_name) = ts_child_id_text(child, source)
1594 && id_name == name
1595 {
1596 out.push(TsDeclaration {
1597 range: id_range(child),
1598 kind: child.kind(),
1599 container: container.map(String::from),
1600 param_count: None,
1601 param_types: vec![],
1602 });
1603 }
1604 }
1605 "struct_declaration" => {
1606 if let Some(id_name) = ts_child_id_text(child, source) {
1607 if id_name == name {
1608 out.push(TsDeclaration {
1609 range: id_range(child),
1610 kind: "struct_declaration",
1611 container: container.map(String::from),
1612 param_count: None,
1613 param_types: vec![],
1614 });
1615 }
1616 if let Some(body) = ts_find_child(child, "struct_body") {
1617 collect_declarations(body, source, name, Some(id_name), out);
1618 }
1619 }
1620 }
1621 "enum_declaration" => {
1622 if let Some(id_name) = ts_child_id_text(child, source) {
1623 if id_name == name {
1624 out.push(TsDeclaration {
1625 range: id_range(child),
1626 kind: "enum_declaration",
1627 container: container.map(String::from),
1628 param_count: None,
1629 param_types: vec![],
1630 });
1631 }
1632 if let Some(body) = ts_find_child(child, "enum_body") {
1634 let mut ecur = body.walk();
1635 for val in body.children(&mut ecur) {
1636 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1637 out.push(TsDeclaration {
1638 range: ts_range(val),
1639 kind: "enum_value",
1640 container: Some(id_name.to_string()),
1641 param_count: None,
1642 param_types: vec![],
1643 });
1644 }
1645 }
1646 }
1647 }
1648 }
1649 "event_definition" | "error_declaration" => {
1650 if let Some(id_name) = ts_child_id_text(child, source)
1651 && id_name == name
1652 {
1653 out.push(TsDeclaration {
1654 range: id_range(child),
1655 kind: child.kind(),
1656 container: container.map(String::from),
1657 param_count: None,
1658 param_types: vec![],
1659 });
1660 }
1661 }
1662 "user_defined_type_definition" => {
1663 if let Some(id_name) = ts_child_id_text(child, source)
1664 && id_name == name
1665 {
1666 out.push(TsDeclaration {
1667 range: id_range(child),
1668 kind: "user_defined_type_definition",
1669 container: container.map(String::from),
1670 param_count: None,
1671 param_types: vec![],
1672 });
1673 }
1674 }
1675 _ => {
1677 collect_declarations(child, source, name, container, out);
1678 }
1679 }
1680 }
1681}
1682
1683fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1689 let mut cursor = node.walk();
1690 node.children(&mut cursor)
1691 .filter(|c| c.kind() == "parameter")
1692 .filter_map(|param| {
1693 let mut pc = param.walk();
1694 param
1695 .children(&mut pc)
1696 .find(|c| {
1697 matches!(
1698 c.kind(),
1699 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1700 )
1701 })
1702 .map(|t| source[t.byte_range()].trim())
1703 })
1704 .collect()
1705}
1706
1707fn collect_parameters(
1709 node: Node,
1710 source: &str,
1711 name: &str,
1712 container: Option<&str>,
1713 out: &mut Vec<TsDeclaration>,
1714) {
1715 let mut cursor = node.walk();
1716 for child in node.children(&mut cursor) {
1717 if child.kind() == "parameter"
1718 && let Some(id_name) = ts_child_id_text(child, source)
1719 && id_name == name
1720 {
1721 out.push(TsDeclaration {
1722 range: id_range(child),
1723 kind: "parameter",
1724 container: container.map(String::from),
1725 param_count: None,
1726 param_types: vec![],
1727 });
1728 }
1729 }
1730}
1731
1732fn ts_range(node: Node) -> Range {
1734 let s = node.start_position();
1735 let e = node.end_position();
1736 Range {
1737 start: Position::new(s.row as u32, s.column as u32),
1738 end: Position::new(e.row as u32, e.column as u32),
1739 }
1740}
1741
1742fn id_range(node: Node) -> Range {
1744 let mut cursor = node.walk();
1745 node.children(&mut cursor)
1746 .find(|c| c.kind() == "identifier" && c.is_named())
1747 .map(|c| ts_range(c))
1748 .unwrap_or_else(|| ts_range(node))
1749}
1750
1751fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1752 let mut cursor = node.walk();
1753 node.children(&mut cursor).find(|c| c.kind() == kind)
1754}
1755
1756pub fn goto_definition_ts(
1764 source: &str,
1765 position: Position,
1766 file_uri: &Url,
1767 completion_cache: &crate::completion::CompletionCache,
1768 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1769) -> Option<Location> {
1770 let ctx = cursor_context(source, position)?;
1771
1772 if let Some(obj_name) = &ctx.object {
1777 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1778 let target_source = read_target_source(&path, text_cache)?;
1779 let target_uri = Url::from_file_path(&path).ok()?;
1780 let decls = find_declarations_by_name(&target_source, &ctx.name);
1781 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1782 return Some(Location {
1783 uri: target_uri,
1784 range: d.range,
1785 });
1786 }
1787 }
1788 let decls = find_declarations_by_name(source, &ctx.name);
1790 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1791 return Some(Location {
1792 uri: file_uri.clone(),
1793 range: d.range,
1794 });
1795 }
1796 }
1797
1798 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1801
1802 match resolved {
1803 Some(ResolvedTarget::SameFile) => {
1804 find_best_declaration(source, &ctx, file_uri)
1806 }
1807 Some(ResolvedTarget::OtherFile { path, name }) => {
1808 let target_source = read_target_source(&path, text_cache);
1810 let target_source = target_source?;
1811 let target_uri = Url::from_file_path(&path).ok()?;
1812 let decls = find_declarations_by_name(&target_source, &name);
1813 decls.first().map(|d| Location {
1814 uri: target_uri,
1815 range: d.range,
1816 })
1817 }
1818 None => {
1819 find_best_declaration(source, &ctx, file_uri)
1821 }
1822 }
1823}
1824
1825#[derive(Debug)]
1826enum ResolvedTarget {
1827 SameFile,
1829 OtherFile { path: String, name: String },
1831}
1832
1833fn resolve_via_cache(
1839 ctx: &CursorContext,
1840 file_uri: &Url,
1841 cache: &crate::completion::CompletionCache,
1842) -> Option<ResolvedTarget> {
1843 let contract_scope = ctx
1845 .contract
1846 .as_ref()
1847 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1848 .copied();
1849
1850 if let Some(contract_id) = contract_scope {
1852 if let Some(func_name) = &ctx.function {
1854 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1857 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1859 && decls.iter().any(|d| d.name == ctx.name)
1860 {
1861 return Some(ResolvedTarget::SameFile);
1862 }
1863 }
1864 }
1865
1866 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1868 && decls.iter().any(|d| d.name == ctx.name)
1869 {
1870 return Some(ResolvedTarget::SameFile);
1871 }
1872
1873 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1875 for &base_id in bases.iter().skip(1) {
1876 if let Some(decls) = cache.scope_declarations.get(&base_id)
1877 && decls.iter().any(|d| d.name == ctx.name)
1878 {
1879 let base_name = cache
1882 .name_to_node_id
1883 .iter()
1884 .find(|&(_, &id)| id == base_id)
1885 .map(|(name, _)| name.clone());
1886
1887 if let Some(base_name) = base_name
1888 && let Some(path) =
1889 find_file_for_contract(cache, base_name.as_str(), file_uri)
1890 {
1891 return Some(ResolvedTarget::OtherFile {
1892 path,
1893 name: ctx.name.clone(),
1894 });
1895 }
1896 return Some(ResolvedTarget::SameFile);
1898 }
1899 }
1900 }
1901 }
1902
1903 if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
1905 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1907 let current_path = file_uri.to_file_path().ok()?;
1908 let current_str = current_path.to_str()?;
1909 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1910 return Some(ResolvedTarget::SameFile);
1911 }
1912 return Some(ResolvedTarget::OtherFile {
1913 path,
1914 name: ctx.name.clone(),
1915 });
1916 }
1917 return Some(ResolvedTarget::SameFile);
1918 }
1919
1920 if cache.name_to_type.contains_key(ctx.name.as_str()) {
1922 return Some(ResolvedTarget::SameFile);
1923 }
1924
1925 None
1926}
1927
1928fn find_function_scope(
1930 cache: &crate::completion::CompletionCache,
1931 contract_id: NodeId,
1932 func_name: &str,
1933) -> Option<NodeId> {
1934 for (&scope_id, &parent_id) in &cache.scope_parent {
1938 if parent_id == contract_id {
1939 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1943 && contract_decls.iter().any(|d| d.name == func_name)
1944 {
1945 if cache.scope_declarations.contains_key(&scope_id)
1949 || cache.scope_parent.values().any(|&p| p == scope_id)
1950 {
1951 return Some(scope_id);
1952 }
1953 }
1954 }
1955 }
1956 None
1957}
1958
1959fn find_file_for_contract(
1961 cache: &crate::completion::CompletionCache,
1962 contract_name: &str,
1963 _file_uri: &Url,
1964) -> Option<String> {
1965 let node_id = cache.name_to_node_id.get(contract_name)?;
1970 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1971 let file_id = scope_range.file_id;
1972
1973 cache
1975 .path_to_file_id
1976 .iter()
1977 .find(|&(_, &fid)| fid == file_id)
1978 .map(|(path, _)| path.to_string())
1979}
1980
1981fn read_target_source(
1983 path: &str,
1984 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1985) -> Option<String> {
1986 let uri = Url::from_file_path(path).ok()?;
1988 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1989 return Some(content.clone());
1990 }
1991 std::fs::read_to_string(path).ok()
1993}
1994
1995fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1997 let decls = find_declarations_by_name(source, &ctx.name);
1998 if decls.is_empty() {
1999 return None;
2000 }
2001
2002 if decls.len() == 1 {
2004 return Some(Location {
2005 uri: file_uri.clone(),
2006 range: decls[0].range,
2007 });
2008 }
2009
2010 if let Some(contract_name) = &ctx.contract
2012 && let Some(d) = decls
2013 .iter()
2014 .find(|d| d.container.as_deref() == Some(contract_name))
2015 {
2016 return Some(Location {
2017 uri: file_uri.clone(),
2018 range: d.range,
2019 });
2020 }
2021
2022 Some(Location {
2024 uri: file_uri.clone(),
2025 range: decls[0].range,
2026 })
2027}
2028
2029#[derive(Debug, Clone, Copy)]
2035pub(crate) enum CodeActionKind<'a> {
2036 InsertAtFileStart { text: &'a str },
2038
2039 ReplaceToken {
2044 replacement: &'a str,
2045 walk_to: Option<&'a str>,
2046 },
2047
2048 DeleteToken,
2051
2052 DeleteLocalVar,
2055
2056 DeleteNodeByKind { node_kind: &'a str },
2060
2061 DeleteChildNode {
2064 walk_to: &'a str,
2065 child_kinds: &'a [&'a str],
2066 },
2067
2068 ReplaceChildNode {
2071 walk_to: &'a str,
2072 child_kind: &'a str,
2073 replacement: &'a str,
2074 },
2075
2076 InsertBeforeNode {
2080 walk_to: &'a str,
2081 before_child: &'a [&'a str],
2082 text: &'a str,
2083 },
2084}
2085
2086pub(crate) fn code_action_edit(
2091 source: &str,
2092 diag_range: Range,
2093 kind: CodeActionKind<'_>,
2094) -> Option<TextEdit> {
2095 let source_bytes = source.as_bytes();
2096
2097 match kind {
2098 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2100 range: Range {
2101 start: Position {
2102 line: 0,
2103 character: 0,
2104 },
2105 end: Position {
2106 line: 0,
2107 character: 0,
2108 },
2109 },
2110 new_text: text.to_string(),
2111 }),
2112
2113 CodeActionKind::ReplaceToken {
2115 replacement,
2116 walk_to,
2117 } => {
2118 let tree = ts_parse(source)?;
2119 let byte = pos_to_bytes(source_bytes, diag_range.start);
2120 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2121 if let Some(target_kind) = walk_to {
2125 loop {
2126 if node.kind() == target_kind {
2127 break;
2128 }
2129 node = node.parent()?;
2130 }
2131 }
2132 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2133 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2134 Some(TextEdit {
2135 range: Range {
2136 start: start_pos,
2137 end: end_pos,
2138 },
2139 new_text: replacement.to_string(),
2140 })
2141 }
2142
2143 CodeActionKind::DeleteToken => {
2145 let tree = ts_parse(source)?;
2146 let byte = pos_to_bytes(source_bytes, diag_range.start);
2147 let node = ts_node_at_byte(tree.root_node(), byte)?;
2148 let start = node.start_byte();
2149 let end =
2150 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2151 node.end_byte() + 1
2152 } else {
2153 node.end_byte()
2154 };
2155 let start_pos = bytes_to_pos(source_bytes, start)?;
2156 let end_pos = bytes_to_pos(source_bytes, end)?;
2157 Some(TextEdit {
2158 range: Range {
2159 start: start_pos,
2160 end: end_pos,
2161 },
2162 new_text: String::new(),
2163 })
2164 }
2165
2166 CodeActionKind::DeleteLocalVar => {
2168 let tree = ts_parse(source)?;
2169 let byte = pos_to_bytes(source_bytes, diag_range.start);
2170 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2171
2172 loop {
2173 if node.kind() == "variable_declaration_statement" {
2174 break;
2175 }
2176 node = node.parent()?;
2177 }
2178
2179 let stmt_start = node.start_byte();
2181 let delete_from = if stmt_start > 0 {
2182 let mut i = stmt_start - 1;
2183 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2184 i -= 1;
2185 }
2186 if source_bytes[i] == b'\n' {
2187 i
2188 } else {
2189 stmt_start
2190 }
2191 } else {
2192 stmt_start
2193 };
2194
2195 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2196 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2197 Some(TextEdit {
2198 range: Range {
2199 start: start_pos,
2200 end: end_pos,
2201 },
2202 new_text: String::new(),
2203 })
2204 }
2205
2206 CodeActionKind::DeleteNodeByKind { node_kind } => {
2208 let tree = ts_parse(source)?;
2209 let byte = pos_to_bytes(source_bytes, diag_range.start);
2210 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2211 loop {
2212 if node.kind() == node_kind {
2213 break;
2214 }
2215 node = node.parent()?;
2216 }
2217 let node_start = node.start_byte();
2219 let delete_from = if node_start > 0 {
2220 let mut i = node_start - 1;
2221 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2222 i -= 1;
2223 }
2224 if source_bytes[i] == b'\n' {
2225 i
2226 } else {
2227 node_start
2228 }
2229 } else {
2230 node_start
2231 };
2232 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2233 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2234 Some(TextEdit {
2235 range: Range {
2236 start: start_pos,
2237 end: end_pos,
2238 },
2239 new_text: String::new(),
2240 })
2241 }
2242
2243 CodeActionKind::DeleteChildNode {
2248 walk_to,
2249 child_kinds,
2250 } => {
2251 let tree = ts_parse(source)?;
2252 let byte = pos_to_bytes(source_bytes, diag_range.start);
2253 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2254 loop {
2255 if node.kind() == walk_to {
2256 break;
2257 }
2258 node = node.parent()?;
2259 }
2260 let mut cursor = node.walk();
2261 let children: Vec<_> = node.children(&mut cursor).collect();
2262 let child = child_kinds
2263 .iter()
2264 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2265 let start = child.start_byte();
2266 let end = if child.end_byte() < source_bytes.len()
2267 && source_bytes[child.end_byte()] == b' '
2268 {
2269 child.end_byte() + 1
2270 } else {
2271 child.end_byte()
2272 };
2273 let start_pos = bytes_to_pos(source_bytes, start)?;
2274 let end_pos = bytes_to_pos(source_bytes, end)?;
2275 Some(TextEdit {
2276 range: Range {
2277 start: start_pos,
2278 end: end_pos,
2279 },
2280 new_text: String::new(),
2281 })
2282 }
2283
2284 CodeActionKind::ReplaceChildNode {
2288 walk_to,
2289 child_kind,
2290 replacement,
2291 } => {
2292 let tree = ts_parse(source)?;
2293 let byte = pos_to_bytes(source_bytes, diag_range.start);
2294 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2295 loop {
2296 if node.kind() == walk_to {
2297 break;
2298 }
2299 node = node.parent()?;
2300 }
2301 let mut cursor = node.walk();
2302 let child = node
2303 .children(&mut cursor)
2304 .find(|c| c.kind() == child_kind)?;
2305 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2306 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2307 Some(TextEdit {
2308 range: Range {
2309 start: start_pos,
2310 end: end_pos,
2311 },
2312 new_text: replacement.to_string(),
2313 })
2314 }
2315
2316 CodeActionKind::InsertBeforeNode {
2323 walk_to,
2324 before_child,
2325 text,
2326 } => {
2327 let tree = ts_parse(source)?;
2328 let byte = pos_to_bytes(source_bytes, diag_range.start);
2329 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2330
2331 loop {
2332 if node.kind() == walk_to {
2333 break;
2334 }
2335 node = node.parent()?;
2336 }
2337
2338 let mut cursor = node.walk();
2339 let children: Vec<_> = node.children(&mut cursor).collect();
2340
2341 for target_kind in before_child {
2343 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2344 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2345 return Some(TextEdit {
2346 range: Range {
2347 start: insert_pos,
2348 end: insert_pos,
2349 },
2350 new_text: text.to_string(),
2351 });
2352 }
2353 }
2354 None
2355 }
2356 }
2357}
2358
2359#[cfg(test)]
2360mod ts_tests {
2361 use super::*;
2362
2363 #[test]
2364 fn test_cursor_context_state_var() {
2365 let source = r#"
2366contract Token {
2367 uint256 public totalSupply;
2368 function mint(uint256 amount) public {
2369 totalSupply += amount;
2370 }
2371}
2372"#;
2373 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2375 assert_eq!(ctx.name, "totalSupply");
2376 assert_eq!(ctx.function.as_deref(), Some("mint"));
2377 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2378 }
2379
2380 #[test]
2381 fn test_cursor_context_top_level() {
2382 let source = r#"
2383contract Foo {}
2384contract Bar {}
2385"#;
2386 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2388 assert_eq!(ctx.name, "Foo");
2389 assert!(ctx.function.is_none());
2390 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2392 }
2393
2394 #[test]
2395 fn test_find_declarations() {
2396 let source = r#"
2397contract Token {
2398 uint256 public totalSupply;
2399 function mint(uint256 amount) public {
2400 totalSupply += amount;
2401 }
2402}
2403"#;
2404 let decls = find_declarations_by_name(source, "totalSupply");
2405 assert_eq!(decls.len(), 1);
2406 assert_eq!(decls[0].kind, "state_variable_declaration");
2407 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2408 }
2409
2410 #[test]
2411 fn test_find_declarations_multiple_contracts() {
2412 let source = r#"
2413contract A {
2414 uint256 public value;
2415}
2416contract B {
2417 uint256 public value;
2418}
2419"#;
2420 let decls = find_declarations_by_name(source, "value");
2421 assert_eq!(decls.len(), 2);
2422 assert_eq!(decls[0].container.as_deref(), Some("A"));
2423 assert_eq!(decls[1].container.as_deref(), Some("B"));
2424 }
2425
2426 #[test]
2427 fn test_find_declarations_enum_value() {
2428 let source = "contract Foo { enum Status { Active, Paused } }";
2429 let decls = find_declarations_by_name(source, "Active");
2430 assert_eq!(decls.len(), 1);
2431 assert_eq!(decls[0].kind, "enum_value");
2432 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2433 }
2434
2435 #[test]
2436 fn test_cursor_context_short_param() {
2437 let source = r#"
2438contract Shop {
2439 uint256 public TAX;
2440 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2441 TAX = tax;
2442 }
2443}
2444"#;
2445 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2447 assert_eq!(ctx.name, "tax");
2448 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2449
2450 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2452 assert_eq!(ctx2.name, "TAX");
2453
2454 let decls = find_declarations_by_name(source, "tax");
2456 assert_eq!(decls.len(), 1);
2457 assert_eq!(decls[0].kind, "parameter");
2458
2459 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2460 assert_eq!(decls_tax_base.len(), 1);
2461 assert_eq!(decls_tax_base[0].kind, "parameter");
2462
2463 let decls_price = find_declarations_by_name(source, "price");
2464 assert_eq!(decls_price.len(), 1);
2465 assert_eq!(decls_price[0].kind, "parameter");
2466
2467 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2469 assert_eq!(decls_tax_upper.len(), 1);
2470 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2471 }
2472
2473 #[test]
2474 fn test_delete_child_node_2462_constructor_public() {
2475 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";
2478 let diag_range = Range {
2479 start: Position {
2480 line: 8,
2481 character: 4,
2482 },
2483 end: Position {
2484 line: 10,
2485 character: 5,
2486 },
2487 };
2488 let source_bytes = source.as_bytes();
2489 let tree = ts_parse(source).expect("parse failed");
2490 let byte = pos_to_bytes(source_bytes, diag_range.start);
2491 eprintln!("2462 byte offset: {byte}");
2492 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2493 loop {
2494 eprintln!(
2495 " ancestor: kind={} start={} end={}",
2496 n.kind(),
2497 n.start_byte(),
2498 n.end_byte()
2499 );
2500 if n.kind() == "constructor_definition" {
2501 let mut cursor = n.walk();
2502 for child in n.children(&mut cursor) {
2503 eprintln!(
2504 " child: kind={:?} text={:?}",
2505 child.kind(),
2506 &source[child.start_byte()..child.end_byte()]
2507 );
2508 }
2509 break;
2510 }
2511 match n.parent() {
2512 Some(p) => n = p,
2513 None => break,
2514 }
2515 }
2516 }
2517 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2518 let edit = code_action_edit(
2519 source,
2520 diag_range,
2521 CodeActionKind::DeleteChildNode {
2522 walk_to: "constructor_definition",
2523 child_kinds: &ck,
2524 },
2525 );
2526 assert!(edit.is_some(), "2462 fix returned None");
2527 let edit = edit.unwrap();
2528 assert_eq!(edit.new_text, "");
2529 let lines: Vec<&str> = source.lines().collect();
2530 let deleted = &lines[edit.range.start.line as usize]
2531 [edit.range.start.character as usize..edit.range.end.character as usize];
2532 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2533 }
2534
2535 #[test]
2536 fn test_delete_child_node_9239_constructor_private() {
2537 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";
2540 let diag_range = Range {
2541 start: Position {
2542 line: 9,
2543 character: 4,
2544 },
2545 end: Position {
2546 line: 11,
2547 character: 5,
2548 },
2549 };
2550 let ck: Vec<&str> = vec!["modifier_invocation"];
2551 let edit = code_action_edit(
2552 source,
2553 diag_range,
2554 CodeActionKind::DeleteChildNode {
2555 walk_to: "constructor_definition",
2556 child_kinds: &ck,
2557 },
2558 );
2559 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2560 let edit = edit.unwrap();
2561 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2563 let lines: Vec<&str> = source.lines().collect();
2565 let line_text = lines[edit.range.start.line as usize];
2566 let deleted =
2567 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2568 assert!(
2569 deleted.contains("private"),
2570 "expected deleted text to contain 'private', got: {:?}",
2571 deleted
2572 );
2573 }
2574
2575 #[test]
2576 fn test_find_best_declaration_same_contract() {
2577 let source = r#"
2578contract A { uint256 public x; }
2579contract B { uint256 public x; }
2580"#;
2581 let ctx = CursorContext {
2582 name: "x".into(),
2583 function: None,
2584 contract: Some("B".into()),
2585 object: None,
2586 arg_count: None,
2587 arg_types: vec![],
2588 };
2589 let uri = Url::parse("file:///test.sol").unwrap();
2590 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2591 assert_eq!(loc.range.start.line, 2);
2593 }
2594}
2595