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 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub name_location: Option<String>,
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub name_locations: Vec<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub referenced_declaration: Option<NodeId>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub node_type: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub member_location: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub member_name: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub absolute_path: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub scope: Option<NodeId>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub base_functions: Vec<NodeId>,
46}
47
48pub const CHILD_KEYS: &[&str] = &[
50 "AST",
51 "arguments",
52 "baseContracts",
53 "baseExpression",
54 "baseName",
55 "baseType",
56 "block",
57 "body",
58 "components",
59 "condition",
60 "declarations",
61 "endExpression",
62 "errorCall",
63 "eventCall",
64 "expression",
65 "externalCall",
66 "falseBody",
67 "falseExpression",
68 "file",
69 "foreign",
70 "functionName",
71 "indexExpression",
72 "initialValue",
73 "initializationExpression",
74 "keyType",
75 "leftExpression",
76 "leftHandSide",
77 "libraryName",
78 "literals",
79 "loopExpression",
80 "members",
81 "modifierName",
82 "modifiers",
83 "name",
84 "names",
85 "nodes",
86 "options",
87 "overrides",
88 "parameters",
89 "pathNode",
90 "post",
91 "pre",
92 "returnParameters",
93 "rightExpression",
94 "rightHandSide",
95 "startExpression",
96 "statements",
97 "storageLayout",
98 "subExpression",
99 "subdenomination",
100 "symbolAliases",
101 "trueBody",
102 "trueExpression",
103 "typeName",
104 "unitAlias",
105 "value",
106 "valueType",
107 "variableNames",
108 "variables",
109];
110
111pub type ExternalRefs = HashMap<SrcLocation, NodeId>;
114
115#[derive(Debug, Clone)]
117pub struct StorageSlotInfo {
118 pub slot: String,
120 pub offset: u32,
122}
123
124#[derive(Debug, Clone)]
130pub struct CachedBuild {
131 pub nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
132 pub path_to_abs: HashMap<RelPath, AbsPath>,
133 pub external_refs: ExternalRefs,
134 pub id_to_path_map: HashMap<crate::types::SolcFileId, String>,
135 pub decl_index: HashMap<NodeId, crate::solc_ast::DeclNode>,
139 pub node_id_to_source_path: HashMap<NodeId, AbsPath>,
143 pub hint_index: crate::inlay_hints::HintIndex,
146 pub doc_index: crate::hover::DocIndex,
149 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
152 pub build_version: i32,
155 pub qualifier_refs: HashMap<NodeId, Vec<NodeId>>,
163 pub base_function_implementation: HashMap<NodeId, Vec<NodeId>>,
174 pub storage_layout: HashMap<NodeId, StorageSlotInfo>,
184 pub low_level_calls: Vec<LowLevelCall>,
191}
192
193impl CachedBuild {
194 pub fn new(ast: Value, build_version: i32, interner: Option<&mut PathInterner>) -> Self {
206 let (mut nodes, path_to_abs, mut external_refs, mut low_level_calls) =
207 if let Some(sources) = ast.get("sources") {
208 cache_ids(sources)
209 } else {
210 (HashMap::new(), HashMap::new(), HashMap::new(), Vec::new())
211 };
212
213 let solc_id_to_path: HashMap<SolcFileId, String> = ast
215 .get("source_id_to_path")
216 .and_then(|v| v.as_object())
217 .map(|obj| {
218 obj.iter()
219 .map(|(k, v)| {
220 (
221 SolcFileId::new(k.clone()),
222 v.as_str().unwrap_or("").to_string(),
223 )
224 })
225 .collect()
226 })
227 .unwrap_or_default();
228
229 let (id_to_path_map, canonical_remap) = if let Some(interner) = interner {
234 let remap = interner.build_remap(&solc_id_to_path);
235
236 for file_nodes in nodes.values_mut() {
238 for info in file_nodes.values_mut() {
239 canonicalize_node_info(info, &remap);
240 }
241 }
242
243 let old_refs = std::mem::take(&mut external_refs);
245 for (src, decl_id) in old_refs {
246 let new_src = SrcLocation::new(remap_src_canonical(src.as_str(), &remap));
247 external_refs.insert(new_src, decl_id);
248 }
249
250 (interner.to_id_to_path_map(), Some(remap))
252 } else {
253 (solc_id_to_path, None)
255 };
256
257 let doc_index = crate::hover::build_doc_index(&ast);
258
259 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
266 match crate::solc_ast::extract_decl_nodes(sources) {
267 Some(extracted) => (
268 extracted
269 .decl_index
270 .into_iter()
271 .map(|(id, decl)| (NodeId(id), decl))
272 .collect(),
273 extracted
274 .node_id_to_source_path
275 .into_iter()
276 .map(|(id, path)| (NodeId(id), AbsPath::new(path)))
277 .collect(),
278 ),
279 None => (HashMap::new(), HashMap::new()),
280 }
281 } else {
282 (HashMap::new(), HashMap::new())
283 };
284
285 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
287 let hint_index = if let Some(sources) = ast.get("sources") {
288 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
289 } else {
290 HashMap::new()
291 };
292
293 let completion_cache = {
295 let sources = ast.get("sources");
296 let contracts = ast.get("contracts");
297 let cc = if let Some(s) = sources {
298 crate::completion::build_completion_cache(s, contracts, canonical_remap.as_ref())
299 } else {
300 crate::completion::build_completion_cache(
301 &serde_json::Value::Object(Default::default()),
302 contracts,
303 canonical_remap.as_ref(),
304 )
305 };
306 std::sync::Arc::new(cc)
307 };
308
309 let qualifier_refs = build_qualifier_refs(&nodes);
314
315 let base_function_implementation = build_base_function_implementation(&nodes);
319
320 let storage_layout = build_storage_layout(&ast);
322
323 if let Some(ref remap) = canonical_remap {
325 for llc in &mut low_level_calls {
326 llc.src = SrcLocation::new(remap_src_canonical(llc.src.as_str(), remap));
327 llc.name_src = SrcLocation::new(remap_src_canonical(llc.name_src.as_str(), remap));
328 }
329 }
330
331 Self {
335 nodes,
336 path_to_abs,
337 external_refs,
338 id_to_path_map,
339 decl_index,
340 node_id_to_source_path,
341 hint_index,
342 doc_index,
343 completion_cache,
344 build_version,
345 qualifier_refs,
346 base_function_implementation,
347 storage_layout,
348 low_level_calls,
349 }
350 }
351
352 pub fn merge_missing_from(&mut self, other: &CachedBuild) {
359 for (abs_path, file_nodes) in &other.nodes {
360 if !self.nodes.contains_key(abs_path) {
361 self.nodes.insert(abs_path.clone(), file_nodes.clone());
362 }
363 }
364 for (k, v) in &other.path_to_abs {
365 self.path_to_abs
366 .entry(k.clone())
367 .or_insert_with(|| v.clone());
368 }
369 for (k, v) in &other.external_refs {
370 self.external_refs.entry(k.clone()).or_insert(*v);
371 }
372 for (k, v) in &other.id_to_path_map {
373 self.id_to_path_map
374 .entry(k.clone())
375 .or_insert_with(|| v.clone());
376 }
377 for (container_id, other_qrefs) in &other.qualifier_refs {
380 let entry = self.qualifier_refs.entry(*container_id).or_default();
381 for &qnode_id in other_qrefs {
382 if !entry.contains(&qnode_id) {
383 entry.push(qnode_id);
384 }
385 }
386 }
387 for (node_id, other_impls) in &other.base_function_implementation {
390 let entry = self
391 .base_function_implementation
392 .entry(*node_id)
393 .or_default();
394 for &impl_id in other_impls {
395 if !entry.contains(&impl_id) {
396 entry.push(impl_id);
397 }
398 }
399 }
400 for (node_id, info) in &other.storage_layout {
402 self.storage_layout
403 .entry(*node_id)
404 .or_insert_with(|| info.clone());
405 }
406 }
407
408 pub fn from_reference_index(
417 nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
418 path_to_abs: HashMap<RelPath, AbsPath>,
419 external_refs: ExternalRefs,
420 id_to_path_map: HashMap<SolcFileId, String>,
421 build_version: i32,
422 interner: Option<&mut PathInterner>,
423 ) -> Self {
424 if let Some(interner) = interner {
427 for path in id_to_path_map.values() {
428 interner.intern(path);
429 }
430 }
431
432 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
433 &serde_json::Value::Object(Default::default()),
434 None,
435 None,
436 ));
437
438 let qualifier_refs = build_qualifier_refs(&nodes);
440
441 let base_function_implementation = build_base_function_implementation(&nodes);
444
445 Self {
446 nodes,
447 path_to_abs,
448 external_refs,
449 id_to_path_map,
450 decl_index: HashMap::new(),
451 node_id_to_source_path: HashMap::new(),
452 hint_index: HashMap::new(),
453 doc_index: HashMap::new(),
454 completion_cache,
455 build_version,
456 qualifier_refs,
457 base_function_implementation,
458 storage_layout: HashMap::new(),
459 low_level_calls: Vec::new(),
460 }
461 }
462}
463
464fn build_storage_layout(ast: &Value) -> HashMap<NodeId, StorageSlotInfo> {
470 let mut map = HashMap::new();
471 let contracts = match ast.get("contracts").and_then(|v| v.as_object()) {
472 Some(c) => c,
473 None => return map,
474 };
475 for (_path, contracts_in_file) in contracts {
476 let file_contracts = match contracts_in_file.as_object() {
477 Some(c) => c,
478 None => continue,
479 };
480 for (_name, contract_data) in file_contracts {
481 for key in &["storageLayout", "transientStorageLayout"] {
483 let entries = match contract_data
484 .get(*key)
485 .and_then(|v| v.get("storage"))
486 .and_then(|v| v.as_array())
487 {
488 Some(arr) => arr,
489 None => continue,
490 };
491 for entry in entries {
492 let ast_id = match entry.get("astId").and_then(|v| v.as_i64()) {
493 Some(id) => id,
494 None => continue,
495 };
496 let slot = entry
497 .get("slot")
498 .and_then(|v| v.as_str())
499 .unwrap_or("0")
500 .to_string();
501 let offset = entry.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
502 map.insert(NodeId(ast_id), StorageSlotInfo { slot, offset });
503 }
504 }
505 }
506 }
507 map
508}
509
510fn build_qualifier_refs(
520 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
521) -> HashMap<NodeId, Vec<NodeId>> {
522 let mut node_scope: HashMap<NodeId, NodeId> = HashMap::new();
525 for file_nodes in nodes.values() {
526 for (id, info) in file_nodes {
527 if let Some(scope_id) = info.scope {
528 node_scope.insert(*id, scope_id);
529 }
530 }
531 }
532
533 let mut qualifier_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
536 for file_nodes in nodes.values() {
537 for (id, info) in file_nodes {
538 if info.name_locations.len() > 1
539 && info.node_type.as_deref() == Some("IdentifierPath")
540 && let Some(ref_decl) = info.referenced_declaration
541 && let Some(&scope_id) = node_scope.get(&ref_decl)
542 {
543 qualifier_refs.entry(scope_id).or_default().push(*id);
544 }
545 }
546 }
547
548 qualifier_refs
549}
550
551fn build_base_function_implementation(
561 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
562) -> HashMap<NodeId, Vec<NodeId>> {
563 let mut index: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
564
565 for file_nodes in nodes.values() {
566 for (id, info) in file_nodes {
567 if info.base_functions.is_empty() {
568 continue;
569 }
570 for &base_id in &info.base_functions {
572 index.entry(*id).or_default().push(base_id);
573 }
574 for &base_id in &info.base_functions {
576 index.entry(base_id).or_default().push(*id);
577 }
578 }
579 }
580
581 for ids in index.values_mut() {
583 ids.sort_unstable();
584 ids.dedup();
585 }
586
587 index
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct LowLevelCall {
601 pub kind: String,
603 pub src: SrcLocation,
606 pub name_src: SrcLocation,
609 pub abs_path: AbsPath,
611}
612
613pub const LOW_LEVEL_CALL_NAMES: &[&str] = &["call", "staticcall", "delegatecall"];
616
617pub const YUL_CALL_OPCODES: &[&str] = &["call", "staticcall", "delegatecall"];
621
622type CachedIds = (
624 HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
625 HashMap<RelPath, AbsPath>,
626 ExternalRefs,
627 Vec<LowLevelCall>,
628);
629
630pub fn remap_src_canonical(src: &str, remap: &HashMap<u64, FileId>) -> String {
634 let Some(last_colon) = src.rfind(':') else {
635 return src.to_owned();
636 };
637 let old_id_str = &src[last_colon + 1..];
638 let Ok(old_id) = old_id_str.parse::<u64>() else {
639 return src.to_owned();
640 };
641 let Some(canonical) = remap.get(&old_id) else {
642 return src.to_owned();
643 };
644 if canonical.0 == old_id {
645 return src.to_owned();
646 }
647 format!("{}{}", &src[..=last_colon], canonical.0)
648}
649
650fn canonicalize_node_info(info: &mut NodeInfo, remap: &HashMap<u64, FileId>) {
653 info.src = SrcLocation::new(remap_src_canonical(info.src.as_str(), remap));
654 if let Some(loc) = info.name_location.as_mut() {
655 *loc = remap_src_canonical(loc, remap);
656 }
657 for loc in &mut info.name_locations {
658 *loc = remap_src_canonical(loc, remap);
659 }
660 if let Some(loc) = info.member_location.as_mut() {
661 *loc = remap_src_canonical(loc, remap);
662 }
663}
664
665pub fn cache_ids(sources: &Value) -> CachedIds {
666 let source_count = sources.as_object().map_or(0, |obj| obj.len());
667
668 let mut nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>> =
671 HashMap::with_capacity(source_count);
672 let mut path_to_abs: HashMap<RelPath, AbsPath> = HashMap::with_capacity(source_count);
673 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
674 let mut low_level_calls: Vec<LowLevelCall> = Vec::new();
675
676 if let Some(sources_obj) = sources.as_object() {
677 for (path, source_data) in sources_obj {
678 if let Some(ast) = source_data.get("ast") {
679 let abs_path = AbsPath::new(
681 ast.get("absolutePath")
682 .and_then(|v| v.as_str())
683 .unwrap_or(path)
684 .to_string(),
685 );
686
687 path_to_abs.insert(RelPath::new(path), abs_path.clone());
688
689 let size_hint = ast
694 .get("nodes")
695 .and_then(|v| v.as_array())
696 .map_or(64, |arr| arr.len() * 8);
697 if !nodes.contains_key(&abs_path) {
698 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
699 }
700
701 if let Some(id) = ast.get("id").and_then(|v| v.as_i64())
702 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
703 {
704 nodes.get_mut(&abs_path).unwrap().insert(
705 NodeId(id),
706 NodeInfo {
707 src: SrcLocation::new(src),
708 name_location: None,
709 name_locations: vec![],
710 referenced_declaration: None,
711 node_type: ast
712 .get("nodeType")
713 .and_then(|v| v.as_str())
714 .map(|s| s.to_string()),
715 member_location: None,
716 member_name: None,
717 absolute_path: ast
718 .get("absolutePath")
719 .and_then(|v| v.as_str())
720 .map(|s| s.to_string()),
721 scope: ast.get("scope").and_then(|v| v.as_i64()).map(NodeId),
722 base_functions: vec![],
723 },
724 );
725 }
726
727 let mut stack = vec![ast];
728
729 while let Some(tree) = stack.pop() {
730 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_i64())
731 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
732 {
733 let id = NodeId(raw_id);
734 let mut name_location = tree
736 .get("nameLocation")
737 .and_then(|v| v.as_str())
738 .map(|s| s.to_string());
739
740 if name_location.is_none()
744 && let Some(name_locations) = tree.get("nameLocations")
745 && let Some(locations_array) = name_locations.as_array()
746 && !locations_array.is_empty()
747 {
748 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
749 if node_type == Some("IdentifierPath") {
750 name_location = locations_array
751 .last()
752 .and_then(|v| v.as_str())
753 .map(|s| s.to_string());
754 } else {
755 name_location = locations_array[0].as_str().map(|s| s.to_string());
756 }
757 }
758
759 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
760 && let Some(locations_array) = name_locations.as_array()
761 {
762 locations_array
763 .iter()
764 .filter_map(|v| v.as_str().map(|s| s.to_string()))
765 .collect()
766 } else {
767 vec![]
768 };
769
770 let mut final_name_location = name_location;
771 if final_name_location.is_none()
772 && let Some(member_loc) =
773 tree.get("memberLocation").and_then(|v| v.as_str())
774 {
775 final_name_location = Some(member_loc.to_string());
776 }
777
778 let node_info = NodeInfo {
779 src: SrcLocation::new(src),
780 name_location: final_name_location,
781 name_locations,
782 referenced_declaration: tree
783 .get("referencedDeclaration")
784 .and_then(|v| v.as_i64())
785 .map(NodeId),
786 node_type: tree
787 .get("nodeType")
788 .and_then(|v| v.as_str())
789 .map(|s| s.to_string()),
790 member_location: tree
791 .get("memberLocation")
792 .and_then(|v| v.as_str())
793 .map(|s| s.to_string()),
794 member_name: tree
795 .get("memberName")
796 .and_then(|v| v.as_str())
797 .map(|s| s.to_string()),
798 absolute_path: tree
799 .get("absolutePath")
800 .and_then(|v| v.as_str())
801 .map(|s| s.to_string()),
802 scope: tree.get("scope").and_then(|v| v.as_i64()).map(NodeId),
803 base_functions: tree
804 .get("baseFunctions")
805 .and_then(|v| v.as_array())
806 .map(|arr| {
807 arr.iter().filter_map(|v| v.as_i64().map(NodeId)).collect()
808 })
809 .unwrap_or_default(),
810 };
811
812 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
813
814 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
816 && let Some(ext_refs) =
817 tree.get("externalReferences").and_then(|v| v.as_array())
818 {
819 for ext_ref in ext_refs {
820 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
821 && let Some(decl_id) =
822 ext_ref.get("declaration").and_then(|v| v.as_i64())
823 {
824 external_refs
825 .insert(SrcLocation::new(src_str), NodeId(decl_id));
826 }
827 }
828 }
829 }
830
831 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("YulFunctionCall")
837 && let Some(fn_obj) = tree.get("functionName")
838 && let Some(fn_name) = fn_obj.get("name").and_then(|v| v.as_str())
839 && YUL_CALL_OPCODES.contains(&fn_name)
840 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
841 {
842 let fn_src = fn_obj.get("src").and_then(|v| v.as_str()).unwrap_or(src);
843 low_level_calls.push(LowLevelCall {
844 kind: fn_name.to_string(),
845 src: SrcLocation::new(src),
846 name_src: SrcLocation::new(fn_src),
847 abs_path: abs_path.clone(),
848 });
849 }
850
851 for key in CHILD_KEYS {
852 push_if_node_or_array(tree, key, &mut stack);
853 }
854 }
855 }
856 }
857 }
858
859 (nodes, path_to_abs, external_refs, low_level_calls)
860}
861
862pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
863 let text = String::from_utf8_lossy(source_bytes);
864 crate::utils::position_to_byte_offset(&text, position)
865}
866
867pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
868 let text = String::from_utf8_lossy(source_bytes);
869 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
870 Some(pos)
871}
872
873pub fn src_to_location(
875 src: &str,
876 id_to_path: &HashMap<crate::types::SolcFileId, String>,
877) -> Option<Location> {
878 let loc = SourceLoc::parse(src)?;
879 let file_path = id_to_path.get(&loc.file_id_str())?;
880
881 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
882 std::path::PathBuf::from(file_path)
883 } else {
884 std::env::current_dir().ok()?.join(file_path)
885 };
886
887 let source_bytes = std::fs::read(&absolute_path).ok()?;
888 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
889 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
890 let uri = Url::from_file_path(&absolute_path).ok()?;
891
892 Some(Location {
893 uri,
894 range: Range {
895 start: start_pos,
896 end: end_pos,
897 },
898 })
899}
900
901pub fn goto_bytes(
902 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
903 path_to_abs: &HashMap<RelPath, AbsPath>,
904 id_to_path: &HashMap<crate::types::SolcFileId, String>,
905 external_refs: &ExternalRefs,
906 uri: &str,
907 position: usize,
908) -> Option<(String, usize, usize)> {
909 let path = match uri.starts_with("file://") {
910 true => &uri[7..],
911 false => uri,
912 };
913
914 let abs_path = path_to_abs.get(path)?;
916
917 let current_file_nodes = nodes.get(abs_path)?;
919
920 let path_to_file_id: HashMap<&str, &crate::types::SolcFileId> =
922 id_to_path.iter().map(|(id, p)| (p.as_str(), id)).collect();
923
924 let current_file_id = path_to_file_id.get(abs_path.as_str());
928
929 for (src_str, decl_id) in external_refs {
931 let Some(src_loc) = SourceLoc::parse(src_str.as_str()) else {
932 continue;
933 };
934
935 if let Some(file_id) = current_file_id {
937 if src_loc.file_id_str() != **file_id {
938 continue;
939 }
940 } else {
941 continue;
942 }
943
944 if src_loc.offset <= position && position < src_loc.end() {
945 let mut target_node: Option<&NodeInfo> = None;
947 for file_nodes in nodes.values() {
948 if let Some(node) = file_nodes.get(decl_id) {
949 target_node = Some(node);
950 break;
951 }
952 }
953 let node = target_node?;
954 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
955 let loc = SourceLoc::parse(loc_str)?;
956 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
957 return Some((file_path, loc.offset, loc.length));
958 }
959 }
960
961 let mut refs = HashMap::new();
962
963 for (id, content) in current_file_nodes {
965 if content.referenced_declaration.is_none() {
966 continue;
967 }
968
969 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
970 continue;
971 };
972
973 if src_loc.offset <= position && position < src_loc.end() {
974 let diff = src_loc.length;
975 if !refs.contains_key(&diff) || refs[&diff] <= *id {
976 refs.insert(diff, *id);
977 }
978 }
979 }
980
981 if refs.is_empty() {
982 let tmp = current_file_nodes.iter();
985 for (_id, content) in tmp {
986 if content.node_type == Some("ImportDirective".to_string()) {
987 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
988 continue;
989 };
990
991 if src_loc.offset <= position
992 && position < src_loc.end()
993 && let Some(import_path) = &content.absolute_path
994 {
995 return Some((import_path.clone(), 0, 0));
996 }
997 }
998 }
999 return None;
1000 }
1001
1002 let min_diff = *refs.keys().min()?;
1004 let chosen_id = refs[&min_diff];
1005 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
1006
1007 let mut target_node: Option<&NodeInfo> = None;
1009 for file_nodes in nodes.values() {
1010 if let Some(node) = file_nodes.get(&ref_id) {
1011 target_node = Some(node);
1012 break;
1013 }
1014 }
1015
1016 let node = target_node?;
1017
1018 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1020 let loc = SourceLoc::parse(loc_str)?;
1021 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
1022
1023 Some((file_path, loc.offset, loc.length))
1024}
1025
1026fn resolve_qualifier_goto(
1031 build: &CachedBuild,
1032 file_uri: &Url,
1033 byte_position: usize,
1034) -> Option<Location> {
1035 let path = file_uri.to_file_path().ok()?;
1036 let path_str = path.to_str()?;
1037 let abs_path = build.path_to_abs.get(path_str)?;
1038 let file_nodes = build.nodes.get(abs_path)?;
1039
1040 let node_id = crate::references::byte_to_id(&build.nodes, abs_path, byte_position)?;
1042 let node_info = file_nodes.get(&node_id)?;
1043
1044 if node_info.node_type.as_deref() != Some("IdentifierPath")
1046 || node_info.name_locations.len() <= 1
1047 {
1048 return None;
1049 }
1050
1051 let first_loc = SourceLoc::parse(&node_info.name_locations[0])?;
1053 if byte_position < first_loc.offset || byte_position >= first_loc.end() {
1054 return None;
1055 }
1056
1057 let ref_decl_id = node_info.referenced_declaration?;
1059 let decl_node = find_node_info(&build.nodes, ref_decl_id)?;
1061 let scope_id = decl_node.scope?;
1062
1063 let container_node = find_node_info(&build.nodes, scope_id)?;
1065 let loc_str = container_node
1066 .name_location
1067 .as_deref()
1068 .unwrap_or(container_node.src.as_str());
1069 let loc = SourceLoc::parse(loc_str)?;
1070 let file_path = build.id_to_path_map.get(&loc.file_id_str())?;
1071
1072 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
1073 std::path::PathBuf::from(file_path)
1074 } else {
1075 std::env::current_dir().ok()?.join(file_path)
1076 };
1077
1078 let target_bytes = std::fs::read(&absolute_path).ok()?;
1079 let start_pos = bytes_to_pos(&target_bytes, loc.offset)?;
1080 let end_pos = bytes_to_pos(&target_bytes, loc.end())?;
1081 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1082
1083 Some(Location {
1084 uri: target_uri,
1085 range: Range {
1086 start: start_pos,
1087 end: end_pos,
1088 },
1089 })
1090}
1091
1092fn find_node_info(
1094 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
1095 node_id: NodeId,
1096) -> Option<&NodeInfo> {
1097 for file_nodes in nodes.values() {
1098 if let Some(node) = file_nodes.get(&node_id) {
1099 return Some(node);
1100 }
1101 }
1102 None
1103}
1104
1105pub fn goto_declaration_cached(
1108 build: &CachedBuild,
1109 file_uri: &Url,
1110 position: Position,
1111 source_bytes: &[u8],
1112) -> Option<Location> {
1113 let byte_position = pos_to_bytes(source_bytes, position);
1114
1115 if let Some(location) = resolve_qualifier_goto(build, file_uri, byte_position) {
1120 return Some(location);
1121 }
1122
1123 if let Some((file_path, location_bytes, length)) = goto_bytes(
1124 &build.nodes,
1125 &build.path_to_abs,
1126 &build.id_to_path_map,
1127 &build.external_refs,
1128 file_uri.as_ref(),
1129 byte_position,
1130 ) {
1131 let target_file_path = std::path::Path::new(&file_path);
1132 let absolute_path = if target_file_path.is_absolute() {
1133 target_file_path.to_path_buf()
1134 } else {
1135 let base = file_uri
1140 .to_file_path()
1141 .ok()
1142 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1143 .or_else(|| std::env::current_dir().ok())
1144 .unwrap_or_default();
1145 base.join(target_file_path)
1146 };
1147
1148 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
1149 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
1150 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
1151 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
1152 {
1153 return Some(Location {
1154 uri: target_uri,
1155 range: Range {
1156 start: start_pos,
1157 end: end_pos,
1158 },
1159 });
1160 }
1161 };
1162
1163 None
1164}
1165
1166pub fn goto_declaration_by_name(
1177 cached_build: &CachedBuild,
1178 file_uri: &Url,
1179 name: &str,
1180 byte_hint: usize,
1181) -> Option<Location> {
1182 let path = match file_uri.as_ref().starts_with("file://") {
1183 true => &file_uri.as_ref()[7..],
1184 false => file_uri.as_ref(),
1185 };
1186 let abs_path = cached_build.path_to_abs.get(path)?;
1187 let built_source = std::fs::read_to_string(abs_path).ok()?;
1189
1190 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
1192
1193 let tmp = {
1194 let this = cached_build.nodes.get(abs_path)?;
1195 this.iter()
1196 };
1197 for (_id, node) in tmp {
1198 let ref_id = match node.referenced_declaration {
1199 Some(id) => id,
1200 None => continue,
1201 };
1202
1203 let Some(src_loc) = SourceLoc::parse(node.src.as_str()) else {
1205 continue;
1206 };
1207 let start = src_loc.offset;
1208 let length = src_loc.length;
1209
1210 if start + length > built_source.len() {
1211 continue;
1212 }
1213
1214 let node_text = &built_source[start..start + length];
1215
1216 let matches = node_text == name
1221 || node_text.contains(&format!(".{name}("))
1222 || node_text.ends_with(&format!(".{name}"));
1223
1224 if matches {
1225 let distance = if byte_hint >= start && byte_hint < start + length {
1229 0 } else if byte_hint < start {
1231 start - byte_hint
1232 } else {
1233 byte_hint - (start + length)
1234 };
1235 candidates.push((distance, length, ref_id));
1236 }
1237 }
1238
1239 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1241 let ref_id = candidates.first()?.2;
1242
1243 let mut target_node: Option<&NodeInfo> = None;
1245 for file_nodes in cached_build.nodes.values() {
1246 if let Some(node) = file_nodes.get(&ref_id) {
1247 target_node = Some(node);
1248 break;
1249 }
1250 }
1251
1252 let node = target_node?;
1253
1254 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1256 let loc = SourceLoc::parse(loc_str)?;
1257
1258 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1259 let location_bytes = loc.offset;
1260 let length = loc.length;
1261
1262 let target_file_path = std::path::Path::new(file_path);
1263 let absolute_path = if target_file_path.is_absolute() {
1264 target_file_path.to_path_buf()
1265 } else {
1266 let base = file_uri
1267 .to_file_path()
1268 .ok()
1269 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1270 .or_else(|| std::env::current_dir().ok())
1271 .unwrap_or_default();
1272 base.join(target_file_path)
1273 };
1274
1275 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1276 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1277 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1278 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1279
1280 Some(Location {
1281 uri: target_uri,
1282 range: Range {
1283 start: start_pos,
1284 end: end_pos,
1285 },
1286 })
1287}
1288
1289#[derive(Debug, Clone)]
1293pub struct CursorContext {
1294 pub name: String,
1296 pub function: Option<String>,
1298 pub contract: Option<String>,
1300 pub object: Option<String>,
1304 pub arg_count: Option<usize>,
1307 pub arg_types: Vec<Option<String>>,
1310}
1311
1312fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1314 let mut parser = Parser::new();
1315 parser
1316 .set_language(&tree_sitter_solidity::LANGUAGE.into())
1317 .expect("failed to load Solidity grammar");
1318 parser.parse(source, None)
1319}
1320
1321pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1327 let line = location.range.start.line as usize;
1328 let start_col = location.range.start.character as usize;
1329 let end_col = location.range.end.character as usize;
1330
1331 if let Some(line_text) = target_source.lines().nth(line)
1332 && end_col <= line_text.len()
1333 {
1334 return &line_text[start_col..end_col] == expected_name;
1335 }
1336 true
1338}
1339
1340fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1342 if byte < node.start_byte() || byte >= node.end_byte() {
1343 return None;
1344 }
1345 let mut cursor = node.walk();
1346 for child in node.children(&mut cursor) {
1347 if child.start_byte() <= byte
1348 && byte < child.end_byte()
1349 && let Some(deeper) = ts_node_at_byte(child, byte)
1350 {
1351 return Some(deeper);
1352 }
1353 }
1354 Some(node)
1355}
1356
1357fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1359 let mut cursor = node.walk();
1360 node.children(&mut cursor)
1361 .find(|c| c.kind() == "identifier" && c.is_named())
1362 .map(|c| &source[c.byte_range()])
1363}
1364
1365fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1371 let expr = if arg_node.kind() == "call_argument" {
1373 let mut c = arg_node.walk();
1374 arg_node.children(&mut c).find(|ch| ch.is_named())?
1375 } else {
1376 arg_node
1377 };
1378
1379 match expr.kind() {
1380 "identifier" => {
1381 let var_name = &source[expr.byte_range()];
1382 find_variable_type(expr, source, var_name)
1384 }
1385 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1386 "boolean_literal" => Some("bool".into()),
1387 "string_literal" | "hex_string_literal" => Some("string".into()),
1388 _ => None,
1389 }
1390}
1391
1392fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1397 let mut scope = from.parent();
1398 while let Some(node) = scope {
1399 match node.kind() {
1400 "function_definition" | "modifier_definition" | "constructor_definition" => {
1401 let mut c = node.walk();
1403 for child in node.children(&mut c) {
1404 if child.kind() == "parameter"
1405 && let Some(id) = ts_child_id_text(child, source)
1406 && id == var_name
1407 {
1408 let mut pc = child.walk();
1410 return child
1411 .children(&mut pc)
1412 .find(|c| {
1413 matches!(
1414 c.kind(),
1415 "type_name"
1416 | "primitive_type"
1417 | "user_defined_type"
1418 | "mapping"
1419 )
1420 })
1421 .map(|t| source[t.byte_range()].trim().to_string());
1422 }
1423 }
1424 }
1425 "function_body" | "block_statement" | "unchecked_block" => {
1426 let mut c = node.walk();
1428 for child in node.children(&mut c) {
1429 if (child.kind() == "variable_declaration_statement"
1430 || child.kind() == "variable_declaration")
1431 && let Some(id) = ts_child_id_text(child, source)
1432 && id == var_name
1433 {
1434 let mut pc = child.walk();
1435 return child
1436 .children(&mut pc)
1437 .find(|c| {
1438 matches!(
1439 c.kind(),
1440 "type_name"
1441 | "primitive_type"
1442 | "user_defined_type"
1443 | "mapping"
1444 )
1445 })
1446 .map(|t| source[t.byte_range()].trim().to_string());
1447 }
1448 }
1449 }
1450 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1451 if let Some(body) = ts_find_child(node, "contract_body") {
1453 let mut c = body.walk();
1454 for child in body.children(&mut c) {
1455 if child.kind() == "state_variable_declaration"
1456 && let Some(id) = ts_child_id_text(child, source)
1457 && id == var_name
1458 {
1459 let mut pc = child.walk();
1460 return child
1461 .children(&mut pc)
1462 .find(|c| {
1463 matches!(
1464 c.kind(),
1465 "type_name"
1466 | "primitive_type"
1467 | "user_defined_type"
1468 | "mapping"
1469 )
1470 })
1471 .map(|t| source[t.byte_range()].trim().to_string());
1472 }
1473 }
1474 }
1475 }
1476 _ => {}
1477 }
1478 scope = node.parent();
1479 }
1480 None
1481}
1482
1483fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1485 let mut cursor = call_node.walk();
1486 call_node
1487 .children(&mut cursor)
1488 .filter(|c| c.kind() == "call_argument")
1489 .map(|arg| infer_argument_type(arg, source))
1490 .collect()
1491}
1492
1493fn best_overload<'a>(
1501 decls: &'a [TsDeclaration],
1502 arg_count: Option<usize>,
1503 arg_types: &[Option<String>],
1504) -> Option<&'a TsDeclaration> {
1505 if decls.len() == 1 {
1506 return decls.first();
1507 }
1508 if decls.is_empty() {
1509 return None;
1510 }
1511
1512 let func_decls: Vec<&TsDeclaration> =
1514 decls.iter().filter(|d| d.param_count.is_some()).collect();
1515
1516 if func_decls.is_empty() {
1517 return decls.first();
1518 }
1519
1520 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1522 let matched: Vec<_> = func_decls
1523 .iter()
1524 .filter(|d| d.param_count == Some(ac))
1525 .collect();
1526 if matched.len() == 1 {
1527 return Some(matched[0]);
1528 }
1529 if matched.is_empty() {
1530 func_decls.iter().collect()
1532 } else {
1533 matched
1534 }
1535 } else {
1536 func_decls.iter().collect()
1537 };
1538
1539 if !arg_types.is_empty() {
1541 let mut best: Option<(&TsDeclaration, usize)> = None;
1542 for &&decl in &count_matched {
1543 let score = arg_types
1544 .iter()
1545 .zip(decl.param_types.iter())
1546 .filter(|(arg_ty, param_ty)| {
1547 if let Some(at) = arg_ty {
1548 at == param_ty.as_str()
1549 } else {
1550 false
1551 }
1552 })
1553 .count();
1554 if best.is_none() || score > best.unwrap().1 {
1555 best = Some((decl, score));
1556 }
1557 }
1558 if let Some((decl, _)) = best {
1559 return Some(decl);
1560 }
1561 }
1562
1563 count_matched.first().map(|d| **d).or(decls.first())
1565}
1566
1567pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1571 let tree = ts_parse(source)?;
1572 let byte = pos_to_bytes(source.as_bytes(), position);
1573 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1574
1575 let id_node = if leaf.kind() == "identifier" {
1577 leaf
1578 } else {
1579 let parent = leaf.parent()?;
1581 if parent.kind() == "identifier" {
1582 parent
1583 } else {
1584 return None;
1585 }
1586 };
1587
1588 let name = source[id_node.byte_range()].to_string();
1589 let mut function = None;
1590 let mut contract = None;
1591
1592 let object = id_node.parent().and_then(|parent| {
1596 if parent.kind() == "member_expression" {
1597 let prop = parent.child_by_field_name("property")?;
1598 if prop.id() == id_node.id() {
1600 let obj = parent.child_by_field_name("object")?;
1601 Some(source[obj.byte_range()].to_string())
1602 } else {
1603 None
1604 }
1605 } else {
1606 None
1607 }
1608 });
1609
1610 let (arg_count, arg_types) = {
1614 let mut node = id_node.parent();
1615 let mut result = (None, vec![]);
1616 while let Some(n) = node {
1617 if n.kind() == "call_expression" {
1618 let types = infer_call_arg_types(n, source);
1619 result = (Some(types.len()), types);
1620 break;
1621 }
1622 node = n.parent();
1623 }
1624 result
1625 };
1626
1627 let mut current = id_node.parent();
1629 while let Some(node) = current {
1630 match node.kind() {
1631 "function_definition" | "modifier_definition" if function.is_none() => {
1632 function = ts_child_id_text(node, source).map(String::from);
1633 }
1634 "constructor_definition" if function.is_none() => {
1635 function = Some("constructor".into());
1636 }
1637 "contract_declaration" | "interface_declaration" | "library_declaration"
1638 if contract.is_none() =>
1639 {
1640 contract = ts_child_id_text(node, source).map(String::from);
1641 }
1642 _ => {}
1643 }
1644 current = node.parent();
1645 }
1646
1647 Some(CursorContext {
1648 name,
1649 function,
1650 contract,
1651 object,
1652 arg_count,
1653 arg_types,
1654 })
1655}
1656
1657#[derive(Debug, Clone)]
1659pub struct TsDeclaration {
1660 pub range: Range,
1662 pub kind: &'static str,
1664 pub container: Option<String>,
1666 pub param_count: Option<usize>,
1668 pub param_types: Vec<String>,
1671}
1672
1673pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1678 let tree = match ts_parse(source) {
1679 Some(t) => t,
1680 None => return vec![],
1681 };
1682 let mut results = Vec::new();
1683 collect_declarations(tree.root_node(), source, name, None, &mut results);
1684 results
1685}
1686
1687fn collect_declarations(
1688 node: Node,
1689 source: &str,
1690 name: &str,
1691 container: Option<&str>,
1692 out: &mut Vec<TsDeclaration>,
1693) {
1694 let mut cursor = node.walk();
1695 for child in node.children(&mut cursor) {
1696 if !child.is_named() {
1697 continue;
1698 }
1699 match child.kind() {
1700 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1701 if let Some(id_name) = ts_child_id_text(child, source) {
1702 if id_name == name {
1703 out.push(TsDeclaration {
1704 range: id_range(child),
1705 kind: child.kind(),
1706 container: container.map(String::from),
1707 param_count: None,
1708 param_types: vec![],
1709 });
1710 }
1711 if let Some(body) = ts_find_child(child, "contract_body") {
1713 collect_declarations(body, source, name, Some(id_name), out);
1714 }
1715 }
1716 }
1717 "function_definition" | "modifier_definition" => {
1718 if let Some(id_name) = ts_child_id_text(child, source) {
1719 if id_name == name {
1720 let types = parameter_type_signature(child, source);
1721 out.push(TsDeclaration {
1722 range: id_range(child),
1723 kind: child.kind(),
1724 container: container.map(String::from),
1725 param_count: Some(types.len()),
1726 param_types: types.into_iter().map(String::from).collect(),
1727 });
1728 }
1729 collect_parameters(child, source, name, container, out);
1731 if let Some(body) = ts_find_child(child, "function_body") {
1733 collect_declarations(body, source, name, container, out);
1734 }
1735 }
1736 }
1737 "constructor_definition" => {
1738 if name == "constructor" {
1739 let types = parameter_type_signature(child, source);
1740 out.push(TsDeclaration {
1741 range: ts_range(child),
1742 kind: "constructor_definition",
1743 container: container.map(String::from),
1744 param_count: Some(types.len()),
1745 param_types: types.into_iter().map(String::from).collect(),
1746 });
1747 }
1748 collect_parameters(child, source, name, container, out);
1750 if let Some(body) = ts_find_child(child, "function_body") {
1751 collect_declarations(body, source, name, container, out);
1752 }
1753 }
1754 "state_variable_declaration" | "variable_declaration" => {
1755 if let Some(id_name) = ts_child_id_text(child, source)
1756 && id_name == name
1757 {
1758 out.push(TsDeclaration {
1759 range: id_range(child),
1760 kind: child.kind(),
1761 container: container.map(String::from),
1762 param_count: None,
1763 param_types: vec![],
1764 });
1765 }
1766 }
1767 "struct_declaration" => {
1768 if let Some(id_name) = ts_child_id_text(child, source) {
1769 if id_name == name {
1770 out.push(TsDeclaration {
1771 range: id_range(child),
1772 kind: "struct_declaration",
1773 container: container.map(String::from),
1774 param_count: None,
1775 param_types: vec![],
1776 });
1777 }
1778 if let Some(body) = ts_find_child(child, "struct_body") {
1779 collect_declarations(body, source, name, Some(id_name), out);
1780 }
1781 }
1782 }
1783 "enum_declaration" => {
1784 if let Some(id_name) = ts_child_id_text(child, source) {
1785 if id_name == name {
1786 out.push(TsDeclaration {
1787 range: id_range(child),
1788 kind: "enum_declaration",
1789 container: container.map(String::from),
1790 param_count: None,
1791 param_types: vec![],
1792 });
1793 }
1794 if let Some(body) = ts_find_child(child, "enum_body") {
1796 let mut ecur = body.walk();
1797 for val in body.children(&mut ecur) {
1798 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1799 out.push(TsDeclaration {
1800 range: ts_range(val),
1801 kind: "enum_value",
1802 container: Some(id_name.to_string()),
1803 param_count: None,
1804 param_types: vec![],
1805 });
1806 }
1807 }
1808 }
1809 }
1810 }
1811 "event_definition" | "error_declaration" => {
1812 if let Some(id_name) = ts_child_id_text(child, source)
1813 && id_name == name
1814 {
1815 out.push(TsDeclaration {
1816 range: id_range(child),
1817 kind: child.kind(),
1818 container: container.map(String::from),
1819 param_count: None,
1820 param_types: vec![],
1821 });
1822 }
1823 }
1824 "user_defined_type_definition" => {
1825 if let Some(id_name) = ts_child_id_text(child, source)
1826 && id_name == name
1827 {
1828 out.push(TsDeclaration {
1829 range: id_range(child),
1830 kind: "user_defined_type_definition",
1831 container: container.map(String::from),
1832 param_count: None,
1833 param_types: vec![],
1834 });
1835 }
1836 }
1837 _ => {
1839 collect_declarations(child, source, name, container, out);
1840 }
1841 }
1842 }
1843}
1844
1845fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1851 let mut cursor = node.walk();
1852 node.children(&mut cursor)
1853 .filter(|c| c.kind() == "parameter")
1854 .filter_map(|param| {
1855 let mut pc = param.walk();
1856 param
1857 .children(&mut pc)
1858 .find(|c| {
1859 matches!(
1860 c.kind(),
1861 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1862 )
1863 })
1864 .map(|t| source[t.byte_range()].trim())
1865 })
1866 .collect()
1867}
1868
1869fn collect_parameters(
1871 node: Node,
1872 source: &str,
1873 name: &str,
1874 container: Option<&str>,
1875 out: &mut Vec<TsDeclaration>,
1876) {
1877 let mut cursor = node.walk();
1878 for child in node.children(&mut cursor) {
1879 if child.kind() == "parameter"
1880 && let Some(id_name) = ts_child_id_text(child, source)
1881 && id_name == name
1882 {
1883 out.push(TsDeclaration {
1884 range: id_range(child),
1885 kind: "parameter",
1886 container: container.map(String::from),
1887 param_count: None,
1888 param_types: vec![],
1889 });
1890 }
1891 }
1892}
1893
1894fn ts_range(node: Node) -> Range {
1896 let s = node.start_position();
1897 let e = node.end_position();
1898 Range {
1899 start: Position::new(s.row as u32, s.column as u32),
1900 end: Position::new(e.row as u32, e.column as u32),
1901 }
1902}
1903
1904fn id_range(node: Node) -> Range {
1906 let mut cursor = node.walk();
1907 node.children(&mut cursor)
1908 .find(|c| c.kind() == "identifier" && c.is_named())
1909 .map(|c| ts_range(c))
1910 .unwrap_or_else(|| ts_range(node))
1911}
1912
1913fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1914 let mut cursor = node.walk();
1915 node.children(&mut cursor).find(|c| c.kind() == kind)
1916}
1917
1918pub fn goto_definition_ts(
1926 source: &str,
1927 position: Position,
1928 file_uri: &Url,
1929 completion_cache: &crate::completion::CompletionCache,
1930 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1931) -> Option<Location> {
1932 let ctx = cursor_context(source, position)?;
1933
1934 if let Some(obj_name) = &ctx.object {
1939 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1940 let target_source = read_target_source(&path, text_cache)?;
1941 let target_uri = Url::from_file_path(&path).ok()?;
1942 let decls = find_declarations_by_name(&target_source, &ctx.name);
1943 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1944 return Some(Location {
1945 uri: target_uri,
1946 range: d.range,
1947 });
1948 }
1949 }
1950 let decls = find_declarations_by_name(source, &ctx.name);
1952 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1953 return Some(Location {
1954 uri: file_uri.clone(),
1955 range: d.range,
1956 });
1957 }
1958 }
1959
1960 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1963
1964 match resolved {
1965 Some(ResolvedTarget::SameFile) => {
1966 find_best_declaration(source, &ctx, file_uri)
1968 }
1969 Some(ResolvedTarget::OtherFile { path, name }) => {
1970 let target_source = read_target_source(&path, text_cache);
1972 let target_source = target_source?;
1973 let target_uri = Url::from_file_path(&path).ok()?;
1974 let decls = find_declarations_by_name(&target_source, &name);
1975 decls.first().map(|d| Location {
1976 uri: target_uri,
1977 range: d.range,
1978 })
1979 }
1980 None => {
1981 find_best_declaration(source, &ctx, file_uri)
1983 }
1984 }
1985}
1986
1987#[derive(Debug)]
1988enum ResolvedTarget {
1989 SameFile,
1991 OtherFile { path: String, name: String },
1993}
1994
1995fn resolve_via_cache(
2001 ctx: &CursorContext,
2002 file_uri: &Url,
2003 cache: &crate::completion::CompletionCache,
2004) -> Option<ResolvedTarget> {
2005 let contract_scope = ctx
2007 .contract
2008 .as_ref()
2009 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
2010 .copied();
2011
2012 if let Some(contract_id) = contract_scope {
2014 if let Some(func_name) = &ctx.function {
2016 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
2019 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
2021 && decls.iter().any(|d| d.name == ctx.name)
2022 {
2023 return Some(ResolvedTarget::SameFile);
2024 }
2025 }
2026 }
2027
2028 if let Some(decls) = cache.scope_declarations.get(&contract_id)
2030 && decls.iter().any(|d| d.name == ctx.name)
2031 {
2032 return Some(ResolvedTarget::SameFile);
2033 }
2034
2035 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
2037 for &base_id in bases.iter().skip(1) {
2038 if let Some(decls) = cache.scope_declarations.get(&base_id)
2039 && decls.iter().any(|d| d.name == ctx.name)
2040 {
2041 let base_name = cache
2044 .name_to_node_id
2045 .iter()
2046 .find(|&(_, &id)| id == base_id)
2047 .map(|(name, _)| name.clone());
2048
2049 if let Some(base_name) = base_name
2050 && let Some(path) =
2051 find_file_for_contract(cache, base_name.as_str(), file_uri)
2052 {
2053 return Some(ResolvedTarget::OtherFile {
2054 path,
2055 name: ctx.name.clone(),
2056 });
2057 }
2058 return Some(ResolvedTarget::SameFile);
2060 }
2061 }
2062 }
2063 }
2064
2065 if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
2067 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
2069 let current_path = file_uri.to_file_path().ok()?;
2070 let current_str = current_path.to_str()?;
2071 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
2072 return Some(ResolvedTarget::SameFile);
2073 }
2074 return Some(ResolvedTarget::OtherFile {
2075 path,
2076 name: ctx.name.clone(),
2077 });
2078 }
2079 return Some(ResolvedTarget::SameFile);
2080 }
2081
2082 if cache.name_to_type.contains_key(ctx.name.as_str()) {
2084 return Some(ResolvedTarget::SameFile);
2085 }
2086
2087 None
2088}
2089
2090fn find_function_scope(
2092 cache: &crate::completion::CompletionCache,
2093 contract_id: NodeId,
2094 func_name: &str,
2095) -> Option<NodeId> {
2096 for (&scope_id, &parent_id) in &cache.scope_parent {
2100 if parent_id == contract_id {
2101 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
2105 && contract_decls.iter().any(|d| d.name == func_name)
2106 {
2107 if cache.scope_declarations.contains_key(&scope_id)
2111 || cache.scope_parent.values().any(|&p| p == scope_id)
2112 {
2113 return Some(scope_id);
2114 }
2115 }
2116 }
2117 }
2118 None
2119}
2120
2121fn find_file_for_contract(
2123 cache: &crate::completion::CompletionCache,
2124 contract_name: &str,
2125 _file_uri: &Url,
2126) -> Option<String> {
2127 let node_id = cache.name_to_node_id.get(contract_name)?;
2132 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
2133 let file_id = scope_range.file_id;
2134
2135 cache
2137 .path_to_file_id
2138 .iter()
2139 .find(|&(_, &fid)| fid == file_id)
2140 .map(|(path, _)| path.to_string())
2141}
2142
2143fn read_target_source(
2145 path: &str,
2146 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
2147) -> Option<String> {
2148 let uri = Url::from_file_path(path).ok()?;
2150 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
2151 return Some(content.clone());
2152 }
2153 std::fs::read_to_string(path).ok()
2155}
2156
2157fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
2159 let decls = find_declarations_by_name(source, &ctx.name);
2160 if decls.is_empty() {
2161 return None;
2162 }
2163
2164 if decls.len() == 1 {
2166 return Some(Location {
2167 uri: file_uri.clone(),
2168 range: decls[0].range,
2169 });
2170 }
2171
2172 if let Some(contract_name) = &ctx.contract
2174 && let Some(d) = decls
2175 .iter()
2176 .find(|d| d.container.as_deref() == Some(contract_name))
2177 {
2178 return Some(Location {
2179 uri: file_uri.clone(),
2180 range: d.range,
2181 });
2182 }
2183
2184 Some(Location {
2186 uri: file_uri.clone(),
2187 range: decls[0].range,
2188 })
2189}
2190
2191#[derive(Debug, Clone, Copy)]
2197pub(crate) enum CodeActionKind<'a> {
2198 InsertAtFileStart { text: &'a str },
2200
2201 ReplaceToken {
2206 replacement: &'a str,
2207 walk_to: Option<&'a str>,
2208 },
2209
2210 DeleteToken,
2213
2214 DeleteLocalVar,
2217
2218 DeleteNodeByKind { node_kind: &'a str },
2222
2223 DeleteChildNode {
2226 walk_to: &'a str,
2227 child_kinds: &'a [&'a str],
2228 },
2229
2230 ReplaceChildNode {
2233 walk_to: &'a str,
2234 child_kind: &'a str,
2235 replacement: &'a str,
2236 },
2237
2238 InsertBeforeNode {
2242 walk_to: &'a str,
2243 before_child: &'a [&'a str],
2244 text: &'a str,
2245 },
2246}
2247
2248pub(crate) fn code_action_edit(
2253 source: &str,
2254 diag_range: Range,
2255 kind: CodeActionKind<'_>,
2256) -> Option<TextEdit> {
2257 let source_bytes = source.as_bytes();
2258
2259 match kind {
2260 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2262 range: Range {
2263 start: Position {
2264 line: 0,
2265 character: 0,
2266 },
2267 end: Position {
2268 line: 0,
2269 character: 0,
2270 },
2271 },
2272 new_text: text.to_string(),
2273 }),
2274
2275 CodeActionKind::ReplaceToken {
2277 replacement,
2278 walk_to,
2279 } => {
2280 let tree = ts_parse(source)?;
2281 let byte = pos_to_bytes(source_bytes, diag_range.start);
2282 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2283 if let Some(target_kind) = walk_to {
2287 loop {
2288 if node.kind() == target_kind {
2289 break;
2290 }
2291 node = node.parent()?;
2292 }
2293 }
2294 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2295 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2296 Some(TextEdit {
2297 range: Range {
2298 start: start_pos,
2299 end: end_pos,
2300 },
2301 new_text: replacement.to_string(),
2302 })
2303 }
2304
2305 CodeActionKind::DeleteToken => {
2307 let tree = ts_parse(source)?;
2308 let byte = pos_to_bytes(source_bytes, diag_range.start);
2309 let node = ts_node_at_byte(tree.root_node(), byte)?;
2310 let start = node.start_byte();
2311 let end =
2312 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2313 node.end_byte() + 1
2314 } else {
2315 node.end_byte()
2316 };
2317 let start_pos = bytes_to_pos(source_bytes, start)?;
2318 let end_pos = bytes_to_pos(source_bytes, end)?;
2319 Some(TextEdit {
2320 range: Range {
2321 start: start_pos,
2322 end: end_pos,
2323 },
2324 new_text: String::new(),
2325 })
2326 }
2327
2328 CodeActionKind::DeleteLocalVar => {
2330 let tree = ts_parse(source)?;
2331 let byte = pos_to_bytes(source_bytes, diag_range.start);
2332 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2333
2334 loop {
2335 if node.kind() == "variable_declaration_statement" {
2336 break;
2337 }
2338 node = node.parent()?;
2339 }
2340
2341 let stmt_start = node.start_byte();
2343 let delete_from = if stmt_start > 0 {
2344 let mut i = stmt_start - 1;
2345 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2346 i -= 1;
2347 }
2348 if source_bytes[i] == b'\n' {
2349 i
2350 } else {
2351 stmt_start
2352 }
2353 } else {
2354 stmt_start
2355 };
2356
2357 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2358 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2359 Some(TextEdit {
2360 range: Range {
2361 start: start_pos,
2362 end: end_pos,
2363 },
2364 new_text: String::new(),
2365 })
2366 }
2367
2368 CodeActionKind::DeleteNodeByKind { node_kind } => {
2370 let tree = ts_parse(source)?;
2371 let byte = pos_to_bytes(source_bytes, diag_range.start);
2372 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2373 loop {
2374 if node.kind() == node_kind {
2375 break;
2376 }
2377 node = node.parent()?;
2378 }
2379 let node_start = node.start_byte();
2381 let delete_from = if node_start > 0 {
2382 let mut i = node_start - 1;
2383 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2384 i -= 1;
2385 }
2386 if source_bytes[i] == b'\n' {
2387 i
2388 } else {
2389 node_start
2390 }
2391 } else {
2392 node_start
2393 };
2394 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2395 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2396 Some(TextEdit {
2397 range: Range {
2398 start: start_pos,
2399 end: end_pos,
2400 },
2401 new_text: String::new(),
2402 })
2403 }
2404
2405 CodeActionKind::DeleteChildNode {
2410 walk_to,
2411 child_kinds,
2412 } => {
2413 let tree = ts_parse(source)?;
2414 let byte = pos_to_bytes(source_bytes, diag_range.start);
2415 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2416 loop {
2417 if node.kind() == walk_to {
2418 break;
2419 }
2420 node = node.parent()?;
2421 }
2422 let mut cursor = node.walk();
2423 let children: Vec<_> = node.children(&mut cursor).collect();
2424 let child = child_kinds
2425 .iter()
2426 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2427 let start = child.start_byte();
2428 let end = if child.end_byte() < source_bytes.len()
2429 && source_bytes[child.end_byte()] == b' '
2430 {
2431 child.end_byte() + 1
2432 } else {
2433 child.end_byte()
2434 };
2435 let start_pos = bytes_to_pos(source_bytes, start)?;
2436 let end_pos = bytes_to_pos(source_bytes, end)?;
2437 Some(TextEdit {
2438 range: Range {
2439 start: start_pos,
2440 end: end_pos,
2441 },
2442 new_text: String::new(),
2443 })
2444 }
2445
2446 CodeActionKind::ReplaceChildNode {
2450 walk_to,
2451 child_kind,
2452 replacement,
2453 } => {
2454 let tree = ts_parse(source)?;
2455 let byte = pos_to_bytes(source_bytes, diag_range.start);
2456 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2457 loop {
2458 if node.kind() == walk_to {
2459 break;
2460 }
2461 node = node.parent()?;
2462 }
2463 let mut cursor = node.walk();
2464 let child = node
2465 .children(&mut cursor)
2466 .find(|c| c.kind() == child_kind)?;
2467 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2468 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2469 Some(TextEdit {
2470 range: Range {
2471 start: start_pos,
2472 end: end_pos,
2473 },
2474 new_text: replacement.to_string(),
2475 })
2476 }
2477
2478 CodeActionKind::InsertBeforeNode {
2485 walk_to,
2486 before_child,
2487 text,
2488 } => {
2489 let tree = ts_parse(source)?;
2490 let byte = pos_to_bytes(source_bytes, diag_range.start);
2491 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2492
2493 loop {
2494 if node.kind() == walk_to {
2495 break;
2496 }
2497 node = node.parent()?;
2498 }
2499
2500 let mut cursor = node.walk();
2501 let children: Vec<_> = node.children(&mut cursor).collect();
2502
2503 for target_kind in before_child {
2505 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2506 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2507 return Some(TextEdit {
2508 range: Range {
2509 start: insert_pos,
2510 end: insert_pos,
2511 },
2512 new_text: text.to_string(),
2513 });
2514 }
2515 }
2516 None
2517 }
2518 }
2519}
2520
2521#[cfg(test)]
2522mod ts_tests {
2523 use super::*;
2524
2525 #[test]
2526 fn test_cursor_context_state_var() {
2527 let source = r#"
2528contract Token {
2529 uint256 public totalSupply;
2530 function mint(uint256 amount) public {
2531 totalSupply += amount;
2532 }
2533}
2534"#;
2535 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2537 assert_eq!(ctx.name, "totalSupply");
2538 assert_eq!(ctx.function.as_deref(), Some("mint"));
2539 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2540 }
2541
2542 #[test]
2543 fn test_cursor_context_top_level() {
2544 let source = r#"
2545contract Foo {}
2546contract Bar {}
2547"#;
2548 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2550 assert_eq!(ctx.name, "Foo");
2551 assert!(ctx.function.is_none());
2552 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2554 }
2555
2556 #[test]
2557 fn test_find_declarations() {
2558 let source = r#"
2559contract Token {
2560 uint256 public totalSupply;
2561 function mint(uint256 amount) public {
2562 totalSupply += amount;
2563 }
2564}
2565"#;
2566 let decls = find_declarations_by_name(source, "totalSupply");
2567 assert_eq!(decls.len(), 1);
2568 assert_eq!(decls[0].kind, "state_variable_declaration");
2569 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2570 }
2571
2572 #[test]
2573 fn test_find_declarations_multiple_contracts() {
2574 let source = r#"
2575contract A {
2576 uint256 public value;
2577}
2578contract B {
2579 uint256 public value;
2580}
2581"#;
2582 let decls = find_declarations_by_name(source, "value");
2583 assert_eq!(decls.len(), 2);
2584 assert_eq!(decls[0].container.as_deref(), Some("A"));
2585 assert_eq!(decls[1].container.as_deref(), Some("B"));
2586 }
2587
2588 #[test]
2589 fn test_find_declarations_enum_value() {
2590 let source = "contract Foo { enum Status { Active, Paused } }";
2591 let decls = find_declarations_by_name(source, "Active");
2592 assert_eq!(decls.len(), 1);
2593 assert_eq!(decls[0].kind, "enum_value");
2594 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2595 }
2596
2597 #[test]
2598 fn test_cursor_context_short_param() {
2599 let source = r#"
2600contract Shop {
2601 uint256 public TAX;
2602 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2603 TAX = tax;
2604 }
2605}
2606"#;
2607 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2609 assert_eq!(ctx.name, "tax");
2610 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2611
2612 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2614 assert_eq!(ctx2.name, "TAX");
2615
2616 let decls = find_declarations_by_name(source, "tax");
2618 assert_eq!(decls.len(), 1);
2619 assert_eq!(decls[0].kind, "parameter");
2620
2621 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2622 assert_eq!(decls_tax_base.len(), 1);
2623 assert_eq!(decls_tax_base[0].kind, "parameter");
2624
2625 let decls_price = find_declarations_by_name(source, "price");
2626 assert_eq!(decls_price.len(), 1);
2627 assert_eq!(decls_price[0].kind, "parameter");
2628
2629 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2631 assert_eq!(decls_tax_upper.len(), 1);
2632 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2633 }
2634
2635 #[test]
2636 fn test_delete_child_node_2462_constructor_public() {
2637 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";
2640 let diag_range = Range {
2641 start: Position {
2642 line: 8,
2643 character: 4,
2644 },
2645 end: Position {
2646 line: 10,
2647 character: 5,
2648 },
2649 };
2650 let source_bytes = source.as_bytes();
2651 let tree = ts_parse(source).expect("parse failed");
2652 let byte = pos_to_bytes(source_bytes, diag_range.start);
2653 eprintln!("2462 byte offset: {byte}");
2654 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2655 loop {
2656 eprintln!(
2657 " ancestor: kind={} start={} end={}",
2658 n.kind(),
2659 n.start_byte(),
2660 n.end_byte()
2661 );
2662 if n.kind() == "constructor_definition" {
2663 let mut cursor = n.walk();
2664 for child in n.children(&mut cursor) {
2665 eprintln!(
2666 " child: kind={:?} text={:?}",
2667 child.kind(),
2668 &source[child.start_byte()..child.end_byte()]
2669 );
2670 }
2671 break;
2672 }
2673 match n.parent() {
2674 Some(p) => n = p,
2675 None => break,
2676 }
2677 }
2678 }
2679 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2680 let edit = code_action_edit(
2681 source,
2682 diag_range,
2683 CodeActionKind::DeleteChildNode {
2684 walk_to: "constructor_definition",
2685 child_kinds: &ck,
2686 },
2687 );
2688 assert!(edit.is_some(), "2462 fix returned None");
2689 let edit = edit.unwrap();
2690 assert_eq!(edit.new_text, "");
2691 let lines: Vec<&str> = source.lines().collect();
2692 let deleted = &lines[edit.range.start.line as usize]
2693 [edit.range.start.character as usize..edit.range.end.character as usize];
2694 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2695 }
2696
2697 #[test]
2698 fn test_delete_child_node_9239_constructor_private() {
2699 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";
2702 let diag_range = Range {
2703 start: Position {
2704 line: 9,
2705 character: 4,
2706 },
2707 end: Position {
2708 line: 11,
2709 character: 5,
2710 },
2711 };
2712 let ck: Vec<&str> = vec!["modifier_invocation"];
2713 let edit = code_action_edit(
2714 source,
2715 diag_range,
2716 CodeActionKind::DeleteChildNode {
2717 walk_to: "constructor_definition",
2718 child_kinds: &ck,
2719 },
2720 );
2721 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2722 let edit = edit.unwrap();
2723 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2725 let lines: Vec<&str> = source.lines().collect();
2727 let line_text = lines[edit.range.start.line as usize];
2728 let deleted =
2729 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2730 assert!(
2731 deleted.contains("private"),
2732 "expected deleted text to contain 'private', got: {:?}",
2733 deleted
2734 );
2735 }
2736
2737 #[test]
2738 fn test_find_best_declaration_same_contract() {
2739 let source = r#"
2740contract A { uint256 public x; }
2741contract B { uint256 public x; }
2742"#;
2743 let ctx = CursorContext {
2744 name: "x".into(),
2745 function: None,
2746 contract: Some("B".into()),
2747 object: None,
2748 arg_count: None,
2749 arg_types: vec![],
2750 };
2751 let uri = Url::parse("file:///test.sol").unwrap();
2752 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2753 assert_eq!(loc.range.start.line, 2);
2755 }
2756
2757 #[test]
2759 fn node_info_deserialize_old_format_with_explicit_nulls() {
2760 let json = r#"{
2761 "src": "100:20:5",
2762 "name_location": null,
2763 "name_locations": [],
2764 "referenced_declaration": null,
2765 "node_type": "Block",
2766 "member_location": null,
2767 "absolute_path": null,
2768 "scope": null,
2769 "base_functions": []
2770 }"#;
2771 let info: NodeInfo = serde_json::from_str(json).unwrap();
2772 assert_eq!(info.src.as_str(), "100:20:5");
2773 assert!(info.name_location.is_none());
2774 assert!(info.name_locations.is_empty());
2775 assert!(info.referenced_declaration.is_none());
2776 assert_eq!(info.node_type.as_deref(), Some("Block"));
2777 assert!(info.member_location.is_none());
2778 assert!(info.absolute_path.is_none());
2779 assert!(info.scope.is_none());
2780 assert!(info.base_functions.is_empty());
2781 }
2782
2783 #[test]
2785 fn node_info_deserialize_new_format_with_absent_fields() {
2786 let json = r#"{
2787 "src": "100:20:5",
2788 "node_type": "Block"
2789 }"#;
2790 let info: NodeInfo = serde_json::from_str(json).unwrap();
2791 assert_eq!(info.src.as_str(), "100:20:5");
2792 assert!(info.name_location.is_none());
2793 assert!(info.name_locations.is_empty());
2794 assert!(info.referenced_declaration.is_none());
2795 assert_eq!(info.node_type.as_deref(), Some("Block"));
2796 assert!(info.member_location.is_none());
2797 assert!(info.absolute_path.is_none());
2798 assert!(info.scope.is_none());
2799 assert!(info.base_functions.is_empty());
2800 }
2801
2802 #[test]
2804 fn node_info_round_trip_skips_null_empty() {
2805 let info = NodeInfo {
2806 src: crate::types::SrcLocation::new("50:10:3".to_string()),
2807 name_location: None,
2808 name_locations: vec![],
2809 referenced_declaration: None,
2810 node_type: Some("Identifier".to_string()),
2811 member_location: None,
2812 member_name: None,
2813 absolute_path: None,
2814 scope: None,
2815 base_functions: vec![],
2816 };
2817 let serialized = serde_json::to_string(&info).unwrap();
2818 assert!(!serialized.contains("name_location"));
2820 assert!(!serialized.contains("name_locations"));
2821 assert!(!serialized.contains("referenced_declaration"));
2822 assert!(!serialized.contains("member_location"));
2823 assert!(!serialized.contains("member_name"));
2824 assert!(!serialized.contains("absolute_path"));
2825 assert!(!serialized.contains("scope"));
2826 assert!(!serialized.contains("base_functions"));
2827 assert!(serialized.contains("\"src\""));
2829 assert!(serialized.contains("\"node_type\""));
2830
2831 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2833 assert_eq!(deserialized.src.as_str(), "50:10:3");
2834 assert!(deserialized.name_location.is_none());
2835 assert!(deserialized.base_functions.is_empty());
2836 }
2837
2838 #[test]
2840 fn node_info_round_trip_preserves_populated_fields() {
2841 use crate::types::NodeId;
2842 let info = NodeInfo {
2843 src: crate::types::SrcLocation::new("200:30:7".to_string()),
2844 name_location: Some("205:4:7".to_string()),
2845 name_locations: vec!["205:4:7".to_string(), "210:4:7".to_string()],
2846 referenced_declaration: Some(NodeId(42)),
2847 node_type: Some("FunctionDefinition".to_string()),
2848 member_location: Some("220:6:7".to_string()),
2849 member_name: Some("transfer".to_string()),
2850 absolute_path: Some("/src/Foo.sol".to_string()),
2851 scope: Some(NodeId(10)),
2852 base_functions: vec![NodeId(100), NodeId(200)],
2853 };
2854 let serialized = serde_json::to_string(&info).unwrap();
2855 assert!(serialized.contains("name_location"));
2857 assert!(serialized.contains("name_locations"));
2858 assert!(serialized.contains("referenced_declaration"));
2859 assert!(serialized.contains("member_location"));
2860 assert!(serialized.contains("member_name"));
2861 assert!(serialized.contains("absolute_path"));
2862 assert!(serialized.contains("scope"));
2863 assert!(serialized.contains("base_functions"));
2864
2865 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2866 assert_eq!(deserialized.name_location.as_deref(), Some("205:4:7"));
2867 assert_eq!(deserialized.name_locations.len(), 2);
2868 assert_eq!(deserialized.referenced_declaration, Some(NodeId(42)));
2869 assert_eq!(deserialized.scope, Some(NodeId(10)));
2870 assert_eq!(deserialized.base_functions.len(), 2);
2871 }
2872}
2873