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 && info.node_type.as_deref() == Some("IdentifierPath")
539 {
540 if 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
549 qualifier_refs
550}
551
552fn build_base_function_implementation(
562 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
563) -> HashMap<NodeId, Vec<NodeId>> {
564 let mut index: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
565
566 for file_nodes in nodes.values() {
567 for (id, info) in file_nodes {
568 if info.base_functions.is_empty() {
569 continue;
570 }
571 for &base_id in &info.base_functions {
573 index.entry(*id).or_default().push(base_id);
574 }
575 for &base_id in &info.base_functions {
577 index.entry(base_id).or_default().push(*id);
578 }
579 }
580 }
581
582 for ids in index.values_mut() {
584 ids.sort_unstable();
585 ids.dedup();
586 }
587
588 index
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct LowLevelCall {
602 pub kind: String,
604 pub src: SrcLocation,
607 pub name_src: SrcLocation,
610 pub abs_path: AbsPath,
612}
613
614pub const LOW_LEVEL_CALL_NAMES: &[&str] = &["call", "staticcall", "delegatecall"];
617
618pub const YUL_CALL_OPCODES: &[&str] = &["call", "staticcall", "delegatecall"];
622
623type CachedIds = (
625 HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
626 HashMap<RelPath, AbsPath>,
627 ExternalRefs,
628 Vec<LowLevelCall>,
629);
630
631pub fn remap_src_canonical(src: &str, remap: &HashMap<u64, FileId>) -> String {
635 let Some(last_colon) = src.rfind(':') else {
636 return src.to_owned();
637 };
638 let old_id_str = &src[last_colon + 1..];
639 let Ok(old_id) = old_id_str.parse::<u64>() else {
640 return src.to_owned();
641 };
642 let Some(canonical) = remap.get(&old_id) else {
643 return src.to_owned();
644 };
645 if canonical.0 == old_id {
646 return src.to_owned();
647 }
648 format!("{}{}", &src[..=last_colon], canonical.0)
649}
650
651fn canonicalize_node_info(info: &mut NodeInfo, remap: &HashMap<u64, FileId>) {
654 info.src = SrcLocation::new(remap_src_canonical(info.src.as_str(), remap));
655 if let Some(loc) = info.name_location.as_mut() {
656 *loc = remap_src_canonical(loc, remap);
657 }
658 for loc in &mut info.name_locations {
659 *loc = remap_src_canonical(loc, remap);
660 }
661 if let Some(loc) = info.member_location.as_mut() {
662 *loc = remap_src_canonical(loc, remap);
663 }
664}
665
666pub fn cache_ids(sources: &Value) -> CachedIds {
667 let source_count = sources.as_object().map_or(0, |obj| obj.len());
668
669 let mut nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>> =
672 HashMap::with_capacity(source_count);
673 let mut path_to_abs: HashMap<RelPath, AbsPath> = HashMap::with_capacity(source_count);
674 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
675 let mut low_level_calls: Vec<LowLevelCall> = Vec::new();
676
677 if let Some(sources_obj) = sources.as_object() {
678 for (path, source_data) in sources_obj {
679 if let Some(ast) = source_data.get("ast") {
680 let abs_path = AbsPath::new(
682 ast.get("absolutePath")
683 .and_then(|v| v.as_str())
684 .unwrap_or(path)
685 .to_string(),
686 );
687
688 path_to_abs.insert(RelPath::new(path), abs_path.clone());
689
690 let size_hint = ast
695 .get("nodes")
696 .and_then(|v| v.as_array())
697 .map_or(64, |arr| arr.len() * 8);
698 if !nodes.contains_key(&abs_path) {
699 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
700 }
701
702 if let Some(id) = ast.get("id").and_then(|v| v.as_i64())
703 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
704 {
705 nodes.get_mut(&abs_path).unwrap().insert(
706 NodeId(id),
707 NodeInfo {
708 src: SrcLocation::new(src),
709 name_location: None,
710 name_locations: vec![],
711 referenced_declaration: None,
712 node_type: ast
713 .get("nodeType")
714 .and_then(|v| v.as_str())
715 .map(|s| s.to_string()),
716 member_location: None,
717 member_name: None,
718 absolute_path: ast
719 .get("absolutePath")
720 .and_then(|v| v.as_str())
721 .map(|s| s.to_string()),
722 scope: ast.get("scope").and_then(|v| v.as_i64()).map(NodeId),
723 base_functions: vec![],
724 },
725 );
726 }
727
728 let mut stack = vec![ast];
729
730 while let Some(tree) = stack.pop() {
731 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_i64())
732 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
733 {
734 let id = NodeId(raw_id);
735 let mut name_location = tree
737 .get("nameLocation")
738 .and_then(|v| v.as_str())
739 .map(|s| s.to_string());
740
741 if name_location.is_none()
745 && let Some(name_locations) = tree.get("nameLocations")
746 && let Some(locations_array) = name_locations.as_array()
747 && !locations_array.is_empty()
748 {
749 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
750 if node_type == Some("IdentifierPath") {
751 name_location = locations_array
752 .last()
753 .and_then(|v| v.as_str())
754 .map(|s| s.to_string());
755 } else {
756 name_location = locations_array[0].as_str().map(|s| s.to_string());
757 }
758 }
759
760 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
761 && let Some(locations_array) = name_locations.as_array()
762 {
763 locations_array
764 .iter()
765 .filter_map(|v| v.as_str().map(|s| s.to_string()))
766 .collect()
767 } else {
768 vec![]
769 };
770
771 let mut final_name_location = name_location;
772 if final_name_location.is_none()
773 && let Some(member_loc) =
774 tree.get("memberLocation").and_then(|v| v.as_str())
775 {
776 final_name_location = Some(member_loc.to_string());
777 }
778
779 let node_info = NodeInfo {
780 src: SrcLocation::new(src),
781 name_location: final_name_location,
782 name_locations,
783 referenced_declaration: tree
784 .get("referencedDeclaration")
785 .and_then(|v| v.as_i64())
786 .map(NodeId),
787 node_type: tree
788 .get("nodeType")
789 .and_then(|v| v.as_str())
790 .map(|s| s.to_string()),
791 member_location: tree
792 .get("memberLocation")
793 .and_then(|v| v.as_str())
794 .map(|s| s.to_string()),
795 member_name: tree
796 .get("memberName")
797 .and_then(|v| v.as_str())
798 .map(|s| s.to_string()),
799 absolute_path: tree
800 .get("absolutePath")
801 .and_then(|v| v.as_str())
802 .map(|s| s.to_string()),
803 scope: tree.get("scope").and_then(|v| v.as_i64()).map(NodeId),
804 base_functions: tree
805 .get("baseFunctions")
806 .and_then(|v| v.as_array())
807 .map(|arr| {
808 arr.iter().filter_map(|v| v.as_i64().map(NodeId)).collect()
809 })
810 .unwrap_or_default(),
811 };
812
813 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
814
815 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
817 && let Some(ext_refs) =
818 tree.get("externalReferences").and_then(|v| v.as_array())
819 {
820 for ext_ref in ext_refs {
821 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
822 && let Some(decl_id) =
823 ext_ref.get("declaration").and_then(|v| v.as_i64())
824 {
825 external_refs
826 .insert(SrcLocation::new(src_str), NodeId(decl_id));
827 }
828 }
829 }
830 }
831
832 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("YulFunctionCall")
838 && let Some(fn_obj) = tree.get("functionName")
839 && let Some(fn_name) = fn_obj.get("name").and_then(|v| v.as_str())
840 && YUL_CALL_OPCODES.contains(&fn_name)
841 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
842 {
843 let fn_src = fn_obj.get("src").and_then(|v| v.as_str()).unwrap_or(src);
844 low_level_calls.push(LowLevelCall {
845 kind: fn_name.to_string(),
846 src: SrcLocation::new(src),
847 name_src: SrcLocation::new(fn_src),
848 abs_path: abs_path.clone(),
849 });
850 }
851
852 for key in CHILD_KEYS {
853 push_if_node_or_array(tree, key, &mut stack);
854 }
855 }
856 }
857 }
858 }
859
860 (nodes, path_to_abs, external_refs, low_level_calls)
861}
862
863pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
864 let text = String::from_utf8_lossy(source_bytes);
865 crate::utils::position_to_byte_offset(&text, position)
866}
867
868pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
869 let text = String::from_utf8_lossy(source_bytes);
870 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
871 Some(pos)
872}
873
874pub fn src_to_location(
876 src: &str,
877 id_to_path: &HashMap<crate::types::SolcFileId, String>,
878) -> Option<Location> {
879 let loc = SourceLoc::parse(src)?;
880 let file_path = id_to_path.get(&loc.file_id_str())?;
881
882 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
883 std::path::PathBuf::from(file_path)
884 } else {
885 std::env::current_dir().ok()?.join(file_path)
886 };
887
888 let source_bytes = std::fs::read(&absolute_path).ok()?;
889 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
890 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
891 let uri = Url::from_file_path(&absolute_path).ok()?;
892
893 Some(Location {
894 uri,
895 range: Range {
896 start: start_pos,
897 end: end_pos,
898 },
899 })
900}
901
902pub fn goto_bytes(
903 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
904 path_to_abs: &HashMap<RelPath, AbsPath>,
905 id_to_path: &HashMap<crate::types::SolcFileId, String>,
906 external_refs: &ExternalRefs,
907 uri: &str,
908 position: usize,
909) -> Option<(String, usize, usize)> {
910 let path = match uri.starts_with("file://") {
911 true => &uri[7..],
912 false => uri,
913 };
914
915 let abs_path = path_to_abs.get(path)?;
917
918 let current_file_nodes = nodes.get(abs_path)?;
920
921 let path_to_file_id: HashMap<&str, &crate::types::SolcFileId> =
923 id_to_path.iter().map(|(id, p)| (p.as_str(), id)).collect();
924
925 let current_file_id = path_to_file_id.get(abs_path.as_str());
929
930 for (src_str, decl_id) in external_refs {
932 let Some(src_loc) = SourceLoc::parse(src_str.as_str()) else {
933 continue;
934 };
935
936 if let Some(file_id) = current_file_id {
938 if src_loc.file_id_str() != **file_id {
939 continue;
940 }
941 } else {
942 continue;
943 }
944
945 if src_loc.offset <= position && position < src_loc.end() {
946 let mut target_node: Option<&NodeInfo> = None;
948 for file_nodes in nodes.values() {
949 if let Some(node) = file_nodes.get(decl_id) {
950 target_node = Some(node);
951 break;
952 }
953 }
954 let node = target_node?;
955 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
956 let loc = SourceLoc::parse(loc_str)?;
957 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
958 return Some((file_path, loc.offset, loc.length));
959 }
960 }
961
962 let mut refs = HashMap::new();
963
964 for (id, content) in current_file_nodes {
966 if content.referenced_declaration.is_none() {
967 continue;
968 }
969
970 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
971 continue;
972 };
973
974 if src_loc.offset <= position && position < src_loc.end() {
975 let diff = src_loc.length;
976 if !refs.contains_key(&diff) || refs[&diff] <= *id {
977 refs.insert(diff, *id);
978 }
979 }
980 }
981
982 if refs.is_empty() {
983 let tmp = current_file_nodes.iter();
986 for (_id, content) in tmp {
987 if content.node_type == Some("ImportDirective".to_string()) {
988 let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
989 continue;
990 };
991
992 if src_loc.offset <= position
993 && position < src_loc.end()
994 && let Some(import_path) = &content.absolute_path
995 {
996 return Some((import_path.clone(), 0, 0));
997 }
998 }
999 }
1000 return None;
1001 }
1002
1003 let min_diff = *refs.keys().min()?;
1005 let chosen_id = refs[&min_diff];
1006 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
1007
1008 let mut target_node: Option<&NodeInfo> = None;
1010 for file_nodes in nodes.values() {
1011 if let Some(node) = file_nodes.get(&ref_id) {
1012 target_node = Some(node);
1013 break;
1014 }
1015 }
1016
1017 let node = target_node?;
1018
1019 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1021 let loc = SourceLoc::parse(loc_str)?;
1022 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
1023
1024 Some((file_path, loc.offset, loc.length))
1025}
1026
1027fn resolve_qualifier_goto(
1032 build: &CachedBuild,
1033 file_uri: &Url,
1034 byte_position: usize,
1035) -> Option<Location> {
1036 let path = file_uri.to_file_path().ok()?;
1037 let path_str = path.to_str()?;
1038 let abs_path = build.path_to_abs.get(path_str)?;
1039 let file_nodes = build.nodes.get(abs_path)?;
1040
1041 let node_id = crate::references::byte_to_id(&build.nodes, abs_path, byte_position)?;
1043 let node_info = file_nodes.get(&node_id)?;
1044
1045 if node_info.node_type.as_deref() != Some("IdentifierPath")
1047 || node_info.name_locations.len() <= 1
1048 {
1049 return None;
1050 }
1051
1052 let first_loc = SourceLoc::parse(&node_info.name_locations[0])?;
1054 if byte_position < first_loc.offset || byte_position >= first_loc.end() {
1055 return None;
1056 }
1057
1058 let ref_decl_id = node_info.referenced_declaration?;
1060 let decl_node = find_node_info(&build.nodes, ref_decl_id)?;
1062 let scope_id = decl_node.scope?;
1063
1064 let container_node = find_node_info(&build.nodes, scope_id)?;
1066 let loc_str = container_node
1067 .name_location
1068 .as_deref()
1069 .unwrap_or(container_node.src.as_str());
1070 let loc = SourceLoc::parse(loc_str)?;
1071 let file_path = build.id_to_path_map.get(&loc.file_id_str())?;
1072
1073 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
1074 std::path::PathBuf::from(file_path)
1075 } else {
1076 std::env::current_dir().ok()?.join(file_path)
1077 };
1078
1079 let target_bytes = std::fs::read(&absolute_path).ok()?;
1080 let start_pos = bytes_to_pos(&target_bytes, loc.offset)?;
1081 let end_pos = bytes_to_pos(&target_bytes, loc.end())?;
1082 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1083
1084 Some(Location {
1085 uri: target_uri,
1086 range: Range {
1087 start: start_pos,
1088 end: end_pos,
1089 },
1090 })
1091}
1092
1093fn find_node_info<'a>(
1095 nodes: &'a HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
1096 node_id: NodeId,
1097) -> Option<&'a NodeInfo> {
1098 for file_nodes in nodes.values() {
1099 if let Some(node) = file_nodes.get(&node_id) {
1100 return Some(node);
1101 }
1102 }
1103 None
1104}
1105
1106pub fn goto_declaration_cached(
1109 build: &CachedBuild,
1110 file_uri: &Url,
1111 position: Position,
1112 source_bytes: &[u8],
1113) -> Option<Location> {
1114 let byte_position = pos_to_bytes(source_bytes, position);
1115
1116 if let Some(location) = resolve_qualifier_goto(build, file_uri, byte_position) {
1121 return Some(location);
1122 }
1123
1124 if let Some((file_path, location_bytes, length)) = goto_bytes(
1125 &build.nodes,
1126 &build.path_to_abs,
1127 &build.id_to_path_map,
1128 &build.external_refs,
1129 file_uri.as_ref(),
1130 byte_position,
1131 ) {
1132 let target_file_path = std::path::Path::new(&file_path);
1133 let absolute_path = if target_file_path.is_absolute() {
1134 target_file_path.to_path_buf()
1135 } else {
1136 let base = file_uri
1141 .to_file_path()
1142 .ok()
1143 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1144 .or_else(|| std::env::current_dir().ok())
1145 .unwrap_or_default();
1146 base.join(target_file_path)
1147 };
1148
1149 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
1150 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
1151 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
1152 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
1153 {
1154 return Some(Location {
1155 uri: target_uri,
1156 range: Range {
1157 start: start_pos,
1158 end: end_pos,
1159 },
1160 });
1161 }
1162 };
1163
1164 None
1165}
1166
1167pub fn goto_declaration_by_name(
1178 cached_build: &CachedBuild,
1179 file_uri: &Url,
1180 name: &str,
1181 byte_hint: usize,
1182) -> Option<Location> {
1183 let path = match file_uri.as_ref().starts_with("file://") {
1184 true => &file_uri.as_ref()[7..],
1185 false => file_uri.as_ref(),
1186 };
1187 let abs_path = cached_build.path_to_abs.get(path)?;
1188 let built_source = std::fs::read_to_string(abs_path).ok()?;
1190
1191 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
1193
1194 let tmp = {
1195 let this = cached_build.nodes.get(abs_path)?;
1196 this.iter()
1197 };
1198 for (_id, node) in tmp {
1199 let ref_id = match node.referenced_declaration {
1200 Some(id) => id,
1201 None => continue,
1202 };
1203
1204 let Some(src_loc) = SourceLoc::parse(node.src.as_str()) else {
1206 continue;
1207 };
1208 let start = src_loc.offset;
1209 let length = src_loc.length;
1210
1211 if start + length > built_source.len() {
1212 continue;
1213 }
1214
1215 let node_text = &built_source[start..start + length];
1216
1217 let matches = node_text == name
1222 || node_text.contains(&format!(".{name}("))
1223 || node_text.ends_with(&format!(".{name}"));
1224
1225 if matches {
1226 let distance = if byte_hint >= start && byte_hint < start + length {
1230 0 } else if byte_hint < start {
1232 start - byte_hint
1233 } else {
1234 byte_hint - (start + length)
1235 };
1236 candidates.push((distance, length, ref_id));
1237 }
1238 }
1239
1240 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1242 let ref_id = candidates.first()?.2;
1243
1244 let mut target_node: Option<&NodeInfo> = None;
1246 for file_nodes in cached_build.nodes.values() {
1247 if let Some(node) = file_nodes.get(&ref_id) {
1248 target_node = Some(node);
1249 break;
1250 }
1251 }
1252
1253 let node = target_node?;
1254
1255 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1257 let loc = SourceLoc::parse(loc_str)?;
1258
1259 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1260 let location_bytes = loc.offset;
1261 let length = loc.length;
1262
1263 let target_file_path = std::path::Path::new(file_path);
1264 let absolute_path = if target_file_path.is_absolute() {
1265 target_file_path.to_path_buf()
1266 } else {
1267 let base = file_uri
1268 .to_file_path()
1269 .ok()
1270 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1271 .or_else(|| std::env::current_dir().ok())
1272 .unwrap_or_default();
1273 base.join(target_file_path)
1274 };
1275
1276 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1277 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1278 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1279 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1280
1281 Some(Location {
1282 uri: target_uri,
1283 range: Range {
1284 start: start_pos,
1285 end: end_pos,
1286 },
1287 })
1288}
1289
1290#[derive(Debug, Clone)]
1294pub struct CursorContext {
1295 pub name: String,
1297 pub function: Option<String>,
1299 pub contract: Option<String>,
1301 pub object: Option<String>,
1305 pub arg_count: Option<usize>,
1308 pub arg_types: Vec<Option<String>>,
1311}
1312
1313fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1315 let mut parser = Parser::new();
1316 parser
1317 .set_language(&tree_sitter_solidity::LANGUAGE.into())
1318 .expect("failed to load Solidity grammar");
1319 parser.parse(source, None)
1320}
1321
1322pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1328 let line = location.range.start.line as usize;
1329 let start_col = location.range.start.character as usize;
1330 let end_col = location.range.end.character as usize;
1331
1332 if let Some(line_text) = target_source.lines().nth(line)
1333 && end_col <= line_text.len()
1334 {
1335 return &line_text[start_col..end_col] == expected_name;
1336 }
1337 true
1339}
1340
1341fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1343 if byte < node.start_byte() || byte >= node.end_byte() {
1344 return None;
1345 }
1346 let mut cursor = node.walk();
1347 for child in node.children(&mut cursor) {
1348 if child.start_byte() <= byte
1349 && byte < child.end_byte()
1350 && let Some(deeper) = ts_node_at_byte(child, byte)
1351 {
1352 return Some(deeper);
1353 }
1354 }
1355 Some(node)
1356}
1357
1358fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1360 let mut cursor = node.walk();
1361 node.children(&mut cursor)
1362 .find(|c| c.kind() == "identifier" && c.is_named())
1363 .map(|c| &source[c.byte_range()])
1364}
1365
1366fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1372 let expr = if arg_node.kind() == "call_argument" {
1374 let mut c = arg_node.walk();
1375 arg_node.children(&mut c).find(|ch| ch.is_named())?
1376 } else {
1377 arg_node
1378 };
1379
1380 match expr.kind() {
1381 "identifier" => {
1382 let var_name = &source[expr.byte_range()];
1383 find_variable_type(expr, source, var_name)
1385 }
1386 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1387 "boolean_literal" => Some("bool".into()),
1388 "string_literal" | "hex_string_literal" => Some("string".into()),
1389 _ => None,
1390 }
1391}
1392
1393fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1398 let mut scope = from.parent();
1399 while let Some(node) = scope {
1400 match node.kind() {
1401 "function_definition" | "modifier_definition" | "constructor_definition" => {
1402 let mut c = node.walk();
1404 for child in node.children(&mut c) {
1405 if child.kind() == "parameter"
1406 && let Some(id) = ts_child_id_text(child, source)
1407 && id == var_name
1408 {
1409 let mut pc = child.walk();
1411 return child
1412 .children(&mut pc)
1413 .find(|c| {
1414 matches!(
1415 c.kind(),
1416 "type_name"
1417 | "primitive_type"
1418 | "user_defined_type"
1419 | "mapping"
1420 )
1421 })
1422 .map(|t| source[t.byte_range()].trim().to_string());
1423 }
1424 }
1425 }
1426 "function_body" | "block_statement" | "unchecked_block" => {
1427 let mut c = node.walk();
1429 for child in node.children(&mut c) {
1430 if (child.kind() == "variable_declaration_statement"
1431 || child.kind() == "variable_declaration")
1432 && let Some(id) = ts_child_id_text(child, source)
1433 && id == var_name
1434 {
1435 let mut pc = child.walk();
1436 return child
1437 .children(&mut pc)
1438 .find(|c| {
1439 matches!(
1440 c.kind(),
1441 "type_name"
1442 | "primitive_type"
1443 | "user_defined_type"
1444 | "mapping"
1445 )
1446 })
1447 .map(|t| source[t.byte_range()].trim().to_string());
1448 }
1449 }
1450 }
1451 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1452 if let Some(body) = ts_find_child(node, "contract_body") {
1454 let mut c = body.walk();
1455 for child in body.children(&mut c) {
1456 if child.kind() == "state_variable_declaration"
1457 && let Some(id) = ts_child_id_text(child, source)
1458 && id == var_name
1459 {
1460 let mut pc = child.walk();
1461 return child
1462 .children(&mut pc)
1463 .find(|c| {
1464 matches!(
1465 c.kind(),
1466 "type_name"
1467 | "primitive_type"
1468 | "user_defined_type"
1469 | "mapping"
1470 )
1471 })
1472 .map(|t| source[t.byte_range()].trim().to_string());
1473 }
1474 }
1475 }
1476 }
1477 _ => {}
1478 }
1479 scope = node.parent();
1480 }
1481 None
1482}
1483
1484fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1486 let mut cursor = call_node.walk();
1487 call_node
1488 .children(&mut cursor)
1489 .filter(|c| c.kind() == "call_argument")
1490 .map(|arg| infer_argument_type(arg, source))
1491 .collect()
1492}
1493
1494fn best_overload<'a>(
1502 decls: &'a [TsDeclaration],
1503 arg_count: Option<usize>,
1504 arg_types: &[Option<String>],
1505) -> Option<&'a TsDeclaration> {
1506 if decls.len() == 1 {
1507 return decls.first();
1508 }
1509 if decls.is_empty() {
1510 return None;
1511 }
1512
1513 let func_decls: Vec<&TsDeclaration> =
1515 decls.iter().filter(|d| d.param_count.is_some()).collect();
1516
1517 if func_decls.is_empty() {
1518 return decls.first();
1519 }
1520
1521 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1523 let matched: Vec<_> = func_decls
1524 .iter()
1525 .filter(|d| d.param_count == Some(ac))
1526 .collect();
1527 if matched.len() == 1 {
1528 return Some(matched[0]);
1529 }
1530 if matched.is_empty() {
1531 func_decls.iter().collect()
1533 } else {
1534 matched
1535 }
1536 } else {
1537 func_decls.iter().collect()
1538 };
1539
1540 if !arg_types.is_empty() {
1542 let mut best: Option<(&TsDeclaration, usize)> = None;
1543 for &&decl in &count_matched {
1544 let score = arg_types
1545 .iter()
1546 .zip(decl.param_types.iter())
1547 .filter(|(arg_ty, param_ty)| {
1548 if let Some(at) = arg_ty {
1549 at == param_ty.as_str()
1550 } else {
1551 false
1552 }
1553 })
1554 .count();
1555 if best.is_none() || score > best.unwrap().1 {
1556 best = Some((decl, score));
1557 }
1558 }
1559 if let Some((decl, _)) = best {
1560 return Some(decl);
1561 }
1562 }
1563
1564 count_matched.first().map(|d| **d).or(decls.first())
1566}
1567
1568pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1572 let tree = ts_parse(source)?;
1573 let byte = pos_to_bytes(source.as_bytes(), position);
1574 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1575
1576 let id_node = if leaf.kind() == "identifier" {
1578 leaf
1579 } else {
1580 let parent = leaf.parent()?;
1582 if parent.kind() == "identifier" {
1583 parent
1584 } else {
1585 return None;
1586 }
1587 };
1588
1589 let name = source[id_node.byte_range()].to_string();
1590 let mut function = None;
1591 let mut contract = None;
1592
1593 let object = id_node.parent().and_then(|parent| {
1597 if parent.kind() == "member_expression" {
1598 let prop = parent.child_by_field_name("property")?;
1599 if prop.id() == id_node.id() {
1601 let obj = parent.child_by_field_name("object")?;
1602 Some(source[obj.byte_range()].to_string())
1603 } else {
1604 None
1605 }
1606 } else {
1607 None
1608 }
1609 });
1610
1611 let (arg_count, arg_types) = {
1615 let mut node = id_node.parent();
1616 let mut result = (None, vec![]);
1617 while let Some(n) = node {
1618 if n.kind() == "call_expression" {
1619 let types = infer_call_arg_types(n, source);
1620 result = (Some(types.len()), types);
1621 break;
1622 }
1623 node = n.parent();
1624 }
1625 result
1626 };
1627
1628 let mut current = id_node.parent();
1630 while let Some(node) = current {
1631 match node.kind() {
1632 "function_definition" | "modifier_definition" if function.is_none() => {
1633 function = ts_child_id_text(node, source).map(String::from);
1634 }
1635 "constructor_definition" if function.is_none() => {
1636 function = Some("constructor".into());
1637 }
1638 "contract_declaration" | "interface_declaration" | "library_declaration"
1639 if contract.is_none() =>
1640 {
1641 contract = ts_child_id_text(node, source).map(String::from);
1642 }
1643 _ => {}
1644 }
1645 current = node.parent();
1646 }
1647
1648 Some(CursorContext {
1649 name,
1650 function,
1651 contract,
1652 object,
1653 arg_count,
1654 arg_types,
1655 })
1656}
1657
1658#[derive(Debug, Clone)]
1660pub struct TsDeclaration {
1661 pub range: Range,
1663 pub kind: &'static str,
1665 pub container: Option<String>,
1667 pub param_count: Option<usize>,
1669 pub param_types: Vec<String>,
1672}
1673
1674pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1679 let tree = match ts_parse(source) {
1680 Some(t) => t,
1681 None => return vec![],
1682 };
1683 let mut results = Vec::new();
1684 collect_declarations(tree.root_node(), source, name, None, &mut results);
1685 results
1686}
1687
1688fn collect_declarations(
1689 node: Node,
1690 source: &str,
1691 name: &str,
1692 container: Option<&str>,
1693 out: &mut Vec<TsDeclaration>,
1694) {
1695 let mut cursor = node.walk();
1696 for child in node.children(&mut cursor) {
1697 if !child.is_named() {
1698 continue;
1699 }
1700 match child.kind() {
1701 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1702 if let Some(id_name) = ts_child_id_text(child, source) {
1703 if id_name == name {
1704 out.push(TsDeclaration {
1705 range: id_range(child),
1706 kind: child.kind(),
1707 container: container.map(String::from),
1708 param_count: None,
1709 param_types: vec![],
1710 });
1711 }
1712 if let Some(body) = ts_find_child(child, "contract_body") {
1714 collect_declarations(body, source, name, Some(id_name), out);
1715 }
1716 }
1717 }
1718 "function_definition" | "modifier_definition" => {
1719 if let Some(id_name) = ts_child_id_text(child, source) {
1720 if id_name == name {
1721 let types = parameter_type_signature(child, source);
1722 out.push(TsDeclaration {
1723 range: id_range(child),
1724 kind: child.kind(),
1725 container: container.map(String::from),
1726 param_count: Some(types.len()),
1727 param_types: types.into_iter().map(String::from).collect(),
1728 });
1729 }
1730 collect_parameters(child, source, name, container, out);
1732 if let Some(body) = ts_find_child(child, "function_body") {
1734 collect_declarations(body, source, name, container, out);
1735 }
1736 }
1737 }
1738 "constructor_definition" => {
1739 if name == "constructor" {
1740 let types = parameter_type_signature(child, source);
1741 out.push(TsDeclaration {
1742 range: ts_range(child),
1743 kind: "constructor_definition",
1744 container: container.map(String::from),
1745 param_count: Some(types.len()),
1746 param_types: types.into_iter().map(String::from).collect(),
1747 });
1748 }
1749 collect_parameters(child, source, name, container, out);
1751 if let Some(body) = ts_find_child(child, "function_body") {
1752 collect_declarations(body, source, name, container, out);
1753 }
1754 }
1755 "state_variable_declaration" | "variable_declaration" => {
1756 if let Some(id_name) = ts_child_id_text(child, source)
1757 && id_name == name
1758 {
1759 out.push(TsDeclaration {
1760 range: id_range(child),
1761 kind: child.kind(),
1762 container: container.map(String::from),
1763 param_count: None,
1764 param_types: vec![],
1765 });
1766 }
1767 }
1768 "struct_declaration" => {
1769 if let Some(id_name) = ts_child_id_text(child, source) {
1770 if id_name == name {
1771 out.push(TsDeclaration {
1772 range: id_range(child),
1773 kind: "struct_declaration",
1774 container: container.map(String::from),
1775 param_count: None,
1776 param_types: vec![],
1777 });
1778 }
1779 if let Some(body) = ts_find_child(child, "struct_body") {
1780 collect_declarations(body, source, name, Some(id_name), out);
1781 }
1782 }
1783 }
1784 "enum_declaration" => {
1785 if let Some(id_name) = ts_child_id_text(child, source) {
1786 if id_name == name {
1787 out.push(TsDeclaration {
1788 range: id_range(child),
1789 kind: "enum_declaration",
1790 container: container.map(String::from),
1791 param_count: None,
1792 param_types: vec![],
1793 });
1794 }
1795 if let Some(body) = ts_find_child(child, "enum_body") {
1797 let mut ecur = body.walk();
1798 for val in body.children(&mut ecur) {
1799 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1800 out.push(TsDeclaration {
1801 range: ts_range(val),
1802 kind: "enum_value",
1803 container: Some(id_name.to_string()),
1804 param_count: None,
1805 param_types: vec![],
1806 });
1807 }
1808 }
1809 }
1810 }
1811 }
1812 "event_definition" | "error_declaration" => {
1813 if let Some(id_name) = ts_child_id_text(child, source)
1814 && id_name == name
1815 {
1816 out.push(TsDeclaration {
1817 range: id_range(child),
1818 kind: child.kind(),
1819 container: container.map(String::from),
1820 param_count: None,
1821 param_types: vec![],
1822 });
1823 }
1824 }
1825 "user_defined_type_definition" => {
1826 if let Some(id_name) = ts_child_id_text(child, source)
1827 && id_name == name
1828 {
1829 out.push(TsDeclaration {
1830 range: id_range(child),
1831 kind: "user_defined_type_definition",
1832 container: container.map(String::from),
1833 param_count: None,
1834 param_types: vec![],
1835 });
1836 }
1837 }
1838 _ => {
1840 collect_declarations(child, source, name, container, out);
1841 }
1842 }
1843 }
1844}
1845
1846fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1852 let mut cursor = node.walk();
1853 node.children(&mut cursor)
1854 .filter(|c| c.kind() == "parameter")
1855 .filter_map(|param| {
1856 let mut pc = param.walk();
1857 param
1858 .children(&mut pc)
1859 .find(|c| {
1860 matches!(
1861 c.kind(),
1862 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1863 )
1864 })
1865 .map(|t| source[t.byte_range()].trim())
1866 })
1867 .collect()
1868}
1869
1870fn collect_parameters(
1872 node: Node,
1873 source: &str,
1874 name: &str,
1875 container: Option<&str>,
1876 out: &mut Vec<TsDeclaration>,
1877) {
1878 let mut cursor = node.walk();
1879 for child in node.children(&mut cursor) {
1880 if child.kind() == "parameter"
1881 && let Some(id_name) = ts_child_id_text(child, source)
1882 && id_name == name
1883 {
1884 out.push(TsDeclaration {
1885 range: id_range(child),
1886 kind: "parameter",
1887 container: container.map(String::from),
1888 param_count: None,
1889 param_types: vec![],
1890 });
1891 }
1892 }
1893}
1894
1895fn ts_range(node: Node) -> Range {
1897 let s = node.start_position();
1898 let e = node.end_position();
1899 Range {
1900 start: Position::new(s.row as u32, s.column as u32),
1901 end: Position::new(e.row as u32, e.column as u32),
1902 }
1903}
1904
1905fn id_range(node: Node) -> Range {
1907 let mut cursor = node.walk();
1908 node.children(&mut cursor)
1909 .find(|c| c.kind() == "identifier" && c.is_named())
1910 .map(|c| ts_range(c))
1911 .unwrap_or_else(|| ts_range(node))
1912}
1913
1914fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1915 let mut cursor = node.walk();
1916 node.children(&mut cursor).find(|c| c.kind() == kind)
1917}
1918
1919pub fn goto_definition_ts(
1927 source: &str,
1928 position: Position,
1929 file_uri: &Url,
1930 completion_cache: &crate::completion::CompletionCache,
1931 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1932) -> Option<Location> {
1933 let ctx = cursor_context(source, position)?;
1934
1935 if let Some(obj_name) = &ctx.object {
1940 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1941 let target_source = read_target_source(&path, text_cache)?;
1942 let target_uri = Url::from_file_path(&path).ok()?;
1943 let decls = find_declarations_by_name(&target_source, &ctx.name);
1944 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1945 return Some(Location {
1946 uri: target_uri,
1947 range: d.range,
1948 });
1949 }
1950 }
1951 let decls = find_declarations_by_name(source, &ctx.name);
1953 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1954 return Some(Location {
1955 uri: file_uri.clone(),
1956 range: d.range,
1957 });
1958 }
1959 }
1960
1961 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1964
1965 match resolved {
1966 Some(ResolvedTarget::SameFile) => {
1967 find_best_declaration(source, &ctx, file_uri)
1969 }
1970 Some(ResolvedTarget::OtherFile { path, name }) => {
1971 let target_source = read_target_source(&path, text_cache);
1973 let target_source = target_source?;
1974 let target_uri = Url::from_file_path(&path).ok()?;
1975 let decls = find_declarations_by_name(&target_source, &name);
1976 decls.first().map(|d| Location {
1977 uri: target_uri,
1978 range: d.range,
1979 })
1980 }
1981 None => {
1982 find_best_declaration(source, &ctx, file_uri)
1984 }
1985 }
1986}
1987
1988#[derive(Debug)]
1989enum ResolvedTarget {
1990 SameFile,
1992 OtherFile { path: String, name: String },
1994}
1995
1996fn resolve_via_cache(
2002 ctx: &CursorContext,
2003 file_uri: &Url,
2004 cache: &crate::completion::CompletionCache,
2005) -> Option<ResolvedTarget> {
2006 let contract_scope = ctx
2008 .contract
2009 .as_ref()
2010 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
2011 .copied();
2012
2013 if let Some(contract_id) = contract_scope {
2015 if let Some(func_name) = &ctx.function {
2017 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
2020 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
2022 && decls.iter().any(|d| d.name == ctx.name)
2023 {
2024 return Some(ResolvedTarget::SameFile);
2025 }
2026 }
2027 }
2028
2029 if let Some(decls) = cache.scope_declarations.get(&contract_id)
2031 && decls.iter().any(|d| d.name == ctx.name)
2032 {
2033 return Some(ResolvedTarget::SameFile);
2034 }
2035
2036 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
2038 for &base_id in bases.iter().skip(1) {
2039 if let Some(decls) = cache.scope_declarations.get(&base_id)
2040 && decls.iter().any(|d| d.name == ctx.name)
2041 {
2042 let base_name = cache
2045 .name_to_node_id
2046 .iter()
2047 .find(|&(_, &id)| id == base_id)
2048 .map(|(name, _)| name.clone());
2049
2050 if let Some(base_name) = base_name
2051 && let Some(path) =
2052 find_file_for_contract(cache, base_name.as_str(), file_uri)
2053 {
2054 return Some(ResolvedTarget::OtherFile {
2055 path,
2056 name: ctx.name.clone(),
2057 });
2058 }
2059 return Some(ResolvedTarget::SameFile);
2061 }
2062 }
2063 }
2064 }
2065
2066 if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
2068 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
2070 let current_path = file_uri.to_file_path().ok()?;
2071 let current_str = current_path.to_str()?;
2072 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
2073 return Some(ResolvedTarget::SameFile);
2074 }
2075 return Some(ResolvedTarget::OtherFile {
2076 path,
2077 name: ctx.name.clone(),
2078 });
2079 }
2080 return Some(ResolvedTarget::SameFile);
2081 }
2082
2083 if cache.name_to_type.contains_key(ctx.name.as_str()) {
2085 return Some(ResolvedTarget::SameFile);
2086 }
2087
2088 None
2089}
2090
2091fn find_function_scope(
2093 cache: &crate::completion::CompletionCache,
2094 contract_id: NodeId,
2095 func_name: &str,
2096) -> Option<NodeId> {
2097 for (&scope_id, &parent_id) in &cache.scope_parent {
2101 if parent_id == contract_id {
2102 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
2106 && contract_decls.iter().any(|d| d.name == func_name)
2107 {
2108 if cache.scope_declarations.contains_key(&scope_id)
2112 || cache.scope_parent.values().any(|&p| p == scope_id)
2113 {
2114 return Some(scope_id);
2115 }
2116 }
2117 }
2118 }
2119 None
2120}
2121
2122fn find_file_for_contract(
2124 cache: &crate::completion::CompletionCache,
2125 contract_name: &str,
2126 _file_uri: &Url,
2127) -> Option<String> {
2128 let node_id = cache.name_to_node_id.get(contract_name)?;
2133 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
2134 let file_id = scope_range.file_id;
2135
2136 cache
2138 .path_to_file_id
2139 .iter()
2140 .find(|&(_, &fid)| fid == file_id)
2141 .map(|(path, _)| path.to_string())
2142}
2143
2144fn read_target_source(
2146 path: &str,
2147 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
2148) -> Option<String> {
2149 let uri = Url::from_file_path(path).ok()?;
2151 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
2152 return Some(content.clone());
2153 }
2154 std::fs::read_to_string(path).ok()
2156}
2157
2158fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
2160 let decls = find_declarations_by_name(source, &ctx.name);
2161 if decls.is_empty() {
2162 return None;
2163 }
2164
2165 if decls.len() == 1 {
2167 return Some(Location {
2168 uri: file_uri.clone(),
2169 range: decls[0].range,
2170 });
2171 }
2172
2173 if let Some(contract_name) = &ctx.contract
2175 && let Some(d) = decls
2176 .iter()
2177 .find(|d| d.container.as_deref() == Some(contract_name))
2178 {
2179 return Some(Location {
2180 uri: file_uri.clone(),
2181 range: d.range,
2182 });
2183 }
2184
2185 Some(Location {
2187 uri: file_uri.clone(),
2188 range: decls[0].range,
2189 })
2190}
2191
2192#[derive(Debug, Clone, Copy)]
2198pub(crate) enum CodeActionKind<'a> {
2199 InsertAtFileStart { text: &'a str },
2201
2202 ReplaceToken {
2207 replacement: &'a str,
2208 walk_to: Option<&'a str>,
2209 },
2210
2211 DeleteToken,
2214
2215 DeleteLocalVar,
2218
2219 DeleteNodeByKind { node_kind: &'a str },
2223
2224 DeleteChildNode {
2227 walk_to: &'a str,
2228 child_kinds: &'a [&'a str],
2229 },
2230
2231 ReplaceChildNode {
2234 walk_to: &'a str,
2235 child_kind: &'a str,
2236 replacement: &'a str,
2237 },
2238
2239 InsertBeforeNode {
2243 walk_to: &'a str,
2244 before_child: &'a [&'a str],
2245 text: &'a str,
2246 },
2247}
2248
2249pub(crate) fn code_action_edit(
2254 source: &str,
2255 diag_range: Range,
2256 kind: CodeActionKind<'_>,
2257) -> Option<TextEdit> {
2258 let source_bytes = source.as_bytes();
2259
2260 match kind {
2261 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2263 range: Range {
2264 start: Position {
2265 line: 0,
2266 character: 0,
2267 },
2268 end: Position {
2269 line: 0,
2270 character: 0,
2271 },
2272 },
2273 new_text: text.to_string(),
2274 }),
2275
2276 CodeActionKind::ReplaceToken {
2278 replacement,
2279 walk_to,
2280 } => {
2281 let tree = ts_parse(source)?;
2282 let byte = pos_to_bytes(source_bytes, diag_range.start);
2283 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2284 if let Some(target_kind) = walk_to {
2288 loop {
2289 if node.kind() == target_kind {
2290 break;
2291 }
2292 node = node.parent()?;
2293 }
2294 }
2295 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2296 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2297 Some(TextEdit {
2298 range: Range {
2299 start: start_pos,
2300 end: end_pos,
2301 },
2302 new_text: replacement.to_string(),
2303 })
2304 }
2305
2306 CodeActionKind::DeleteToken => {
2308 let tree = ts_parse(source)?;
2309 let byte = pos_to_bytes(source_bytes, diag_range.start);
2310 let node = ts_node_at_byte(tree.root_node(), byte)?;
2311 let start = node.start_byte();
2312 let end =
2313 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2314 node.end_byte() + 1
2315 } else {
2316 node.end_byte()
2317 };
2318 let start_pos = bytes_to_pos(source_bytes, start)?;
2319 let end_pos = bytes_to_pos(source_bytes, end)?;
2320 Some(TextEdit {
2321 range: Range {
2322 start: start_pos,
2323 end: end_pos,
2324 },
2325 new_text: String::new(),
2326 })
2327 }
2328
2329 CodeActionKind::DeleteLocalVar => {
2331 let tree = ts_parse(source)?;
2332 let byte = pos_to_bytes(source_bytes, diag_range.start);
2333 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2334
2335 loop {
2336 if node.kind() == "variable_declaration_statement" {
2337 break;
2338 }
2339 node = node.parent()?;
2340 }
2341
2342 let stmt_start = node.start_byte();
2344 let delete_from = if stmt_start > 0 {
2345 let mut i = stmt_start - 1;
2346 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2347 i -= 1;
2348 }
2349 if source_bytes[i] == b'\n' {
2350 i
2351 } else {
2352 stmt_start
2353 }
2354 } else {
2355 stmt_start
2356 };
2357
2358 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2359 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2360 Some(TextEdit {
2361 range: Range {
2362 start: start_pos,
2363 end: end_pos,
2364 },
2365 new_text: String::new(),
2366 })
2367 }
2368
2369 CodeActionKind::DeleteNodeByKind { node_kind } => {
2371 let tree = ts_parse(source)?;
2372 let byte = pos_to_bytes(source_bytes, diag_range.start);
2373 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2374 loop {
2375 if node.kind() == node_kind {
2376 break;
2377 }
2378 node = node.parent()?;
2379 }
2380 let node_start = node.start_byte();
2382 let delete_from = if node_start > 0 {
2383 let mut i = node_start - 1;
2384 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2385 i -= 1;
2386 }
2387 if source_bytes[i] == b'\n' {
2388 i
2389 } else {
2390 node_start
2391 }
2392 } else {
2393 node_start
2394 };
2395 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2396 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2397 Some(TextEdit {
2398 range: Range {
2399 start: start_pos,
2400 end: end_pos,
2401 },
2402 new_text: String::new(),
2403 })
2404 }
2405
2406 CodeActionKind::DeleteChildNode {
2411 walk_to,
2412 child_kinds,
2413 } => {
2414 let tree = ts_parse(source)?;
2415 let byte = pos_to_bytes(source_bytes, diag_range.start);
2416 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2417 loop {
2418 if node.kind() == walk_to {
2419 break;
2420 }
2421 node = node.parent()?;
2422 }
2423 let mut cursor = node.walk();
2424 let children: Vec<_> = node.children(&mut cursor).collect();
2425 let child = child_kinds
2426 .iter()
2427 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2428 let start = child.start_byte();
2429 let end = if child.end_byte() < source_bytes.len()
2430 && source_bytes[child.end_byte()] == b' '
2431 {
2432 child.end_byte() + 1
2433 } else {
2434 child.end_byte()
2435 };
2436 let start_pos = bytes_to_pos(source_bytes, start)?;
2437 let end_pos = bytes_to_pos(source_bytes, end)?;
2438 Some(TextEdit {
2439 range: Range {
2440 start: start_pos,
2441 end: end_pos,
2442 },
2443 new_text: String::new(),
2444 })
2445 }
2446
2447 CodeActionKind::ReplaceChildNode {
2451 walk_to,
2452 child_kind,
2453 replacement,
2454 } => {
2455 let tree = ts_parse(source)?;
2456 let byte = pos_to_bytes(source_bytes, diag_range.start);
2457 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2458 loop {
2459 if node.kind() == walk_to {
2460 break;
2461 }
2462 node = node.parent()?;
2463 }
2464 let mut cursor = node.walk();
2465 let child = node
2466 .children(&mut cursor)
2467 .find(|c| c.kind() == child_kind)?;
2468 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2469 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2470 Some(TextEdit {
2471 range: Range {
2472 start: start_pos,
2473 end: end_pos,
2474 },
2475 new_text: replacement.to_string(),
2476 })
2477 }
2478
2479 CodeActionKind::InsertBeforeNode {
2486 walk_to,
2487 before_child,
2488 text,
2489 } => {
2490 let tree = ts_parse(source)?;
2491 let byte = pos_to_bytes(source_bytes, diag_range.start);
2492 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2493
2494 loop {
2495 if node.kind() == walk_to {
2496 break;
2497 }
2498 node = node.parent()?;
2499 }
2500
2501 let mut cursor = node.walk();
2502 let children: Vec<_> = node.children(&mut cursor).collect();
2503
2504 for target_kind in before_child {
2506 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2507 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2508 return Some(TextEdit {
2509 range: Range {
2510 start: insert_pos,
2511 end: insert_pos,
2512 },
2513 new_text: text.to_string(),
2514 });
2515 }
2516 }
2517 None
2518 }
2519 }
2520}
2521
2522#[cfg(test)]
2523mod ts_tests {
2524 use super::*;
2525
2526 #[test]
2527 fn test_cursor_context_state_var() {
2528 let source = r#"
2529contract Token {
2530 uint256 public totalSupply;
2531 function mint(uint256 amount) public {
2532 totalSupply += amount;
2533 }
2534}
2535"#;
2536 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2538 assert_eq!(ctx.name, "totalSupply");
2539 assert_eq!(ctx.function.as_deref(), Some("mint"));
2540 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2541 }
2542
2543 #[test]
2544 fn test_cursor_context_top_level() {
2545 let source = r#"
2546contract Foo {}
2547contract Bar {}
2548"#;
2549 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2551 assert_eq!(ctx.name, "Foo");
2552 assert!(ctx.function.is_none());
2553 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2555 }
2556
2557 #[test]
2558 fn test_find_declarations() {
2559 let source = r#"
2560contract Token {
2561 uint256 public totalSupply;
2562 function mint(uint256 amount) public {
2563 totalSupply += amount;
2564 }
2565}
2566"#;
2567 let decls = find_declarations_by_name(source, "totalSupply");
2568 assert_eq!(decls.len(), 1);
2569 assert_eq!(decls[0].kind, "state_variable_declaration");
2570 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2571 }
2572
2573 #[test]
2574 fn test_find_declarations_multiple_contracts() {
2575 let source = r#"
2576contract A {
2577 uint256 public value;
2578}
2579contract B {
2580 uint256 public value;
2581}
2582"#;
2583 let decls = find_declarations_by_name(source, "value");
2584 assert_eq!(decls.len(), 2);
2585 assert_eq!(decls[0].container.as_deref(), Some("A"));
2586 assert_eq!(decls[1].container.as_deref(), Some("B"));
2587 }
2588
2589 #[test]
2590 fn test_find_declarations_enum_value() {
2591 let source = "contract Foo { enum Status { Active, Paused } }";
2592 let decls = find_declarations_by_name(source, "Active");
2593 assert_eq!(decls.len(), 1);
2594 assert_eq!(decls[0].kind, "enum_value");
2595 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2596 }
2597
2598 #[test]
2599 fn test_cursor_context_short_param() {
2600 let source = r#"
2601contract Shop {
2602 uint256 public TAX;
2603 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2604 TAX = tax;
2605 }
2606}
2607"#;
2608 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2610 assert_eq!(ctx.name, "tax");
2611 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2612
2613 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2615 assert_eq!(ctx2.name, "TAX");
2616
2617 let decls = find_declarations_by_name(source, "tax");
2619 assert_eq!(decls.len(), 1);
2620 assert_eq!(decls[0].kind, "parameter");
2621
2622 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2623 assert_eq!(decls_tax_base.len(), 1);
2624 assert_eq!(decls_tax_base[0].kind, "parameter");
2625
2626 let decls_price = find_declarations_by_name(source, "price");
2627 assert_eq!(decls_price.len(), 1);
2628 assert_eq!(decls_price[0].kind, "parameter");
2629
2630 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2632 assert_eq!(decls_tax_upper.len(), 1);
2633 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2634 }
2635
2636 #[test]
2637 fn test_delete_child_node_2462_constructor_public() {
2638 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";
2641 let diag_range = Range {
2642 start: Position {
2643 line: 8,
2644 character: 4,
2645 },
2646 end: Position {
2647 line: 10,
2648 character: 5,
2649 },
2650 };
2651 let source_bytes = source.as_bytes();
2652 let tree = ts_parse(source).expect("parse failed");
2653 let byte = pos_to_bytes(source_bytes, diag_range.start);
2654 eprintln!("2462 byte offset: {byte}");
2655 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2656 loop {
2657 eprintln!(
2658 " ancestor: kind={} start={} end={}",
2659 n.kind(),
2660 n.start_byte(),
2661 n.end_byte()
2662 );
2663 if n.kind() == "constructor_definition" {
2664 let mut cursor = n.walk();
2665 for child in n.children(&mut cursor) {
2666 eprintln!(
2667 " child: kind={:?} text={:?}",
2668 child.kind(),
2669 &source[child.start_byte()..child.end_byte()]
2670 );
2671 }
2672 break;
2673 }
2674 match n.parent() {
2675 Some(p) => n = p,
2676 None => break,
2677 }
2678 }
2679 }
2680 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2681 let edit = code_action_edit(
2682 source,
2683 diag_range,
2684 CodeActionKind::DeleteChildNode {
2685 walk_to: "constructor_definition",
2686 child_kinds: &ck,
2687 },
2688 );
2689 assert!(edit.is_some(), "2462 fix returned None");
2690 let edit = edit.unwrap();
2691 assert_eq!(edit.new_text, "");
2692 let lines: Vec<&str> = source.lines().collect();
2693 let deleted = &lines[edit.range.start.line as usize]
2694 [edit.range.start.character as usize..edit.range.end.character as usize];
2695 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2696 }
2697
2698 #[test]
2699 fn test_delete_child_node_9239_constructor_private() {
2700 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";
2703 let diag_range = Range {
2704 start: Position {
2705 line: 9,
2706 character: 4,
2707 },
2708 end: Position {
2709 line: 11,
2710 character: 5,
2711 },
2712 };
2713 let ck: Vec<&str> = vec!["modifier_invocation"];
2714 let edit = code_action_edit(
2715 source,
2716 diag_range,
2717 CodeActionKind::DeleteChildNode {
2718 walk_to: "constructor_definition",
2719 child_kinds: &ck,
2720 },
2721 );
2722 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2723 let edit = edit.unwrap();
2724 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2726 let lines: Vec<&str> = source.lines().collect();
2728 let line_text = lines[edit.range.start.line as usize];
2729 let deleted =
2730 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2731 assert!(
2732 deleted.contains("private"),
2733 "expected deleted text to contain 'private', got: {:?}",
2734 deleted
2735 );
2736 }
2737
2738 #[test]
2739 fn test_find_best_declaration_same_contract() {
2740 let source = r#"
2741contract A { uint256 public x; }
2742contract B { uint256 public x; }
2743"#;
2744 let ctx = CursorContext {
2745 name: "x".into(),
2746 function: None,
2747 contract: Some("B".into()),
2748 object: None,
2749 arg_count: None,
2750 arg_types: vec![],
2751 };
2752 let uri = Url::parse("file:///test.sol").unwrap();
2753 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2754 assert_eq!(loc.range.start.line, 2);
2756 }
2757
2758 #[test]
2760 fn node_info_deserialize_old_format_with_explicit_nulls() {
2761 let json = r#"{
2762 "src": "100:20:5",
2763 "name_location": null,
2764 "name_locations": [],
2765 "referenced_declaration": null,
2766 "node_type": "Block",
2767 "member_location": null,
2768 "absolute_path": null,
2769 "scope": null,
2770 "base_functions": []
2771 }"#;
2772 let info: NodeInfo = serde_json::from_str(json).unwrap();
2773 assert_eq!(info.src.as_str(), "100:20:5");
2774 assert!(info.name_location.is_none());
2775 assert!(info.name_locations.is_empty());
2776 assert!(info.referenced_declaration.is_none());
2777 assert_eq!(info.node_type.as_deref(), Some("Block"));
2778 assert!(info.member_location.is_none());
2779 assert!(info.absolute_path.is_none());
2780 assert!(info.scope.is_none());
2781 assert!(info.base_functions.is_empty());
2782 }
2783
2784 #[test]
2786 fn node_info_deserialize_new_format_with_absent_fields() {
2787 let json = r#"{
2788 "src": "100:20:5",
2789 "node_type": "Block"
2790 }"#;
2791 let info: NodeInfo = serde_json::from_str(json).unwrap();
2792 assert_eq!(info.src.as_str(), "100:20:5");
2793 assert!(info.name_location.is_none());
2794 assert!(info.name_locations.is_empty());
2795 assert!(info.referenced_declaration.is_none());
2796 assert_eq!(info.node_type.as_deref(), Some("Block"));
2797 assert!(info.member_location.is_none());
2798 assert!(info.absolute_path.is_none());
2799 assert!(info.scope.is_none());
2800 assert!(info.base_functions.is_empty());
2801 }
2802
2803 #[test]
2805 fn node_info_round_trip_skips_null_empty() {
2806 let info = NodeInfo {
2807 src: crate::types::SrcLocation::new("50:10:3".to_string()),
2808 name_location: None,
2809 name_locations: vec![],
2810 referenced_declaration: None,
2811 node_type: Some("Identifier".to_string()),
2812 member_location: None,
2813 member_name: None,
2814 absolute_path: None,
2815 scope: None,
2816 base_functions: vec![],
2817 };
2818 let serialized = serde_json::to_string(&info).unwrap();
2819 assert!(!serialized.contains("name_location"));
2821 assert!(!serialized.contains("name_locations"));
2822 assert!(!serialized.contains("referenced_declaration"));
2823 assert!(!serialized.contains("member_location"));
2824 assert!(!serialized.contains("member_name"));
2825 assert!(!serialized.contains("absolute_path"));
2826 assert!(!serialized.contains("scope"));
2827 assert!(!serialized.contains("base_functions"));
2828 assert!(serialized.contains("\"src\""));
2830 assert!(serialized.contains("\"node_type\""));
2831
2832 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2834 assert_eq!(deserialized.src.as_str(), "50:10:3");
2835 assert!(deserialized.name_location.is_none());
2836 assert!(deserialized.base_functions.is_empty());
2837 }
2838
2839 #[test]
2841 fn node_info_round_trip_preserves_populated_fields() {
2842 use crate::types::NodeId;
2843 let info = NodeInfo {
2844 src: crate::types::SrcLocation::new("200:30:7".to_string()),
2845 name_location: Some("205:4:7".to_string()),
2846 name_locations: vec!["205:4:7".to_string(), "210:4:7".to_string()],
2847 referenced_declaration: Some(NodeId(42)),
2848 node_type: Some("FunctionDefinition".to_string()),
2849 member_location: Some("220:6:7".to_string()),
2850 member_name: Some("transfer".to_string()),
2851 absolute_path: Some("/src/Foo.sol".to_string()),
2852 scope: Some(NodeId(10)),
2853 base_functions: vec![NodeId(100), NodeId(200)],
2854 };
2855 let serialized = serde_json::to_string(&info).unwrap();
2856 assert!(serialized.contains("name_location"));
2858 assert!(serialized.contains("name_locations"));
2859 assert!(serialized.contains("referenced_declaration"));
2860 assert!(serialized.contains("member_location"));
2861 assert!(serialized.contains("member_name"));
2862 assert!(serialized.contains("absolute_path"));
2863 assert!(serialized.contains("scope"));
2864 assert!(serialized.contains("base_functions"));
2865
2866 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2867 assert_eq!(deserialized.name_location.as_deref(), Some("205:4:7"));
2868 assert_eq!(deserialized.name_locations.len(), 2);
2869 assert_eq!(deserialized.referenced_declaration, Some(NodeId(42)));
2870 assert_eq!(deserialized.scope, Some(NodeId(10)));
2871 assert_eq!(deserialized.base_functions.len(), 2);
2872 }
2873}
2874