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 "clauses",
59 "components",
60 "condition",
61 "declarations",
62 "endExpression",
63 "errorCall",
64 "eventCall",
65 "expression",
66 "externalCall",
67 "falseBody",
68 "falseExpression",
69 "file",
70 "foreign",
71 "functionName",
72 "indexExpression",
73 "initialValue",
74 "initializationExpression",
75 "keyType",
76 "leftExpression",
77 "leftHandSide",
78 "libraryName",
79 "literals",
80 "loopExpression",
81 "members",
82 "modifierName",
83 "modifiers",
84 "name",
85 "names",
86 "nodes",
87 "options",
88 "overrides",
89 "parameters",
90 "pathNode",
91 "post",
92 "pre",
93 "returnParameters",
94 "rightExpression",
95 "rightHandSide",
96 "startExpression",
97 "statements",
98 "storageLayout",
99 "subExpression",
100 "subdenomination",
101 "symbolAliases",
102 "trueBody",
103 "trueExpression",
104 "typeName",
105 "unitAlias",
106 "value",
107 "valueType",
108 "variableNames",
109 "variables",
110];
111
112pub type ExternalRefs = HashMap<SrcLocation, NodeId>;
115
116#[derive(Debug, Clone)]
118pub struct StorageSlotInfo {
119 pub slot: String,
121 pub offset: u32,
123}
124
125#[derive(Debug, Clone)]
131pub struct CachedBuild {
132 pub nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
133 pub path_to_abs: HashMap<RelPath, AbsPath>,
134 pub external_refs: ExternalRefs,
135 pub id_to_path_map: HashMap<crate::types::SolcFileId, String>,
136 pub decl_index: HashMap<NodeId, crate::solc_ast::DeclNode>,
140 pub node_id_to_source_path: HashMap<NodeId, AbsPath>,
144 pub hint_index: crate::inlay_hints::HintIndex,
147 pub doc_index: crate::hover::DocIndex,
150 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
153 pub build_version: i32,
156 pub qualifier_refs: HashMap<NodeId, Vec<NodeId>>,
164 pub base_function_implementation: HashMap<NodeId, Vec<NodeId>>,
175 pub storage_layout: HashMap<NodeId, StorageSlotInfo>,
185 pub low_level_calls: Vec<LowLevelCall>,
192}
193
194impl CachedBuild {
195 pub fn new(ast: Value, build_version: i32, interner: Option<&mut PathInterner>) -> Self {
207 let (mut nodes, path_to_abs, mut external_refs, mut low_level_calls) =
208 if let Some(sources) = ast.get("sources") {
209 cache_ids(sources)
210 } else {
211 (HashMap::new(), HashMap::new(), HashMap::new(), Vec::new())
212 };
213
214 let solc_id_to_path: HashMap<SolcFileId, String> = ast
216 .get("source_id_to_path")
217 .and_then(|v| v.as_object())
218 .map(|obj| {
219 obj.iter()
220 .map(|(k, v)| {
221 (
222 SolcFileId::new(k.clone()),
223 v.as_str().unwrap_or("").to_string(),
224 )
225 })
226 .collect()
227 })
228 .unwrap_or_default();
229
230 let (id_to_path_map, canonical_remap) = if let Some(interner) = interner {
235 let remap = interner.build_remap(&solc_id_to_path);
236
237 for file_nodes in nodes.values_mut() {
239 for info in file_nodes.values_mut() {
240 canonicalize_node_info(info, &remap);
241 }
242 }
243
244 let old_refs = std::mem::take(&mut external_refs);
246 for (src, decl_id) in old_refs {
247 let new_src = SrcLocation::new(remap_src_canonical(src.as_str(), &remap));
248 external_refs.insert(new_src, decl_id);
249 }
250
251 (interner.to_id_to_path_map(), Some(remap))
253 } else {
254 (solc_id_to_path, None)
256 };
257
258 let doc_index = crate::hover::build_doc_index(&ast);
259
260 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
267 match crate::solc_ast::extract_decl_nodes(sources) {
268 Some(extracted) => (
269 extracted
270 .decl_index
271 .into_iter()
272 .map(|(id, decl)| (NodeId(id), decl))
273 .collect(),
274 extracted
275 .node_id_to_source_path
276 .into_iter()
277 .map(|(id, path)| (NodeId(id), AbsPath::new(path)))
278 .collect(),
279 ),
280 None => (HashMap::new(), HashMap::new()),
281 }
282 } else {
283 (HashMap::new(), HashMap::new())
284 };
285
286 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
288 let hint_index = if let Some(sources) = ast.get("sources") {
289 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
290 } else {
291 HashMap::new()
292 };
293
294 let completion_cache = {
296 let sources = ast.get("sources");
297 let contracts = ast.get("contracts");
298 let cc = if let Some(s) = sources {
299 crate::completion::build_completion_cache(s, contracts, canonical_remap.as_ref())
300 } else {
301 crate::completion::build_completion_cache(
302 &serde_json::Value::Object(Default::default()),
303 contracts,
304 canonical_remap.as_ref(),
305 )
306 };
307 std::sync::Arc::new(cc)
308 };
309
310 let qualifier_refs = build_qualifier_refs(&nodes);
315
316 let base_function_implementation = build_base_function_implementation(&nodes);
320
321 let storage_layout = build_storage_layout(&ast);
323
324 if let Some(ref remap) = canonical_remap {
326 for llc in &mut low_level_calls {
327 llc.src = SrcLocation::new(remap_src_canonical(llc.src.as_str(), remap));
328 llc.name_src = SrcLocation::new(remap_src_canonical(llc.name_src.as_str(), remap));
329 }
330 }
331
332 Self {
336 nodes,
337 path_to_abs,
338 external_refs,
339 id_to_path_map,
340 decl_index,
341 node_id_to_source_path,
342 hint_index,
343 doc_index,
344 completion_cache,
345 build_version,
346 qualifier_refs,
347 base_function_implementation,
348 storage_layout,
349 low_level_calls,
350 }
351 }
352
353 pub fn merge_missing_from(&mut self, other: &CachedBuild) {
360 for (abs_path, file_nodes) in &other.nodes {
361 if !self.nodes.contains_key(abs_path) {
362 self.nodes.insert(abs_path.clone(), file_nodes.clone());
363 }
364 }
365 for (k, v) in &other.path_to_abs {
366 self.path_to_abs
367 .entry(k.clone())
368 .or_insert_with(|| v.clone());
369 }
370 for (k, v) in &other.external_refs {
371 self.external_refs.entry(k.clone()).or_insert(*v);
372 }
373 for (k, v) in &other.id_to_path_map {
374 self.id_to_path_map
375 .entry(k.clone())
376 .or_insert_with(|| v.clone());
377 }
378 for (container_id, other_qrefs) in &other.qualifier_refs {
381 let entry = self.qualifier_refs.entry(*container_id).or_default();
382 for &qnode_id in other_qrefs {
383 if !entry.contains(&qnode_id) {
384 entry.push(qnode_id);
385 }
386 }
387 }
388 for (node_id, other_impls) in &other.base_function_implementation {
391 let entry = self
392 .base_function_implementation
393 .entry(*node_id)
394 .or_default();
395 for &impl_id in other_impls {
396 if !entry.contains(&impl_id) {
397 entry.push(impl_id);
398 }
399 }
400 }
401 for (node_id, info) in &other.storage_layout {
403 self.storage_layout
404 .entry(*node_id)
405 .or_insert_with(|| info.clone());
406 }
407 }
408
409 pub fn from_reference_index(
418 nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
419 path_to_abs: HashMap<RelPath, AbsPath>,
420 external_refs: ExternalRefs,
421 id_to_path_map: HashMap<SolcFileId, String>,
422 build_version: i32,
423 interner: Option<&mut PathInterner>,
424 ) -> Self {
425 if let Some(interner) = interner {
428 for path in id_to_path_map.values() {
429 interner.intern(path);
430 }
431 }
432
433 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
434 &serde_json::Value::Object(Default::default()),
435 None,
436 None,
437 ));
438
439 let qualifier_refs = build_qualifier_refs(&nodes);
441
442 let base_function_implementation = build_base_function_implementation(&nodes);
445
446 Self {
447 nodes,
448 path_to_abs,
449 external_refs,
450 id_to_path_map,
451 decl_index: HashMap::new(),
452 node_id_to_source_path: HashMap::new(),
453 hint_index: HashMap::new(),
454 doc_index: HashMap::new(),
455 completion_cache,
456 build_version,
457 qualifier_refs,
458 base_function_implementation,
459 storage_layout: HashMap::new(),
460 low_level_calls: Vec::new(),
461 }
462 }
463}
464
465fn build_storage_layout(ast: &Value) -> HashMap<NodeId, StorageSlotInfo> {
471 let mut map = HashMap::new();
472 let contracts = match ast.get("contracts").and_then(|v| v.as_object()) {
473 Some(c) => c,
474 None => return map,
475 };
476 for (_path, contracts_in_file) in contracts {
477 let file_contracts = match contracts_in_file.as_object() {
478 Some(c) => c,
479 None => continue,
480 };
481 for (_name, contract_data) in file_contracts {
482 for key in &["storageLayout", "transientStorageLayout"] {
484 let entries = match contract_data
485 .get(*key)
486 .and_then(|v| v.get("storage"))
487 .and_then(|v| v.as_array())
488 {
489 Some(arr) => arr,
490 None => continue,
491 };
492 for entry in entries {
493 let ast_id = match entry.get("astId").and_then(|v| v.as_i64()) {
494 Some(id) => id,
495 None => continue,
496 };
497 let slot = entry
498 .get("slot")
499 .and_then(|v| v.as_str())
500 .unwrap_or("0")
501 .to_string();
502 let offset = entry.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
503 map.insert(NodeId(ast_id), StorageSlotInfo { slot, offset });
504 }
505 }
506 }
507 }
508 map
509}
510
511fn build_qualifier_refs(
521 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
522) -> HashMap<NodeId, Vec<NodeId>> {
523 let mut node_scope: HashMap<NodeId, NodeId> = HashMap::new();
526 for file_nodes in nodes.values() {
527 for (id, info) in file_nodes {
528 if let Some(scope_id) = info.scope {
529 node_scope.insert(*id, scope_id);
530 }
531 }
532 }
533
534 let mut qualifier_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
537 for file_nodes in nodes.values() {
538 for (id, info) in file_nodes {
539 if info.name_locations.len() > 1
540 && info.node_type.as_deref() == Some("IdentifierPath")
541 && let Some(ref_decl) = info.referenced_declaration
542 && let Some(&scope_id) = node_scope.get(&ref_decl)
543 {
544 qualifier_refs.entry(scope_id).or_default().push(*id);
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(
1095 nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
1096 node_id: NodeId,
1097) -> Option<&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 let Some(node_text) = built_source.get(start..start + length) else {
1217 continue;
1218 };
1219
1220 let matches = node_text == name
1225 || node_text.contains(&format!(".{name}("))
1226 || node_text.ends_with(&format!(".{name}"));
1227
1228 if matches {
1229 let distance = if byte_hint >= start && byte_hint < start + length {
1233 0 } else if byte_hint < start {
1235 start - byte_hint
1236 } else {
1237 byte_hint - (start + length)
1238 };
1239 candidates.push((distance, length, ref_id));
1240 }
1241 }
1242
1243 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1245 let ref_id = candidates.first()?.2;
1246
1247 let mut target_node: Option<&NodeInfo> = None;
1249 for file_nodes in cached_build.nodes.values() {
1250 if let Some(node) = file_nodes.get(&ref_id) {
1251 target_node = Some(node);
1252 break;
1253 }
1254 }
1255
1256 let node = target_node?;
1257
1258 let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1260 let loc = SourceLoc::parse(loc_str)?;
1261
1262 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1263 let location_bytes = loc.offset;
1264 let length = loc.length;
1265
1266 let target_file_path = std::path::Path::new(file_path);
1267 let absolute_path = if target_file_path.is_absolute() {
1268 target_file_path.to_path_buf()
1269 } else {
1270 let base = file_uri
1271 .to_file_path()
1272 .ok()
1273 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1274 .or_else(|| std::env::current_dir().ok())
1275 .unwrap_or_default();
1276 base.join(target_file_path)
1277 };
1278
1279 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1280 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1281 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1282 let target_uri = Url::from_file_path(&absolute_path).ok()?;
1283
1284 Some(Location {
1285 uri: target_uri,
1286 range: Range {
1287 start: start_pos,
1288 end: end_pos,
1289 },
1290 })
1291}
1292
1293#[derive(Debug, Clone)]
1297pub struct CursorContext {
1298 pub name: String,
1300 pub function: Option<String>,
1302 pub contract: Option<String>,
1304 pub object: Option<String>,
1308 pub arg_count: Option<usize>,
1311 pub arg_types: Vec<Option<String>>,
1314}
1315
1316fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1318 let mut parser = Parser::new();
1319 parser
1320 .set_language(&tree_sitter_solidity::LANGUAGE.into())
1321 .expect("failed to load Solidity grammar");
1322 parser.parse(source, None)
1323}
1324
1325pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1331 let line = location.range.start.line as usize;
1332 let start_col = location.range.start.character as usize;
1333 let end_col = location.range.end.character as usize;
1334
1335 if let Some(line_text) = target_source.lines().nth(line)
1341 && let Some(slice) = line_text.get(start_col..end_col)
1342 {
1343 return slice == expected_name;
1344 }
1345 true
1347}
1348
1349fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1351 if byte < node.start_byte() || byte >= node.end_byte() {
1352 return None;
1353 }
1354 let mut cursor = node.walk();
1355 for child in node.children(&mut cursor) {
1356 if child.start_byte() <= byte
1357 && byte < child.end_byte()
1358 && let Some(deeper) = ts_node_at_byte(child, byte)
1359 {
1360 return Some(deeper);
1361 }
1362 }
1363 Some(node)
1364}
1365
1366fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1368 let mut cursor = node.walk();
1369 node.children(&mut cursor)
1370 .find(|c| c.kind() == "identifier" && c.is_named())
1371 .map(|c| &source[c.byte_range()])
1372}
1373
1374fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1380 let expr = if arg_node.kind() == "call_argument" {
1382 let mut c = arg_node.walk();
1383 arg_node.children(&mut c).find(|ch| ch.is_named())?
1384 } else {
1385 arg_node
1386 };
1387
1388 match expr.kind() {
1389 "identifier" => {
1390 let var_name = &source[expr.byte_range()];
1391 find_variable_type(expr, source, var_name)
1393 }
1394 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1395 "boolean_literal" => Some("bool".into()),
1396 "string_literal" | "hex_string_literal" => Some("string".into()),
1397 _ => None,
1398 }
1399}
1400
1401fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1406 let mut scope = from.parent();
1407 while let Some(node) = scope {
1408 match node.kind() {
1409 "function_definition" | "modifier_definition" | "constructor_definition" => {
1410 let mut c = node.walk();
1412 for child in node.children(&mut c) {
1413 if child.kind() == "parameter"
1414 && let Some(id) = ts_child_id_text(child, source)
1415 && id == var_name
1416 {
1417 let mut pc = child.walk();
1419 return child
1420 .children(&mut pc)
1421 .find(|c| {
1422 matches!(
1423 c.kind(),
1424 "type_name"
1425 | "primitive_type"
1426 | "user_defined_type"
1427 | "mapping"
1428 )
1429 })
1430 .map(|t| source[t.byte_range()].trim().to_string());
1431 }
1432 }
1433 }
1434 "function_body" | "block_statement" | "unchecked_block" => {
1435 let mut c = node.walk();
1437 for child in node.children(&mut c) {
1438 if (child.kind() == "variable_declaration_statement"
1439 || child.kind() == "variable_declaration")
1440 && let Some(id) = ts_child_id_text(child, source)
1441 && id == var_name
1442 {
1443 let mut pc = child.walk();
1444 return child
1445 .children(&mut pc)
1446 .find(|c| {
1447 matches!(
1448 c.kind(),
1449 "type_name"
1450 | "primitive_type"
1451 | "user_defined_type"
1452 | "mapping"
1453 )
1454 })
1455 .map(|t| source[t.byte_range()].trim().to_string());
1456 }
1457 }
1458 }
1459 "contract_declaration" | "library_declaration" | "interface_declaration" => {
1460 if let Some(body) = ts_find_child(node, "contract_body") {
1462 let mut c = body.walk();
1463 for child in body.children(&mut c) {
1464 if child.kind() == "state_variable_declaration"
1465 && let Some(id) = ts_child_id_text(child, source)
1466 && id == var_name
1467 {
1468 let mut pc = child.walk();
1469 return child
1470 .children(&mut pc)
1471 .find(|c| {
1472 matches!(
1473 c.kind(),
1474 "type_name"
1475 | "primitive_type"
1476 | "user_defined_type"
1477 | "mapping"
1478 )
1479 })
1480 .map(|t| source[t.byte_range()].trim().to_string());
1481 }
1482 }
1483 }
1484 }
1485 _ => {}
1486 }
1487 scope = node.parent();
1488 }
1489 None
1490}
1491
1492fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1494 let mut cursor = call_node.walk();
1495 call_node
1496 .children(&mut cursor)
1497 .filter(|c| c.kind() == "call_argument")
1498 .map(|arg| infer_argument_type(arg, source))
1499 .collect()
1500}
1501
1502fn best_overload<'a>(
1510 decls: &'a [TsDeclaration],
1511 arg_count: Option<usize>,
1512 arg_types: &[Option<String>],
1513) -> Option<&'a TsDeclaration> {
1514 if decls.len() == 1 {
1515 return decls.first();
1516 }
1517 if decls.is_empty() {
1518 return None;
1519 }
1520
1521 let func_decls: Vec<&TsDeclaration> =
1523 decls.iter().filter(|d| d.param_count.is_some()).collect();
1524
1525 if func_decls.is_empty() {
1526 return decls.first();
1527 }
1528
1529 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1531 let matched: Vec<_> = func_decls
1532 .iter()
1533 .filter(|d| d.param_count == Some(ac))
1534 .collect();
1535 if matched.len() == 1 {
1536 return Some(matched[0]);
1537 }
1538 if matched.is_empty() {
1539 func_decls.iter().collect()
1541 } else {
1542 matched
1543 }
1544 } else {
1545 func_decls.iter().collect()
1546 };
1547
1548 if !arg_types.is_empty() {
1550 let mut best: Option<(&TsDeclaration, usize)> = None;
1551 for &&decl in &count_matched {
1552 let score = arg_types
1553 .iter()
1554 .zip(decl.param_types.iter())
1555 .filter(|(arg_ty, param_ty)| {
1556 if let Some(at) = arg_ty {
1557 at == param_ty.as_str()
1558 } else {
1559 false
1560 }
1561 })
1562 .count();
1563 if best.is_none() || score > best.unwrap().1 {
1564 best = Some((decl, score));
1565 }
1566 }
1567 if let Some((decl, _)) = best {
1568 return Some(decl);
1569 }
1570 }
1571
1572 count_matched.first().map(|d| **d).or(decls.first())
1574}
1575
1576pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1580 let tree = ts_parse(source)?;
1581 let byte = pos_to_bytes(source.as_bytes(), position);
1582 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1583
1584 let id_node = if leaf.kind() == "identifier" {
1586 leaf
1587 } else {
1588 let parent = leaf.parent()?;
1590 if parent.kind() == "identifier" {
1591 parent
1592 } else {
1593 return None;
1594 }
1595 };
1596
1597 let name = source[id_node.byte_range()].to_string();
1598 let mut function = None;
1599 let mut contract = None;
1600
1601 let object = id_node.parent().and_then(|parent| {
1605 if parent.kind() == "member_expression" {
1606 let prop = parent.child_by_field_name("property")?;
1607 if prop.id() == id_node.id() {
1609 let obj = parent.child_by_field_name("object")?;
1610 Some(source[obj.byte_range()].to_string())
1611 } else {
1612 None
1613 }
1614 } else {
1615 None
1616 }
1617 });
1618
1619 let (arg_count, arg_types) = {
1623 let mut node = id_node.parent();
1624 let mut result = (None, vec![]);
1625 while let Some(n) = node {
1626 if n.kind() == "call_expression" {
1627 let types = infer_call_arg_types(n, source);
1628 result = (Some(types.len()), types);
1629 break;
1630 }
1631 node = n.parent();
1632 }
1633 result
1634 };
1635
1636 let mut current = id_node.parent();
1638 while let Some(node) = current {
1639 match node.kind() {
1640 "function_definition" | "modifier_definition" if function.is_none() => {
1641 function = ts_child_id_text(node, source).map(String::from);
1642 }
1643 "constructor_definition" if function.is_none() => {
1644 function = Some("constructor".into());
1645 }
1646 "contract_declaration" | "interface_declaration" | "library_declaration"
1647 if contract.is_none() =>
1648 {
1649 contract = ts_child_id_text(node, source).map(String::from);
1650 }
1651 _ => {}
1652 }
1653 current = node.parent();
1654 }
1655
1656 Some(CursorContext {
1657 name,
1658 function,
1659 contract,
1660 object,
1661 arg_count,
1662 arg_types,
1663 })
1664}
1665
1666#[derive(Debug, Clone)]
1668pub struct TsDeclaration {
1669 pub range: Range,
1671 pub kind: &'static str,
1673 pub container: Option<String>,
1675 pub param_count: Option<usize>,
1677 pub param_types: Vec<String>,
1680}
1681
1682pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1687 let tree = match ts_parse(source) {
1688 Some(t) => t,
1689 None => return vec![],
1690 };
1691 let mut results = Vec::new();
1692 collect_declarations(tree.root_node(), source, name, None, &mut results);
1693 results
1694}
1695
1696fn collect_declarations(
1697 node: Node,
1698 source: &str,
1699 name: &str,
1700 container: Option<&str>,
1701 out: &mut Vec<TsDeclaration>,
1702) {
1703 let mut cursor = node.walk();
1704 for child in node.children(&mut cursor) {
1705 if !child.is_named() {
1706 continue;
1707 }
1708 match child.kind() {
1709 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1710 if let Some(id_name) = ts_child_id_text(child, source) {
1711 if id_name == name {
1712 out.push(TsDeclaration {
1713 range: id_range(child),
1714 kind: child.kind(),
1715 container: container.map(String::from),
1716 param_count: None,
1717 param_types: vec![],
1718 });
1719 }
1720 if let Some(body) = ts_find_child(child, "contract_body") {
1722 collect_declarations(body, source, name, Some(id_name), out);
1723 }
1724 }
1725 }
1726 "function_definition" | "modifier_definition" => {
1727 if let Some(id_name) = ts_child_id_text(child, source) {
1728 if id_name == name {
1729 let types = parameter_type_signature(child, source);
1730 out.push(TsDeclaration {
1731 range: id_range(child),
1732 kind: child.kind(),
1733 container: container.map(String::from),
1734 param_count: Some(types.len()),
1735 param_types: types.into_iter().map(String::from).collect(),
1736 });
1737 }
1738 collect_parameters(child, source, name, container, out);
1740 if let Some(body) = ts_find_child(child, "function_body") {
1742 collect_declarations(body, source, name, container, out);
1743 }
1744 }
1745 }
1746 "constructor_definition" => {
1747 if name == "constructor" {
1748 let types = parameter_type_signature(child, source);
1749 out.push(TsDeclaration {
1750 range: ts_range(child),
1751 kind: "constructor_definition",
1752 container: container.map(String::from),
1753 param_count: Some(types.len()),
1754 param_types: types.into_iter().map(String::from).collect(),
1755 });
1756 }
1757 collect_parameters(child, source, name, container, out);
1759 if let Some(body) = ts_find_child(child, "function_body") {
1760 collect_declarations(body, source, name, container, out);
1761 }
1762 }
1763 "state_variable_declaration" | "variable_declaration" => {
1764 if let Some(id_name) = ts_child_id_text(child, source)
1765 && id_name == name
1766 {
1767 out.push(TsDeclaration {
1768 range: id_range(child),
1769 kind: child.kind(),
1770 container: container.map(String::from),
1771 param_count: None,
1772 param_types: vec![],
1773 });
1774 }
1775 }
1776 "struct_declaration" => {
1777 if let Some(id_name) = ts_child_id_text(child, source) {
1778 if id_name == name {
1779 out.push(TsDeclaration {
1780 range: id_range(child),
1781 kind: "struct_declaration",
1782 container: container.map(String::from),
1783 param_count: None,
1784 param_types: vec![],
1785 });
1786 }
1787 if let Some(body) = ts_find_child(child, "struct_body") {
1788 collect_declarations(body, source, name, Some(id_name), out);
1789 }
1790 }
1791 }
1792 "enum_declaration" => {
1793 if let Some(id_name) = ts_child_id_text(child, source) {
1794 if id_name == name {
1795 out.push(TsDeclaration {
1796 range: id_range(child),
1797 kind: "enum_declaration",
1798 container: container.map(String::from),
1799 param_count: None,
1800 param_types: vec![],
1801 });
1802 }
1803 if let Some(body) = ts_find_child(child, "enum_body") {
1805 let mut ecur = body.walk();
1806 for val in body.children(&mut ecur) {
1807 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1808 out.push(TsDeclaration {
1809 range: ts_range(val),
1810 kind: "enum_value",
1811 container: Some(id_name.to_string()),
1812 param_count: None,
1813 param_types: vec![],
1814 });
1815 }
1816 }
1817 }
1818 }
1819 }
1820 "event_definition" | "error_declaration" => {
1821 if let Some(id_name) = ts_child_id_text(child, source)
1822 && id_name == name
1823 {
1824 out.push(TsDeclaration {
1825 range: id_range(child),
1826 kind: child.kind(),
1827 container: container.map(String::from),
1828 param_count: None,
1829 param_types: vec![],
1830 });
1831 }
1832 }
1833 "user_defined_type_definition" => {
1834 if let Some(id_name) = ts_child_id_text(child, source)
1835 && id_name == name
1836 {
1837 out.push(TsDeclaration {
1838 range: id_range(child),
1839 kind: "user_defined_type_definition",
1840 container: container.map(String::from),
1841 param_count: None,
1842 param_types: vec![],
1843 });
1844 }
1845 }
1846 _ => {
1848 collect_declarations(child, source, name, container, out);
1849 }
1850 }
1851 }
1852}
1853
1854fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1860 let mut cursor = node.walk();
1861 node.children(&mut cursor)
1862 .filter(|c| c.kind() == "parameter")
1863 .filter_map(|param| {
1864 let mut pc = param.walk();
1865 param
1866 .children(&mut pc)
1867 .find(|c| {
1868 matches!(
1869 c.kind(),
1870 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1871 )
1872 })
1873 .map(|t| source[t.byte_range()].trim())
1874 })
1875 .collect()
1876}
1877
1878fn collect_parameters(
1880 node: Node,
1881 source: &str,
1882 name: &str,
1883 container: Option<&str>,
1884 out: &mut Vec<TsDeclaration>,
1885) {
1886 let mut cursor = node.walk();
1887 for child in node.children(&mut cursor) {
1888 if child.kind() == "parameter"
1889 && let Some(id_name) = ts_child_id_text(child, source)
1890 && id_name == name
1891 {
1892 out.push(TsDeclaration {
1893 range: id_range(child),
1894 kind: "parameter",
1895 container: container.map(String::from),
1896 param_count: None,
1897 param_types: vec![],
1898 });
1899 }
1900 }
1901}
1902
1903fn ts_range(node: Node) -> Range {
1905 let s = node.start_position();
1906 let e = node.end_position();
1907 Range {
1908 start: Position::new(s.row as u32, s.column as u32),
1909 end: Position::new(e.row as u32, e.column as u32),
1910 }
1911}
1912
1913fn id_range(node: Node) -> Range {
1915 let mut cursor = node.walk();
1916 node.children(&mut cursor)
1917 .find(|c| c.kind() == "identifier" && c.is_named())
1918 .map(|c| ts_range(c))
1919 .unwrap_or_else(|| ts_range(node))
1920}
1921
1922fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1923 let mut cursor = node.walk();
1924 node.children(&mut cursor).find(|c| c.kind() == kind)
1925}
1926
1927pub fn goto_definition_ts(
1935 source: &str,
1936 position: Position,
1937 file_uri: &Url,
1938 completion_cache: &crate::completion::CompletionCache,
1939 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1940) -> Option<Location> {
1941 let ctx = cursor_context(source, position)?;
1942
1943 if let Some(obj_name) = &ctx.object {
1948 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1949 let target_source = read_target_source(&path, text_cache)?;
1950 let target_uri = Url::from_file_path(&path).ok()?;
1951 let decls = find_declarations_by_name(&target_source, &ctx.name);
1952 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1953 return Some(Location {
1954 uri: target_uri,
1955 range: d.range,
1956 });
1957 }
1958 }
1959 let decls = find_declarations_by_name(source, &ctx.name);
1961 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1962 return Some(Location {
1963 uri: file_uri.clone(),
1964 range: d.range,
1965 });
1966 }
1967 }
1968
1969 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1972
1973 match resolved {
1974 Some(ResolvedTarget::SameFile) => {
1975 find_best_declaration(source, &ctx, file_uri)
1977 }
1978 Some(ResolvedTarget::OtherFile { path, name }) => {
1979 let target_source = read_target_source(&path, text_cache);
1981 let target_source = target_source?;
1982 let target_uri = Url::from_file_path(&path).ok()?;
1983 let decls = find_declarations_by_name(&target_source, &name);
1984 decls.first().map(|d| Location {
1985 uri: target_uri,
1986 range: d.range,
1987 })
1988 }
1989 None => {
1990 find_best_declaration(source, &ctx, file_uri)
1992 }
1993 }
1994}
1995
1996#[derive(Debug)]
1997enum ResolvedTarget {
1998 SameFile,
2000 OtherFile { path: String, name: String },
2002}
2003
2004fn resolve_via_cache(
2010 ctx: &CursorContext,
2011 file_uri: &Url,
2012 cache: &crate::completion::CompletionCache,
2013) -> Option<ResolvedTarget> {
2014 let contract_scope = ctx
2016 .contract
2017 .as_ref()
2018 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
2019 .copied();
2020
2021 if let Some(contract_id) = contract_scope {
2023 if let Some(func_name) = &ctx.function {
2025 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
2028 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
2030 && decls.iter().any(|d| d.name == ctx.name)
2031 {
2032 return Some(ResolvedTarget::SameFile);
2033 }
2034 }
2035 }
2036
2037 if let Some(decls) = cache.scope_declarations.get(&contract_id)
2039 && decls.iter().any(|d| d.name == ctx.name)
2040 {
2041 return Some(ResolvedTarget::SameFile);
2042 }
2043
2044 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
2046 for &base_id in bases.iter().skip(1) {
2047 if let Some(decls) = cache.scope_declarations.get(&base_id)
2048 && decls.iter().any(|d| d.name == ctx.name)
2049 {
2050 let base_name = cache
2053 .name_to_node_id
2054 .iter()
2055 .find(|&(_, &id)| id == base_id)
2056 .map(|(name, _)| name.clone());
2057
2058 if let Some(base_name) = base_name
2059 && let Some(path) =
2060 find_file_for_contract(cache, base_name.as_str(), file_uri)
2061 {
2062 return Some(ResolvedTarget::OtherFile {
2063 path,
2064 name: ctx.name.clone(),
2065 });
2066 }
2067 return Some(ResolvedTarget::SameFile);
2069 }
2070 }
2071 }
2072 }
2073
2074 if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
2076 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
2078 let current_path = file_uri.to_file_path().ok()?;
2079 let current_str = current_path.to_str()?;
2080 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
2081 return Some(ResolvedTarget::SameFile);
2082 }
2083 return Some(ResolvedTarget::OtherFile {
2084 path,
2085 name: ctx.name.clone(),
2086 });
2087 }
2088 return Some(ResolvedTarget::SameFile);
2089 }
2090
2091 if cache.name_to_type.contains_key(ctx.name.as_str()) {
2093 return Some(ResolvedTarget::SameFile);
2094 }
2095
2096 None
2097}
2098
2099fn find_function_scope(
2101 cache: &crate::completion::CompletionCache,
2102 contract_id: NodeId,
2103 func_name: &str,
2104) -> Option<NodeId> {
2105 for (&scope_id, &parent_id) in &cache.scope_parent {
2109 if parent_id == contract_id {
2110 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
2114 && contract_decls.iter().any(|d| d.name == func_name)
2115 {
2116 if cache.scope_declarations.contains_key(&scope_id)
2120 || cache.scope_parent.values().any(|&p| p == scope_id)
2121 {
2122 return Some(scope_id);
2123 }
2124 }
2125 }
2126 }
2127 None
2128}
2129
2130fn find_file_for_contract(
2132 cache: &crate::completion::CompletionCache,
2133 contract_name: &str,
2134 _file_uri: &Url,
2135) -> Option<String> {
2136 let node_id = cache.name_to_node_id.get(contract_name)?;
2141 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
2142 let file_id = scope_range.file_id;
2143
2144 cache
2146 .path_to_file_id
2147 .iter()
2148 .find(|&(_, &fid)| fid == file_id)
2149 .map(|(path, _)| path.to_string())
2150}
2151
2152fn read_target_source(
2154 path: &str,
2155 text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
2156) -> Option<String> {
2157 let uri = Url::from_file_path(path).ok()?;
2159 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
2160 return Some(content.clone());
2161 }
2162 std::fs::read_to_string(path).ok()
2164}
2165
2166fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
2168 let decls = find_declarations_by_name(source, &ctx.name);
2169 if decls.is_empty() {
2170 return None;
2171 }
2172
2173 if decls.len() == 1 {
2175 return Some(Location {
2176 uri: file_uri.clone(),
2177 range: decls[0].range,
2178 });
2179 }
2180
2181 if let Some(contract_name) = &ctx.contract
2183 && let Some(d) = decls
2184 .iter()
2185 .find(|d| d.container.as_deref() == Some(contract_name))
2186 {
2187 return Some(Location {
2188 uri: file_uri.clone(),
2189 range: d.range,
2190 });
2191 }
2192
2193 Some(Location {
2195 uri: file_uri.clone(),
2196 range: decls[0].range,
2197 })
2198}
2199
2200#[derive(Debug, Clone, Copy)]
2206pub(crate) enum CodeActionKind<'a> {
2207 InsertAtFileStart { text: &'a str },
2209
2210 ReplaceToken {
2215 replacement: &'a str,
2216 walk_to: Option<&'a str>,
2217 },
2218
2219 DeleteToken,
2222
2223 DeleteLocalVar,
2226
2227 DeleteNodeByKind { node_kind: &'a str },
2231
2232 DeleteChildNode {
2235 walk_to: &'a str,
2236 child_kinds: &'a [&'a str],
2237 },
2238
2239 ReplaceChildNode {
2242 walk_to: &'a str,
2243 child_kind: &'a str,
2244 replacement: &'a str,
2245 },
2246
2247 InsertBeforeNode {
2251 walk_to: &'a str,
2252 before_child: &'a [&'a str],
2253 text: &'a str,
2254 },
2255}
2256
2257pub(crate) fn code_action_edit(
2262 source: &str,
2263 diag_range: Range,
2264 kind: CodeActionKind<'_>,
2265) -> Option<TextEdit> {
2266 let source_bytes = source.as_bytes();
2267
2268 match kind {
2269 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2271 range: Range {
2272 start: Position {
2273 line: 0,
2274 character: 0,
2275 },
2276 end: Position {
2277 line: 0,
2278 character: 0,
2279 },
2280 },
2281 new_text: text.to_string(),
2282 }),
2283
2284 CodeActionKind::ReplaceToken {
2286 replacement,
2287 walk_to,
2288 } => {
2289 let tree = ts_parse(source)?;
2290 let byte = pos_to_bytes(source_bytes, diag_range.start);
2291 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2292 if let Some(target_kind) = walk_to {
2296 loop {
2297 if node.kind() == target_kind {
2298 break;
2299 }
2300 node = node.parent()?;
2301 }
2302 }
2303 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2304 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2305 Some(TextEdit {
2306 range: Range {
2307 start: start_pos,
2308 end: end_pos,
2309 },
2310 new_text: replacement.to_string(),
2311 })
2312 }
2313
2314 CodeActionKind::DeleteToken => {
2316 let tree = ts_parse(source)?;
2317 let byte = pos_to_bytes(source_bytes, diag_range.start);
2318 let node = ts_node_at_byte(tree.root_node(), byte)?;
2319 let start = node.start_byte();
2320 let end =
2321 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2322 node.end_byte() + 1
2323 } else {
2324 node.end_byte()
2325 };
2326 let start_pos = bytes_to_pos(source_bytes, start)?;
2327 let end_pos = bytes_to_pos(source_bytes, end)?;
2328 Some(TextEdit {
2329 range: Range {
2330 start: start_pos,
2331 end: end_pos,
2332 },
2333 new_text: String::new(),
2334 })
2335 }
2336
2337 CodeActionKind::DeleteLocalVar => {
2339 let tree = ts_parse(source)?;
2340 let byte = pos_to_bytes(source_bytes, diag_range.start);
2341 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2342
2343 loop {
2344 if node.kind() == "variable_declaration_statement" {
2345 break;
2346 }
2347 node = node.parent()?;
2348 }
2349
2350 let stmt_start = node.start_byte();
2352 let delete_from = if stmt_start > 0 {
2353 let mut i = stmt_start - 1;
2354 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2355 i -= 1;
2356 }
2357 if source_bytes[i] == b'\n' {
2358 i
2359 } else {
2360 stmt_start
2361 }
2362 } else {
2363 stmt_start
2364 };
2365
2366 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2367 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2368 Some(TextEdit {
2369 range: Range {
2370 start: start_pos,
2371 end: end_pos,
2372 },
2373 new_text: String::new(),
2374 })
2375 }
2376
2377 CodeActionKind::DeleteNodeByKind { node_kind } => {
2379 let tree = ts_parse(source)?;
2380 let byte = pos_to_bytes(source_bytes, diag_range.start);
2381 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2382 loop {
2383 if node.kind() == node_kind {
2384 break;
2385 }
2386 node = node.parent()?;
2387 }
2388 let node_start = node.start_byte();
2390 let delete_from = if node_start > 0 {
2391 let mut i = node_start - 1;
2392 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2393 i -= 1;
2394 }
2395 if source_bytes[i] == b'\n' {
2396 i
2397 } else {
2398 node_start
2399 }
2400 } else {
2401 node_start
2402 };
2403 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2404 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2405 Some(TextEdit {
2406 range: Range {
2407 start: start_pos,
2408 end: end_pos,
2409 },
2410 new_text: String::new(),
2411 })
2412 }
2413
2414 CodeActionKind::DeleteChildNode {
2419 walk_to,
2420 child_kinds,
2421 } => {
2422 let tree = ts_parse(source)?;
2423 let byte = pos_to_bytes(source_bytes, diag_range.start);
2424 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2425 loop {
2426 if node.kind() == walk_to {
2427 break;
2428 }
2429 node = node.parent()?;
2430 }
2431 let mut cursor = node.walk();
2432 let children: Vec<_> = node.children(&mut cursor).collect();
2433 let child = child_kinds
2434 .iter()
2435 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2436 let start = child.start_byte();
2437 let end = if child.end_byte() < source_bytes.len()
2438 && source_bytes[child.end_byte()] == b' '
2439 {
2440 child.end_byte() + 1
2441 } else {
2442 child.end_byte()
2443 };
2444 let start_pos = bytes_to_pos(source_bytes, start)?;
2445 let end_pos = bytes_to_pos(source_bytes, end)?;
2446 Some(TextEdit {
2447 range: Range {
2448 start: start_pos,
2449 end: end_pos,
2450 },
2451 new_text: String::new(),
2452 })
2453 }
2454
2455 CodeActionKind::ReplaceChildNode {
2459 walk_to,
2460 child_kind,
2461 replacement,
2462 } => {
2463 let tree = ts_parse(source)?;
2464 let byte = pos_to_bytes(source_bytes, diag_range.start);
2465 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2466 loop {
2467 if node.kind() == walk_to {
2468 break;
2469 }
2470 node = node.parent()?;
2471 }
2472 let mut cursor = node.walk();
2473 let child = node
2474 .children(&mut cursor)
2475 .find(|c| c.kind() == child_kind)?;
2476 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2477 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2478 Some(TextEdit {
2479 range: Range {
2480 start: start_pos,
2481 end: end_pos,
2482 },
2483 new_text: replacement.to_string(),
2484 })
2485 }
2486
2487 CodeActionKind::InsertBeforeNode {
2494 walk_to,
2495 before_child,
2496 text,
2497 } => {
2498 let tree = ts_parse(source)?;
2499 let byte = pos_to_bytes(source_bytes, diag_range.start);
2500 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2501
2502 loop {
2503 if node.kind() == walk_to {
2504 break;
2505 }
2506 node = node.parent()?;
2507 }
2508
2509 let mut cursor = node.walk();
2510 let children: Vec<_> = node.children(&mut cursor).collect();
2511
2512 for target_kind in before_child {
2514 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2515 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2516 return Some(TextEdit {
2517 range: Range {
2518 start: insert_pos,
2519 end: insert_pos,
2520 },
2521 new_text: text.to_string(),
2522 });
2523 }
2524 }
2525 None
2526 }
2527 }
2528}
2529
2530#[cfg(test)]
2531mod ts_tests {
2532 use super::*;
2533
2534 #[test]
2535 fn test_cursor_context_state_var() {
2536 let source = r#"
2537contract Token {
2538 uint256 public totalSupply;
2539 function mint(uint256 amount) public {
2540 totalSupply += amount;
2541 }
2542}
2543"#;
2544 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2546 assert_eq!(ctx.name, "totalSupply");
2547 assert_eq!(ctx.function.as_deref(), Some("mint"));
2548 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2549 }
2550
2551 #[test]
2552 fn test_cursor_context_top_level() {
2553 let source = r#"
2554contract Foo {}
2555contract Bar {}
2556"#;
2557 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2559 assert_eq!(ctx.name, "Foo");
2560 assert!(ctx.function.is_none());
2561 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2563 }
2564
2565 #[test]
2566 fn test_find_declarations() {
2567 let source = r#"
2568contract Token {
2569 uint256 public totalSupply;
2570 function mint(uint256 amount) public {
2571 totalSupply += amount;
2572 }
2573}
2574"#;
2575 let decls = find_declarations_by_name(source, "totalSupply");
2576 assert_eq!(decls.len(), 1);
2577 assert_eq!(decls[0].kind, "state_variable_declaration");
2578 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2579 }
2580
2581 #[test]
2582 fn test_find_declarations_multiple_contracts() {
2583 let source = r#"
2584contract A {
2585 uint256 public value;
2586}
2587contract B {
2588 uint256 public value;
2589}
2590"#;
2591 let decls = find_declarations_by_name(source, "value");
2592 assert_eq!(decls.len(), 2);
2593 assert_eq!(decls[0].container.as_deref(), Some("A"));
2594 assert_eq!(decls[1].container.as_deref(), Some("B"));
2595 }
2596
2597 #[test]
2598 fn test_find_declarations_enum_value() {
2599 let source = "contract Foo { enum Status { Active, Paused } }";
2600 let decls = find_declarations_by_name(source, "Active");
2601 assert_eq!(decls.len(), 1);
2602 assert_eq!(decls[0].kind, "enum_value");
2603 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2604 }
2605
2606 #[test]
2607 fn test_cursor_context_short_param() {
2608 let source = r#"
2609contract Shop {
2610 uint256 public TAX;
2611 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2612 TAX = tax;
2613 }
2614}
2615"#;
2616 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2618 assert_eq!(ctx.name, "tax");
2619 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2620
2621 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2623 assert_eq!(ctx2.name, "TAX");
2624
2625 let decls = find_declarations_by_name(source, "tax");
2627 assert_eq!(decls.len(), 1);
2628 assert_eq!(decls[0].kind, "parameter");
2629
2630 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2631 assert_eq!(decls_tax_base.len(), 1);
2632 assert_eq!(decls_tax_base[0].kind, "parameter");
2633
2634 let decls_price = find_declarations_by_name(source, "price");
2635 assert_eq!(decls_price.len(), 1);
2636 assert_eq!(decls_price[0].kind, "parameter");
2637
2638 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2640 assert_eq!(decls_tax_upper.len(), 1);
2641 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2642 }
2643
2644 #[test]
2645 fn test_delete_child_node_2462_constructor_public() {
2646 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";
2649 let diag_range = Range {
2650 start: Position {
2651 line: 8,
2652 character: 4,
2653 },
2654 end: Position {
2655 line: 10,
2656 character: 5,
2657 },
2658 };
2659 let source_bytes = source.as_bytes();
2660 let tree = ts_parse(source).expect("parse failed");
2661 let byte = pos_to_bytes(source_bytes, diag_range.start);
2662 eprintln!("2462 byte offset: {byte}");
2663 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2664 loop {
2665 eprintln!(
2666 " ancestor: kind={} start={} end={}",
2667 n.kind(),
2668 n.start_byte(),
2669 n.end_byte()
2670 );
2671 if n.kind() == "constructor_definition" {
2672 let mut cursor = n.walk();
2673 for child in n.children(&mut cursor) {
2674 eprintln!(
2675 " child: kind={:?} text={:?}",
2676 child.kind(),
2677 &source[child.start_byte()..child.end_byte()]
2678 );
2679 }
2680 break;
2681 }
2682 match n.parent() {
2683 Some(p) => n = p,
2684 None => break,
2685 }
2686 }
2687 }
2688 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2689 let edit = code_action_edit(
2690 source,
2691 diag_range,
2692 CodeActionKind::DeleteChildNode {
2693 walk_to: "constructor_definition",
2694 child_kinds: &ck,
2695 },
2696 );
2697 assert!(edit.is_some(), "2462 fix returned None");
2698 let edit = edit.unwrap();
2699 assert_eq!(edit.new_text, "");
2700 let lines: Vec<&str> = source.lines().collect();
2701 let deleted = &lines[edit.range.start.line as usize]
2702 [edit.range.start.character as usize..edit.range.end.character as usize];
2703 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2704 }
2705
2706 #[test]
2707 fn test_delete_child_node_9239_constructor_private() {
2708 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";
2711 let diag_range = Range {
2712 start: Position {
2713 line: 9,
2714 character: 4,
2715 },
2716 end: Position {
2717 line: 11,
2718 character: 5,
2719 },
2720 };
2721 let ck: Vec<&str> = vec!["modifier_invocation"];
2722 let edit = code_action_edit(
2723 source,
2724 diag_range,
2725 CodeActionKind::DeleteChildNode {
2726 walk_to: "constructor_definition",
2727 child_kinds: &ck,
2728 },
2729 );
2730 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2731 let edit = edit.unwrap();
2732 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2734 let lines: Vec<&str> = source.lines().collect();
2736 let line_text = lines[edit.range.start.line as usize];
2737 let deleted =
2738 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2739 assert!(
2740 deleted.contains("private"),
2741 "expected deleted text to contain 'private', got: {:?}",
2742 deleted
2743 );
2744 }
2745
2746 #[test]
2747 fn test_find_best_declaration_same_contract() {
2748 let source = r#"
2749contract A { uint256 public x; }
2750contract B { uint256 public x; }
2751"#;
2752 let ctx = CursorContext {
2753 name: "x".into(),
2754 function: None,
2755 contract: Some("B".into()),
2756 object: None,
2757 arg_count: None,
2758 arg_types: vec![],
2759 };
2760 let uri = Url::parse("file:///test.sol").unwrap();
2761 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2762 assert_eq!(loc.range.start.line, 2);
2764 }
2765
2766 #[test]
2768 fn node_info_deserialize_old_format_with_explicit_nulls() {
2769 let json = r#"{
2770 "src": "100:20:5",
2771 "name_location": null,
2772 "name_locations": [],
2773 "referenced_declaration": null,
2774 "node_type": "Block",
2775 "member_location": null,
2776 "absolute_path": null,
2777 "scope": null,
2778 "base_functions": []
2779 }"#;
2780 let info: NodeInfo = serde_json::from_str(json).unwrap();
2781 assert_eq!(info.src.as_str(), "100:20:5");
2782 assert!(info.name_location.is_none());
2783 assert!(info.name_locations.is_empty());
2784 assert!(info.referenced_declaration.is_none());
2785 assert_eq!(info.node_type.as_deref(), Some("Block"));
2786 assert!(info.member_location.is_none());
2787 assert!(info.absolute_path.is_none());
2788 assert!(info.scope.is_none());
2789 assert!(info.base_functions.is_empty());
2790 }
2791
2792 #[test]
2794 fn node_info_deserialize_new_format_with_absent_fields() {
2795 let json = r#"{
2796 "src": "100:20:5",
2797 "node_type": "Block"
2798 }"#;
2799 let info: NodeInfo = serde_json::from_str(json).unwrap();
2800 assert_eq!(info.src.as_str(), "100:20:5");
2801 assert!(info.name_location.is_none());
2802 assert!(info.name_locations.is_empty());
2803 assert!(info.referenced_declaration.is_none());
2804 assert_eq!(info.node_type.as_deref(), Some("Block"));
2805 assert!(info.member_location.is_none());
2806 assert!(info.absolute_path.is_none());
2807 assert!(info.scope.is_none());
2808 assert!(info.base_functions.is_empty());
2809 }
2810
2811 #[test]
2813 fn node_info_round_trip_skips_null_empty() {
2814 let info = NodeInfo {
2815 src: crate::types::SrcLocation::new("50:10:3".to_string()),
2816 name_location: None,
2817 name_locations: vec![],
2818 referenced_declaration: None,
2819 node_type: Some("Identifier".to_string()),
2820 member_location: None,
2821 member_name: None,
2822 absolute_path: None,
2823 scope: None,
2824 base_functions: vec![],
2825 };
2826 let serialized = serde_json::to_string(&info).unwrap();
2827 assert!(!serialized.contains("name_location"));
2829 assert!(!serialized.contains("name_locations"));
2830 assert!(!serialized.contains("referenced_declaration"));
2831 assert!(!serialized.contains("member_location"));
2832 assert!(!serialized.contains("member_name"));
2833 assert!(!serialized.contains("absolute_path"));
2834 assert!(!serialized.contains("scope"));
2835 assert!(!serialized.contains("base_functions"));
2836 assert!(serialized.contains("\"src\""));
2838 assert!(serialized.contains("\"node_type\""));
2839
2840 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2842 assert_eq!(deserialized.src.as_str(), "50:10:3");
2843 assert!(deserialized.name_location.is_none());
2844 assert!(deserialized.base_functions.is_empty());
2845 }
2846
2847 #[test]
2849 fn node_info_round_trip_preserves_populated_fields() {
2850 use crate::types::NodeId;
2851 let info = NodeInfo {
2852 src: crate::types::SrcLocation::new("200:30:7".to_string()),
2853 name_location: Some("205:4:7".to_string()),
2854 name_locations: vec!["205:4:7".to_string(), "210:4:7".to_string()],
2855 referenced_declaration: Some(NodeId(42)),
2856 node_type: Some("FunctionDefinition".to_string()),
2857 member_location: Some("220:6:7".to_string()),
2858 member_name: Some("transfer".to_string()),
2859 absolute_path: Some("/src/Foo.sol".to_string()),
2860 scope: Some(NodeId(10)),
2861 base_functions: vec![NodeId(100), NodeId(200)],
2862 };
2863 let serialized = serde_json::to_string(&info).unwrap();
2864 assert!(serialized.contains("name_location"));
2866 assert!(serialized.contains("name_locations"));
2867 assert!(serialized.contains("referenced_declaration"));
2868 assert!(serialized.contains("member_location"));
2869 assert!(serialized.contains("member_name"));
2870 assert!(serialized.contains("absolute_path"));
2871 assert!(serialized.contains("scope"));
2872 assert!(serialized.contains("base_functions"));
2873
2874 let deserialized: NodeInfo = serde_json::from_str(&serialized).unwrap();
2875 assert_eq!(deserialized.name_location.as_deref(), Some("205:4:7"));
2876 assert_eq!(deserialized.name_locations.len(), 2);
2877 assert_eq!(deserialized.referenced_declaration, Some(NodeId(42)));
2878 assert_eq!(deserialized.scope, Some(NodeId(10)));
2879 assert_eq!(deserialized.base_functions.len(), 2);
2880 }
2881
2882 #[test]
2888 fn validate_goto_target_survives_a_column_inside_a_character() {
2889 let source = "contract C {\n // 状態\n uint256 total;\n}";
2890 let location = Location {
2892 uri: Url::parse("file:///tmp/C.sol").unwrap(),
2893 range: Range {
2894 start: Position::new(1, 8),
2895 end: Position::new(1, 9),
2896 },
2897 };
2898
2899 let _ = validate_goto_target(source, &location, "total");
2901 }
2902
2903 #[test]
2906 fn validate_goto_target_survives_an_inverted_range() {
2907 let source = "contract C {\n uint256 total;\n}";
2908 let location = Location {
2909 uri: Url::parse("file:///tmp/C.sol").unwrap(),
2910 range: Range {
2911 start: Position::new(1, 12),
2912 end: Position::new(1, 4),
2913 },
2914 };
2915
2916 let _ = validate_goto_target(source, &location, "total");
2917 }
2918
2919 #[test]
2923 fn goto_declaration_by_name_survives_a_stale_offset_inside_a_character() {
2924 let dir = tempfile::tempdir().expect("tempdir");
2925 let file = dir.path().join("C.sol");
2926 std::fs::write(&file, "contract C {\n // 状態\n}\n").expect("write");
2928 let abs = file.to_string_lossy().to_string();
2929
2930 let ast = serde_json::json!({
2931 "sources": {
2932 &abs: {
2933 "id": 0,
2934 "ast": {
2935 "id": 1,
2936 "nodeType": "SourceUnit",
2937 "src": "0:30:0",
2938 "absolutePath": &abs,
2939 "nodes": [{
2940 "id": 2,
2941 "nodeType": "Identifier",
2942 "name": "total",
2943 "src": "22:5:0",
2945 "referencedDeclaration": 3
2946 }]
2947 }
2948 }
2949 },
2950 "source_id_to_path": { "0": &abs }
2951 });
2952
2953 let build = CachedBuild::new(ast, 1, None);
2954 let uri = Url::from_file_path(&file).expect("uri");
2955
2956 let _ = goto_declaration_by_name(&build, &uri, "total", 22);
2958 }
2959
2960 #[test]
2962 fn validate_goto_target_still_matches_on_ascii() {
2963 let source = "contract C {\n uint256 total;\n}";
2964 let location = Location {
2965 uri: Url::parse("file:///tmp/C.sol").unwrap(),
2966 range: Range {
2967 start: Position::new(1, 12),
2968 end: Position::new(1, 17),
2969 },
2970 };
2971
2972 assert!(validate_goto_target(source, &location, "total"));
2973 assert!(!validate_goto_target(source, &location, "other"));
2974 }
2975}
2976