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::{NodeId, SourceLoc};
8use crate::utils::push_if_node_or_array;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct NodeInfo {
12 pub src: String,
13 pub name_location: Option<String>,
14 pub name_locations: Vec<String>,
15 pub referenced_declaration: Option<NodeId>,
16 pub node_type: Option<String>,
17 pub member_location: Option<String>,
18 pub absolute_path: Option<String>,
19}
20
21pub const CHILD_KEYS: &[&str] = &[
23 "AST",
24 "arguments",
25 "baseContracts",
26 "baseExpression",
27 "baseName",
28 "baseType",
29 "block",
30 "body",
31 "components",
32 "condition",
33 "declarations",
34 "endExpression",
35 "errorCall",
36 "eventCall",
37 "expression",
38 "externalCall",
39 "falseBody",
40 "falseExpression",
41 "file",
42 "foreign",
43 "functionName",
44 "indexExpression",
45 "initialValue",
46 "initializationExpression",
47 "keyType",
48 "leftExpression",
49 "leftHandSide",
50 "libraryName",
51 "literals",
52 "loopExpression",
53 "members",
54 "modifierName",
55 "modifiers",
56 "name",
57 "names",
58 "nodes",
59 "options",
60 "overrides",
61 "parameters",
62 "pathNode",
63 "post",
64 "pre",
65 "returnParameters",
66 "rightExpression",
67 "rightHandSide",
68 "startExpression",
69 "statements",
70 "storageLayout",
71 "subExpression",
72 "subdenomination",
73 "symbolAliases",
74 "trueBody",
75 "trueExpression",
76 "typeName",
77 "unitAlias",
78 "value",
79 "valueType",
80 "variableNames",
81 "variables",
82];
83
84pub type ExternalRefs = HashMap<String, NodeId>;
87
88#[derive(Debug, Clone)]
94pub struct CachedBuild {
95 pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
96 pub path_to_abs: HashMap<String, String>,
97 pub external_refs: ExternalRefs,
98 pub id_to_path_map: HashMap<String, String>,
99 pub decl_index: HashMap<i64, crate::solc_ast::DeclNode>,
103 pub node_id_to_source_path: HashMap<i64, String>,
107 pub gas_index: crate::gas::GasIndex,
110 pub hint_index: crate::inlay_hints::HintIndex,
113 pub doc_index: crate::hover::DocIndex,
116 pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
119 pub build_version: i32,
122 pub content_hash: u64,
126}
127
128impl CachedBuild {
129 pub fn new(ast: Value, build_version: i32) -> Self {
136 let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
137 cache_ids(sources)
138 } else {
139 (HashMap::new(), HashMap::new(), HashMap::new())
140 };
141
142 let id_to_path_map = ast
143 .get("source_id_to_path")
144 .and_then(|v| v.as_object())
145 .map(|obj| {
146 obj.iter()
147 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
148 .collect()
149 })
150 .unwrap_or_default();
151
152 let gas_index = crate::gas::build_gas_index(&ast);
153
154 let doc_index = crate::hover::build_doc_index(&ast);
155
156 let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
163 match crate::solc_ast::extract_decl_nodes(sources) {
164 Some(extracted) => (extracted.decl_index, extracted.node_id_to_source_path),
165 None => (HashMap::new(), HashMap::new()),
166 }
167 } else {
168 (HashMap::new(), HashMap::new())
169 };
170
171 let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
173 let hint_index = if let Some(sources) = ast.get("sources") {
174 crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
175 } else {
176 HashMap::new()
177 };
178
179 let completion_cache = {
181 let sources = ast.get("sources");
182 let contracts = ast.get("contracts");
183 let cc = if let Some(s) = sources {
184 crate::completion::build_completion_cache(s, contracts)
185 } else {
186 crate::completion::build_completion_cache(
187 &serde_json::Value::Object(Default::default()),
188 contracts,
189 )
190 };
191 std::sync::Arc::new(cc)
192 };
193
194 Self {
198 nodes,
199 path_to_abs,
200 external_refs,
201 id_to_path_map,
202 decl_index,
203 node_id_to_source_path,
204 gas_index,
205 hint_index,
206 doc_index,
207 completion_cache,
208 build_version,
209 content_hash: 0,
210 }
211 }
212
213 pub fn merge_missing_from(&mut self, other: &CachedBuild) {
220 for (abs_path, file_nodes) in &other.nodes {
221 if !self.nodes.contains_key(abs_path) {
222 self.nodes.insert(abs_path.clone(), file_nodes.clone());
223 }
224 }
225 for (k, v) in &other.path_to_abs {
226 self.path_to_abs
227 .entry(k.clone())
228 .or_insert_with(|| v.clone());
229 }
230 for (k, v) in &other.external_refs {
231 self.external_refs.entry(k.clone()).or_insert(*v);
232 }
233 for (k, v) in &other.id_to_path_map {
234 self.id_to_path_map
235 .entry(k.clone())
236 .or_insert_with(|| v.clone());
237 }
238 }
239
240 pub fn from_reference_index(
245 nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
246 path_to_abs: HashMap<String, String>,
247 external_refs: ExternalRefs,
248 id_to_path_map: HashMap<String, String>,
249 build_version: i32,
250 ) -> Self {
251 let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
252 &serde_json::Value::Object(Default::default()),
253 None,
254 ));
255
256 Self {
257 nodes,
258 path_to_abs,
259 external_refs,
260 id_to_path_map,
261 decl_index: HashMap::new(),
262 node_id_to_source_path: HashMap::new(),
263 gas_index: HashMap::new(),
264 hint_index: HashMap::new(),
265 doc_index: HashMap::new(),
266 completion_cache,
267 build_version,
268 content_hash: 0,
269 }
270 }
271}
272
273type CachedIds = (
275 HashMap<String, HashMap<NodeId, NodeInfo>>,
276 HashMap<String, String>,
277 ExternalRefs,
278);
279
280pub fn cache_ids(sources: &Value) -> CachedIds {
281 let source_count = sources.as_object().map_or(0, |obj| obj.len());
282
283 let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> =
286 HashMap::with_capacity(source_count);
287 let mut path_to_abs: HashMap<String, String> = HashMap::with_capacity(source_count);
288 let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
289
290 if let Some(sources_obj) = sources.as_object() {
291 for (path, source_data) in sources_obj {
292 if let Some(ast) = source_data.get("ast") {
293 let abs_path = ast
295 .get("absolutePath")
296 .and_then(|v| v.as_str())
297 .unwrap_or(path)
298 .to_string();
299
300 path_to_abs.insert(path.clone(), abs_path.clone());
301
302 let size_hint = ast
307 .get("nodes")
308 .and_then(|v| v.as_array())
309 .map_or(64, |arr| arr.len() * 8);
310 if !nodes.contains_key(&abs_path) {
311 nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
312 }
313
314 if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
315 && let Some(src) = ast.get("src").and_then(|v| v.as_str())
316 {
317 nodes.get_mut(&abs_path).unwrap().insert(
318 NodeId(id),
319 NodeInfo {
320 src: src.to_string(),
321 name_location: None,
322 name_locations: vec![],
323 referenced_declaration: None,
324 node_type: ast
325 .get("nodeType")
326 .and_then(|v| v.as_str())
327 .map(|s| s.to_string()),
328 member_location: None,
329 absolute_path: ast
330 .get("absolutePath")
331 .and_then(|v| v.as_str())
332 .map(|s| s.to_string()),
333 },
334 );
335 }
336
337 let mut stack = vec![ast];
338
339 while let Some(tree) = stack.pop() {
340 if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
341 && let Some(src) = tree.get("src").and_then(|v| v.as_str())
342 {
343 let id = NodeId(raw_id);
344 let mut name_location = tree
346 .get("nameLocation")
347 .and_then(|v| v.as_str())
348 .map(|s| s.to_string());
349
350 if name_location.is_none()
354 && let Some(name_locations) = tree.get("nameLocations")
355 && let Some(locations_array) = name_locations.as_array()
356 && !locations_array.is_empty()
357 {
358 let node_type = tree.get("nodeType").and_then(|v| v.as_str());
359 if node_type == Some("IdentifierPath") {
360 name_location = locations_array
361 .last()
362 .and_then(|v| v.as_str())
363 .map(|s| s.to_string());
364 } else {
365 name_location = locations_array[0].as_str().map(|s| s.to_string());
366 }
367 }
368
369 let name_locations = if let Some(name_locations) = tree.get("nameLocations")
370 && let Some(locations_array) = name_locations.as_array()
371 {
372 locations_array
373 .iter()
374 .filter_map(|v| v.as_str().map(|s| s.to_string()))
375 .collect()
376 } else {
377 vec![]
378 };
379
380 let mut final_name_location = name_location;
381 if final_name_location.is_none()
382 && let Some(member_loc) =
383 tree.get("memberLocation").and_then(|v| v.as_str())
384 {
385 final_name_location = Some(member_loc.to_string());
386 }
387
388 let node_info = NodeInfo {
389 src: src.to_string(),
390 name_location: final_name_location,
391 name_locations,
392 referenced_declaration: tree
393 .get("referencedDeclaration")
394 .and_then(|v| v.as_u64())
395 .map(NodeId),
396 node_type: tree
397 .get("nodeType")
398 .and_then(|v| v.as_str())
399 .map(|s| s.to_string()),
400 member_location: tree
401 .get("memberLocation")
402 .and_then(|v| v.as_str())
403 .map(|s| s.to_string()),
404 absolute_path: tree
405 .get("absolutePath")
406 .and_then(|v| v.as_str())
407 .map(|s| s.to_string()),
408 };
409
410 nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
411
412 if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
414 && let Some(ext_refs) =
415 tree.get("externalReferences").and_then(|v| v.as_array())
416 {
417 for ext_ref in ext_refs {
418 if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
419 && let Some(decl_id) =
420 ext_ref.get("declaration").and_then(|v| v.as_u64())
421 {
422 external_refs.insert(src_str.to_string(), NodeId(decl_id));
423 }
424 }
425 }
426 }
427
428 for key in CHILD_KEYS {
429 push_if_node_or_array(tree, key, &mut stack);
430 }
431 }
432 }
433 }
434 }
435
436 (nodes, path_to_abs, external_refs)
437}
438
439pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
440 let text = String::from_utf8_lossy(source_bytes);
441 crate::utils::position_to_byte_offset(&text, position)
442}
443
444pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
445 let text = String::from_utf8_lossy(source_bytes);
446 let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
447 Some(pos)
448}
449
450pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
452 let loc = SourceLoc::parse(src)?;
453 let file_path = id_to_path.get(&loc.file_id_str())?;
454
455 let absolute_path = if std::path::Path::new(file_path).is_absolute() {
456 std::path::PathBuf::from(file_path)
457 } else {
458 std::env::current_dir().ok()?.join(file_path)
459 };
460
461 let source_bytes = std::fs::read(&absolute_path).ok()?;
462 let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
463 let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
464 let uri = Url::from_file_path(&absolute_path).ok()?;
465
466 Some(Location {
467 uri,
468 range: Range {
469 start: start_pos,
470 end: end_pos,
471 },
472 })
473}
474
475pub fn goto_bytes(
476 nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
477 path_to_abs: &HashMap<String, String>,
478 id_to_path: &HashMap<String, String>,
479 external_refs: &ExternalRefs,
480 uri: &str,
481 position: usize,
482) -> Option<(String, usize, usize)> {
483 let path = match uri.starts_with("file://") {
484 true => &uri[7..],
485 false => uri,
486 };
487
488 let abs_path = path_to_abs.get(path)?;
490
491 let current_file_nodes = nodes.get(abs_path)?;
493
494 let path_to_file_id: HashMap<&str, &str> = id_to_path
496 .iter()
497 .map(|(id, p)| (p.as_str(), id.as_str()))
498 .collect();
499
500 let current_file_id = path_to_file_id.get(abs_path.as_str());
504
505 for (src_str, decl_id) in external_refs {
507 let Some(src_loc) = SourceLoc::parse(src_str) else {
508 continue;
509 };
510
511 if let Some(file_id) = current_file_id {
513 if src_loc.file_id_str() != *file_id {
514 continue;
515 }
516 } else {
517 continue;
518 }
519
520 if src_loc.offset <= position && position < src_loc.end() {
521 let mut target_node: Option<&NodeInfo> = None;
523 for file_nodes in nodes.values() {
524 if let Some(node) = file_nodes.get(decl_id) {
525 target_node = Some(node);
526 break;
527 }
528 }
529 let node = target_node?;
530 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
531 let loc = SourceLoc::parse(loc_str)?;
532 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
533 return Some((file_path, loc.offset, loc.length));
534 }
535 }
536
537 let mut refs = HashMap::new();
538
539 for (id, content) in current_file_nodes {
541 if content.referenced_declaration.is_none() {
542 continue;
543 }
544
545 let Some(src_loc) = SourceLoc::parse(&content.src) else {
546 continue;
547 };
548
549 if src_loc.offset <= position && position < src_loc.end() {
550 let diff = src_loc.length;
551 if !refs.contains_key(&diff) || refs[&diff] <= *id {
552 refs.insert(diff, *id);
553 }
554 }
555 }
556
557 if refs.is_empty() {
558 let tmp = current_file_nodes.iter();
561 for (_id, content) in tmp {
562 if content.node_type == Some("ImportDirective".to_string()) {
563 let Some(src_loc) = SourceLoc::parse(&content.src) else {
564 continue;
565 };
566
567 if src_loc.offset <= position
568 && position < src_loc.end()
569 && let Some(import_path) = &content.absolute_path
570 {
571 return Some((import_path.clone(), 0, 0));
572 }
573 }
574 }
575 return None;
576 }
577
578 let min_diff = *refs.keys().min()?;
580 let chosen_id = refs[&min_diff];
581 let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
582
583 let mut target_node: Option<&NodeInfo> = None;
585 for file_nodes in nodes.values() {
586 if let Some(node) = file_nodes.get(&ref_id) {
587 target_node = Some(node);
588 break;
589 }
590 }
591
592 let node = target_node?;
593
594 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
596 let loc = SourceLoc::parse(loc_str)?;
597 let file_path = id_to_path.get(&loc.file_id_str())?.clone();
598
599 Some((file_path, loc.offset, loc.length))
600}
601
602pub fn goto_declaration_cached(
605 build: &CachedBuild,
606 file_uri: &Url,
607 position: Position,
608 source_bytes: &[u8],
609) -> Option<Location> {
610 let byte_position = pos_to_bytes(source_bytes, position);
611
612 if let Some((file_path, location_bytes, length)) = goto_bytes(
613 &build.nodes,
614 &build.path_to_abs,
615 &build.id_to_path_map,
616 &build.external_refs,
617 file_uri.as_ref(),
618 byte_position,
619 ) {
620 let target_file_path = std::path::Path::new(&file_path);
621 let absolute_path = if target_file_path.is_absolute() {
622 target_file_path.to_path_buf()
623 } else {
624 let base = file_uri
629 .to_file_path()
630 .ok()
631 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
632 .or_else(|| std::env::current_dir().ok())
633 .unwrap_or_default();
634 base.join(target_file_path)
635 };
636
637 if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
638 && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
639 && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
640 && let Ok(target_uri) = Url::from_file_path(&absolute_path)
641 {
642 return Some(Location {
643 uri: target_uri,
644 range: Range {
645 start: start_pos,
646 end: end_pos,
647 },
648 });
649 }
650 };
651
652 None
653}
654
655pub fn goto_declaration_by_name(
666 cached_build: &CachedBuild,
667 file_uri: &Url,
668 name: &str,
669 byte_hint: usize,
670) -> Option<Location> {
671 let path = match file_uri.as_ref().starts_with("file://") {
672 true => &file_uri.as_ref()[7..],
673 false => file_uri.as_ref(),
674 };
675 let abs_path = cached_build.path_to_abs.get(path)?;
676 let built_source = std::fs::read_to_string(abs_path).ok()?;
678
679 let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
681
682 let tmp = {
683 let this = cached_build.nodes.get(abs_path)?;
684 this.iter()
685 };
686 for (_id, node) in tmp {
687 let ref_id = match node.referenced_declaration {
688 Some(id) => id,
689 None => continue,
690 };
691
692 let Some(src_loc) = SourceLoc::parse(&node.src) else {
694 continue;
695 };
696 let start = src_loc.offset;
697 let length = src_loc.length;
698
699 if start + length > built_source.len() {
700 continue;
701 }
702
703 let node_text = &built_source[start..start + length];
704
705 let matches = node_text == name
710 || node_text.contains(&format!(".{name}("))
711 || node_text.ends_with(&format!(".{name}"));
712
713 if matches {
714 let distance = if byte_hint >= start && byte_hint < start + length {
718 0 } else if byte_hint < start {
720 start - byte_hint
721 } else {
722 byte_hint - (start + length)
723 };
724 candidates.push((distance, length, ref_id));
725 }
726 }
727
728 candidates.sort_by_key(|&(dist, span, _)| (dist, span));
730 let ref_id = candidates.first()?.2;
731
732 let mut target_node: Option<&NodeInfo> = None;
734 for file_nodes in cached_build.nodes.values() {
735 if let Some(node) = file_nodes.get(&ref_id) {
736 target_node = Some(node);
737 break;
738 }
739 }
740
741 let node = target_node?;
742
743 let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
745 let loc = SourceLoc::parse(loc_str)?;
746
747 let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
748 let location_bytes = loc.offset;
749 let length = loc.length;
750
751 let target_file_path = std::path::Path::new(file_path);
752 let absolute_path = if target_file_path.is_absolute() {
753 target_file_path.to_path_buf()
754 } else {
755 let base = file_uri
756 .to_file_path()
757 .ok()
758 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
759 .or_else(|| std::env::current_dir().ok())
760 .unwrap_or_default();
761 base.join(target_file_path)
762 };
763
764 let target_source_bytes = std::fs::read(&absolute_path).ok()?;
765 let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
766 let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
767 let target_uri = Url::from_file_path(&absolute_path).ok()?;
768
769 Some(Location {
770 uri: target_uri,
771 range: Range {
772 start: start_pos,
773 end: end_pos,
774 },
775 })
776}
777
778#[derive(Debug, Clone)]
782pub struct CursorContext {
783 pub name: String,
785 pub function: Option<String>,
787 pub contract: Option<String>,
789 pub object: Option<String>,
793 pub arg_count: Option<usize>,
796 pub arg_types: Vec<Option<String>>,
799}
800
801fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
803 let mut parser = Parser::new();
804 parser
805 .set_language(&tree_sitter_solidity::LANGUAGE.into())
806 .expect("failed to load Solidity grammar");
807 parser.parse(source, None)
808}
809
810pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
816 let line = location.range.start.line as usize;
817 let start_col = location.range.start.character as usize;
818 let end_col = location.range.end.character as usize;
819
820 if let Some(line_text) = target_source.lines().nth(line)
821 && end_col <= line_text.len()
822 {
823 return &line_text[start_col..end_col] == expected_name;
824 }
825 true
827}
828
829fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
831 if byte < node.start_byte() || byte >= node.end_byte() {
832 return None;
833 }
834 let mut cursor = node.walk();
835 for child in node.children(&mut cursor) {
836 if child.start_byte() <= byte
837 && byte < child.end_byte()
838 && let Some(deeper) = ts_node_at_byte(child, byte)
839 {
840 return Some(deeper);
841 }
842 }
843 Some(node)
844}
845
846fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
848 let mut cursor = node.walk();
849 node.children(&mut cursor)
850 .find(|c| c.kind() == "identifier" && c.is_named())
851 .map(|c| &source[c.byte_range()])
852}
853
854fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
860 let expr = if arg_node.kind() == "call_argument" {
862 let mut c = arg_node.walk();
863 arg_node.children(&mut c).find(|ch| ch.is_named())?
864 } else {
865 arg_node
866 };
867
868 match expr.kind() {
869 "identifier" => {
870 let var_name = &source[expr.byte_range()];
871 find_variable_type(expr, source, var_name)
873 }
874 "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
875 "boolean_literal" => Some("bool".into()),
876 "string_literal" | "hex_string_literal" => Some("string".into()),
877 _ => None,
878 }
879}
880
881fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
886 let mut scope = from.parent();
887 while let Some(node) = scope {
888 match node.kind() {
889 "function_definition" | "modifier_definition" | "constructor_definition" => {
890 let mut c = node.walk();
892 for child in node.children(&mut c) {
893 if child.kind() == "parameter"
894 && let Some(id) = ts_child_id_text(child, source)
895 && id == var_name
896 {
897 let mut pc = child.walk();
899 return child
900 .children(&mut pc)
901 .find(|c| {
902 matches!(
903 c.kind(),
904 "type_name"
905 | "primitive_type"
906 | "user_defined_type"
907 | "mapping"
908 )
909 })
910 .map(|t| source[t.byte_range()].trim().to_string());
911 }
912 }
913 }
914 "function_body" | "block_statement" | "unchecked_block" => {
915 let mut c = node.walk();
917 for child in node.children(&mut c) {
918 if (child.kind() == "variable_declaration_statement"
919 || child.kind() == "variable_declaration")
920 && let Some(id) = ts_child_id_text(child, source)
921 && id == var_name
922 {
923 let mut pc = child.walk();
924 return child
925 .children(&mut pc)
926 .find(|c| {
927 matches!(
928 c.kind(),
929 "type_name"
930 | "primitive_type"
931 | "user_defined_type"
932 | "mapping"
933 )
934 })
935 .map(|t| source[t.byte_range()].trim().to_string());
936 }
937 }
938 }
939 "contract_declaration" | "library_declaration" | "interface_declaration" => {
940 if let Some(body) = ts_find_child(node, "contract_body") {
942 let mut c = body.walk();
943 for child in body.children(&mut c) {
944 if child.kind() == "state_variable_declaration"
945 && let Some(id) = ts_child_id_text(child, source)
946 && id == var_name
947 {
948 let mut pc = child.walk();
949 return child
950 .children(&mut pc)
951 .find(|c| {
952 matches!(
953 c.kind(),
954 "type_name"
955 | "primitive_type"
956 | "user_defined_type"
957 | "mapping"
958 )
959 })
960 .map(|t| source[t.byte_range()].trim().to_string());
961 }
962 }
963 }
964 }
965 _ => {}
966 }
967 scope = node.parent();
968 }
969 None
970}
971
972fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
974 let mut cursor = call_node.walk();
975 call_node
976 .children(&mut cursor)
977 .filter(|c| c.kind() == "call_argument")
978 .map(|arg| infer_argument_type(arg, source))
979 .collect()
980}
981
982fn best_overload<'a>(
990 decls: &'a [TsDeclaration],
991 arg_count: Option<usize>,
992 arg_types: &[Option<String>],
993) -> Option<&'a TsDeclaration> {
994 if decls.len() == 1 {
995 return decls.first();
996 }
997 if decls.is_empty() {
998 return None;
999 }
1000
1001 let func_decls: Vec<&TsDeclaration> =
1003 decls.iter().filter(|d| d.param_count.is_some()).collect();
1004
1005 if func_decls.is_empty() {
1006 return decls.first();
1007 }
1008
1009 let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1011 let matched: Vec<_> = func_decls
1012 .iter()
1013 .filter(|d| d.param_count == Some(ac))
1014 .collect();
1015 if matched.len() == 1 {
1016 return Some(matched[0]);
1017 }
1018 if matched.is_empty() {
1019 func_decls.iter().collect()
1021 } else {
1022 matched
1023 }
1024 } else {
1025 func_decls.iter().collect()
1026 };
1027
1028 if !arg_types.is_empty() {
1030 let mut best: Option<(&TsDeclaration, usize)> = None;
1031 for &&decl in &count_matched {
1032 let score = arg_types
1033 .iter()
1034 .zip(decl.param_types.iter())
1035 .filter(|(arg_ty, param_ty)| {
1036 if let Some(at) = arg_ty {
1037 at == param_ty.as_str()
1038 } else {
1039 false
1040 }
1041 })
1042 .count();
1043 if best.is_none() || score > best.unwrap().1 {
1044 best = Some((decl, score));
1045 }
1046 }
1047 if let Some((decl, _)) = best {
1048 return Some(decl);
1049 }
1050 }
1051
1052 count_matched.first().map(|d| **d).or(decls.first())
1054}
1055
1056pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1060 let tree = ts_parse(source)?;
1061 let byte = pos_to_bytes(source.as_bytes(), position);
1062 let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1063
1064 let id_node = if leaf.kind() == "identifier" {
1066 leaf
1067 } else {
1068 let parent = leaf.parent()?;
1070 if parent.kind() == "identifier" {
1071 parent
1072 } else {
1073 return None;
1074 }
1075 };
1076
1077 let name = source[id_node.byte_range()].to_string();
1078 let mut function = None;
1079 let mut contract = None;
1080
1081 let object = id_node.parent().and_then(|parent| {
1085 if parent.kind() == "member_expression" {
1086 let prop = parent.child_by_field_name("property")?;
1087 if prop.id() == id_node.id() {
1089 let obj = parent.child_by_field_name("object")?;
1090 Some(source[obj.byte_range()].to_string())
1091 } else {
1092 None
1093 }
1094 } else {
1095 None
1096 }
1097 });
1098
1099 let (arg_count, arg_types) = {
1103 let mut node = id_node.parent();
1104 let mut result = (None, vec![]);
1105 while let Some(n) = node {
1106 if n.kind() == "call_expression" {
1107 let types = infer_call_arg_types(n, source);
1108 result = (Some(types.len()), types);
1109 break;
1110 }
1111 node = n.parent();
1112 }
1113 result
1114 };
1115
1116 let mut current = id_node.parent();
1118 while let Some(node) = current {
1119 match node.kind() {
1120 "function_definition" | "modifier_definition" if function.is_none() => {
1121 function = ts_child_id_text(node, source).map(String::from);
1122 }
1123 "constructor_definition" if function.is_none() => {
1124 function = Some("constructor".into());
1125 }
1126 "contract_declaration" | "interface_declaration" | "library_declaration"
1127 if contract.is_none() =>
1128 {
1129 contract = ts_child_id_text(node, source).map(String::from);
1130 }
1131 _ => {}
1132 }
1133 current = node.parent();
1134 }
1135
1136 Some(CursorContext {
1137 name,
1138 function,
1139 contract,
1140 object,
1141 arg_count,
1142 arg_types,
1143 })
1144}
1145
1146#[derive(Debug, Clone)]
1148pub struct TsDeclaration {
1149 pub range: Range,
1151 pub kind: &'static str,
1153 pub container: Option<String>,
1155 pub param_count: Option<usize>,
1157 pub param_types: Vec<String>,
1160}
1161
1162pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1167 let tree = match ts_parse(source) {
1168 Some(t) => t,
1169 None => return vec![],
1170 };
1171 let mut results = Vec::new();
1172 collect_declarations(tree.root_node(), source, name, None, &mut results);
1173 results
1174}
1175
1176fn collect_declarations(
1177 node: Node,
1178 source: &str,
1179 name: &str,
1180 container: Option<&str>,
1181 out: &mut Vec<TsDeclaration>,
1182) {
1183 let mut cursor = node.walk();
1184 for child in node.children(&mut cursor) {
1185 if !child.is_named() {
1186 continue;
1187 }
1188 match child.kind() {
1189 "contract_declaration" | "interface_declaration" | "library_declaration" => {
1190 if let Some(id_name) = ts_child_id_text(child, source) {
1191 if id_name == name {
1192 out.push(TsDeclaration {
1193 range: id_range(child),
1194 kind: child.kind(),
1195 container: container.map(String::from),
1196 param_count: None,
1197 param_types: vec![],
1198 });
1199 }
1200 if let Some(body) = ts_find_child(child, "contract_body") {
1202 collect_declarations(body, source, name, Some(id_name), out);
1203 }
1204 }
1205 }
1206 "function_definition" | "modifier_definition" => {
1207 if let Some(id_name) = ts_child_id_text(child, source) {
1208 if id_name == name {
1209 let types = parameter_type_signature(child, source);
1210 out.push(TsDeclaration {
1211 range: id_range(child),
1212 kind: child.kind(),
1213 container: container.map(String::from),
1214 param_count: Some(types.len()),
1215 param_types: types.into_iter().map(String::from).collect(),
1216 });
1217 }
1218 collect_parameters(child, source, name, container, out);
1220 if let Some(body) = ts_find_child(child, "function_body") {
1222 collect_declarations(body, source, name, container, out);
1223 }
1224 }
1225 }
1226 "constructor_definition" => {
1227 if name == "constructor" {
1228 let types = parameter_type_signature(child, source);
1229 out.push(TsDeclaration {
1230 range: ts_range(child),
1231 kind: "constructor_definition",
1232 container: container.map(String::from),
1233 param_count: Some(types.len()),
1234 param_types: types.into_iter().map(String::from).collect(),
1235 });
1236 }
1237 collect_parameters(child, source, name, container, out);
1239 if let Some(body) = ts_find_child(child, "function_body") {
1240 collect_declarations(body, source, name, container, out);
1241 }
1242 }
1243 "state_variable_declaration" | "variable_declaration" => {
1244 if let Some(id_name) = ts_child_id_text(child, source)
1245 && id_name == name
1246 {
1247 out.push(TsDeclaration {
1248 range: id_range(child),
1249 kind: child.kind(),
1250 container: container.map(String::from),
1251 param_count: None,
1252 param_types: vec![],
1253 });
1254 }
1255 }
1256 "struct_declaration" => {
1257 if let Some(id_name) = ts_child_id_text(child, source) {
1258 if id_name == name {
1259 out.push(TsDeclaration {
1260 range: id_range(child),
1261 kind: "struct_declaration",
1262 container: container.map(String::from),
1263 param_count: None,
1264 param_types: vec![],
1265 });
1266 }
1267 if let Some(body) = ts_find_child(child, "struct_body") {
1268 collect_declarations(body, source, name, Some(id_name), out);
1269 }
1270 }
1271 }
1272 "enum_declaration" => {
1273 if let Some(id_name) = ts_child_id_text(child, source) {
1274 if id_name == name {
1275 out.push(TsDeclaration {
1276 range: id_range(child),
1277 kind: "enum_declaration",
1278 container: container.map(String::from),
1279 param_count: None,
1280 param_types: vec![],
1281 });
1282 }
1283 if let Some(body) = ts_find_child(child, "enum_body") {
1285 let mut ecur = body.walk();
1286 for val in body.children(&mut ecur) {
1287 if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1288 out.push(TsDeclaration {
1289 range: ts_range(val),
1290 kind: "enum_value",
1291 container: Some(id_name.to_string()),
1292 param_count: None,
1293 param_types: vec![],
1294 });
1295 }
1296 }
1297 }
1298 }
1299 }
1300 "event_definition" | "error_declaration" => {
1301 if let Some(id_name) = ts_child_id_text(child, source)
1302 && id_name == name
1303 {
1304 out.push(TsDeclaration {
1305 range: id_range(child),
1306 kind: child.kind(),
1307 container: container.map(String::from),
1308 param_count: None,
1309 param_types: vec![],
1310 });
1311 }
1312 }
1313 "user_defined_type_definition" => {
1314 if let Some(id_name) = ts_child_id_text(child, source)
1315 && id_name == name
1316 {
1317 out.push(TsDeclaration {
1318 range: id_range(child),
1319 kind: "user_defined_type_definition",
1320 container: container.map(String::from),
1321 param_count: None,
1322 param_types: vec![],
1323 });
1324 }
1325 }
1326 _ => {
1328 collect_declarations(child, source, name, container, out);
1329 }
1330 }
1331 }
1332}
1333
1334fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1340 let mut cursor = node.walk();
1341 node.children(&mut cursor)
1342 .filter(|c| c.kind() == "parameter")
1343 .filter_map(|param| {
1344 let mut pc = param.walk();
1345 param
1346 .children(&mut pc)
1347 .find(|c| {
1348 matches!(
1349 c.kind(),
1350 "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1351 )
1352 })
1353 .map(|t| source[t.byte_range()].trim())
1354 })
1355 .collect()
1356}
1357
1358fn collect_parameters(
1360 node: Node,
1361 source: &str,
1362 name: &str,
1363 container: Option<&str>,
1364 out: &mut Vec<TsDeclaration>,
1365) {
1366 let mut cursor = node.walk();
1367 for child in node.children(&mut cursor) {
1368 if child.kind() == "parameter"
1369 && let Some(id_name) = ts_child_id_text(child, source)
1370 && id_name == name
1371 {
1372 out.push(TsDeclaration {
1373 range: id_range(child),
1374 kind: "parameter",
1375 container: container.map(String::from),
1376 param_count: None,
1377 param_types: vec![],
1378 });
1379 }
1380 }
1381}
1382
1383fn ts_range(node: Node) -> Range {
1385 let s = node.start_position();
1386 let e = node.end_position();
1387 Range {
1388 start: Position::new(s.row as u32, s.column as u32),
1389 end: Position::new(e.row as u32, e.column as u32),
1390 }
1391}
1392
1393fn id_range(node: Node) -> Range {
1395 let mut cursor = node.walk();
1396 node.children(&mut cursor)
1397 .find(|c| c.kind() == "identifier" && c.is_named())
1398 .map(|c| ts_range(c))
1399 .unwrap_or_else(|| ts_range(node))
1400}
1401
1402fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1403 let mut cursor = node.walk();
1404 node.children(&mut cursor).find(|c| c.kind() == kind)
1405}
1406
1407pub fn goto_definition_ts(
1415 source: &str,
1416 position: Position,
1417 file_uri: &Url,
1418 completion_cache: &crate::completion::CompletionCache,
1419 text_cache: &HashMap<String, (i32, String)>,
1420) -> Option<Location> {
1421 let ctx = cursor_context(source, position)?;
1422
1423 if let Some(obj_name) = &ctx.object {
1428 if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1429 let target_source = read_target_source(&path, text_cache)?;
1430 let target_uri = Url::from_file_path(&path).ok()?;
1431 let decls = find_declarations_by_name(&target_source, &ctx.name);
1432 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1433 return Some(Location {
1434 uri: target_uri,
1435 range: d.range,
1436 });
1437 }
1438 }
1439 let decls = find_declarations_by_name(source, &ctx.name);
1441 if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1442 return Some(Location {
1443 uri: file_uri.clone(),
1444 range: d.range,
1445 });
1446 }
1447 }
1448
1449 let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1452
1453 match resolved {
1454 Some(ResolvedTarget::SameFile) => {
1455 find_best_declaration(source, &ctx, file_uri)
1457 }
1458 Some(ResolvedTarget::OtherFile { path, name }) => {
1459 let target_source = read_target_source(&path, text_cache);
1461 let target_source = target_source?;
1462 let target_uri = Url::from_file_path(&path).ok()?;
1463 let decls = find_declarations_by_name(&target_source, &name);
1464 decls.first().map(|d| Location {
1465 uri: target_uri,
1466 range: d.range,
1467 })
1468 }
1469 None => {
1470 find_best_declaration(source, &ctx, file_uri)
1472 }
1473 }
1474}
1475
1476#[derive(Debug)]
1477enum ResolvedTarget {
1478 SameFile,
1480 OtherFile { path: String, name: String },
1482}
1483
1484fn resolve_via_cache(
1490 ctx: &CursorContext,
1491 file_uri: &Url,
1492 cache: &crate::completion::CompletionCache,
1493) -> Option<ResolvedTarget> {
1494 let contract_scope = ctx
1496 .contract
1497 .as_ref()
1498 .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1499 .copied();
1500
1501 if let Some(contract_id) = contract_scope {
1503 if let Some(func_name) = &ctx.function {
1505 if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1508 if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1510 && decls.iter().any(|d| d.name == ctx.name)
1511 {
1512 return Some(ResolvedTarget::SameFile);
1513 }
1514 }
1515 }
1516
1517 if let Some(decls) = cache.scope_declarations.get(&contract_id)
1519 && decls.iter().any(|d| d.name == ctx.name)
1520 {
1521 return Some(ResolvedTarget::SameFile);
1522 }
1523
1524 if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1526 for &base_id in bases.iter().skip(1) {
1527 if let Some(decls) = cache.scope_declarations.get(&base_id)
1528 && decls.iter().any(|d| d.name == ctx.name)
1529 {
1530 let base_name = cache
1533 .name_to_node_id
1534 .iter()
1535 .find(|&(_, &id)| id == base_id)
1536 .map(|(name, _)| name.clone());
1537
1538 if let Some(base_name) = base_name
1539 && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1540 {
1541 return Some(ResolvedTarget::OtherFile {
1542 path,
1543 name: ctx.name.clone(),
1544 });
1545 }
1546 return Some(ResolvedTarget::SameFile);
1548 }
1549 }
1550 }
1551 }
1552
1553 if cache.name_to_node_id.contains_key(&ctx.name) {
1555 if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1557 let current_path = file_uri.to_file_path().ok()?;
1558 let current_str = current_path.to_str()?;
1559 if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1560 return Some(ResolvedTarget::SameFile);
1561 }
1562 return Some(ResolvedTarget::OtherFile {
1563 path,
1564 name: ctx.name.clone(),
1565 });
1566 }
1567 return Some(ResolvedTarget::SameFile);
1568 }
1569
1570 if cache.name_to_type.contains_key(&ctx.name) {
1572 return Some(ResolvedTarget::SameFile);
1573 }
1574
1575 None
1576}
1577
1578fn find_function_scope(
1580 cache: &crate::completion::CompletionCache,
1581 contract_id: NodeId,
1582 func_name: &str,
1583) -> Option<NodeId> {
1584 for (&scope_id, &parent_id) in &cache.scope_parent {
1588 if parent_id == contract_id {
1589 if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1593 && contract_decls.iter().any(|d| d.name == func_name)
1594 {
1595 if cache.scope_declarations.contains_key(&scope_id)
1599 || cache.scope_parent.values().any(|&p| p == scope_id)
1600 {
1601 return Some(scope_id);
1602 }
1603 }
1604 }
1605 }
1606 None
1607}
1608
1609fn find_file_for_contract(
1611 cache: &crate::completion::CompletionCache,
1612 contract_name: &str,
1613 _file_uri: &Url,
1614) -> Option<String> {
1615 let node_id = cache.name_to_node_id.get(contract_name)?;
1620 let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1621 let file_id = scope_range.file_id;
1622
1623 cache
1625 .path_to_file_id
1626 .iter()
1627 .find(|&(_, &fid)| fid == file_id)
1628 .map(|(path, _)| path.clone())
1629}
1630
1631fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1633 let uri = Url::from_file_path(path).ok()?;
1635 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1636 return Some(content.clone());
1637 }
1638 std::fs::read_to_string(path).ok()
1640}
1641
1642fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1644 let decls = find_declarations_by_name(source, &ctx.name);
1645 if decls.is_empty() {
1646 return None;
1647 }
1648
1649 if decls.len() == 1 {
1651 return Some(Location {
1652 uri: file_uri.clone(),
1653 range: decls[0].range,
1654 });
1655 }
1656
1657 if let Some(contract_name) = &ctx.contract
1659 && let Some(d) = decls
1660 .iter()
1661 .find(|d| d.container.as_deref() == Some(contract_name))
1662 {
1663 return Some(Location {
1664 uri: file_uri.clone(),
1665 range: d.range,
1666 });
1667 }
1668
1669 Some(Location {
1671 uri: file_uri.clone(),
1672 range: decls[0].range,
1673 })
1674}
1675
1676#[derive(Debug, Clone, Copy)]
1682pub(crate) enum CodeActionKind<'a> {
1683 InsertAtFileStart { text: &'a str },
1685
1686 ReplaceToken {
1691 replacement: &'a str,
1692 walk_to: Option<&'a str>,
1693 },
1694
1695 DeleteToken,
1698
1699 DeleteLocalVar,
1702
1703 DeleteNodeByKind { node_kind: &'a str },
1707
1708 DeleteChildNode {
1711 walk_to: &'a str,
1712 child_kinds: &'a [&'a str],
1713 },
1714
1715 ReplaceChildNode {
1718 walk_to: &'a str,
1719 child_kind: &'a str,
1720 replacement: &'a str,
1721 },
1722
1723 InsertBeforeNode {
1727 walk_to: &'a str,
1728 before_child: &'a [&'a str],
1729 text: &'a str,
1730 },
1731}
1732
1733pub(crate) fn code_action_edit(
1738 source: &str,
1739 diag_range: Range,
1740 kind: CodeActionKind<'_>,
1741) -> Option<TextEdit> {
1742 let source_bytes = source.as_bytes();
1743
1744 match kind {
1745 CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
1747 range: Range {
1748 start: Position {
1749 line: 0,
1750 character: 0,
1751 },
1752 end: Position {
1753 line: 0,
1754 character: 0,
1755 },
1756 },
1757 new_text: text.to_string(),
1758 }),
1759
1760 CodeActionKind::ReplaceToken {
1762 replacement,
1763 walk_to,
1764 } => {
1765 let tree = ts_parse(source)?;
1766 let byte = pos_to_bytes(source_bytes, diag_range.start);
1767 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1768 if let Some(target_kind) = walk_to {
1772 loop {
1773 if node.kind() == target_kind {
1774 break;
1775 }
1776 node = node.parent()?;
1777 }
1778 }
1779 let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
1780 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1781 Some(TextEdit {
1782 range: Range {
1783 start: start_pos,
1784 end: end_pos,
1785 },
1786 new_text: replacement.to_string(),
1787 })
1788 }
1789
1790 CodeActionKind::DeleteToken => {
1792 let tree = ts_parse(source)?;
1793 let byte = pos_to_bytes(source_bytes, diag_range.start);
1794 let node = ts_node_at_byte(tree.root_node(), byte)?;
1795 let start = node.start_byte();
1796 let end =
1797 if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
1798 node.end_byte() + 1
1799 } else {
1800 node.end_byte()
1801 };
1802 let start_pos = bytes_to_pos(source_bytes, start)?;
1803 let end_pos = bytes_to_pos(source_bytes, end)?;
1804 Some(TextEdit {
1805 range: Range {
1806 start: start_pos,
1807 end: end_pos,
1808 },
1809 new_text: String::new(),
1810 })
1811 }
1812
1813 CodeActionKind::DeleteLocalVar => {
1815 let tree = ts_parse(source)?;
1816 let byte = pos_to_bytes(source_bytes, diag_range.start);
1817 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1818
1819 loop {
1820 if node.kind() == "variable_declaration_statement" {
1821 break;
1822 }
1823 node = node.parent()?;
1824 }
1825
1826 let stmt_start = node.start_byte();
1828 let delete_from = if stmt_start > 0 {
1829 let mut i = stmt_start - 1;
1830 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
1831 i -= 1;
1832 }
1833 if source_bytes[i] == b'\n' {
1834 i
1835 } else {
1836 stmt_start
1837 }
1838 } else {
1839 stmt_start
1840 };
1841
1842 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
1843 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1844 Some(TextEdit {
1845 range: Range {
1846 start: start_pos,
1847 end: end_pos,
1848 },
1849 new_text: String::new(),
1850 })
1851 }
1852
1853 CodeActionKind::DeleteNodeByKind { node_kind } => {
1855 let tree = ts_parse(source)?;
1856 let byte = pos_to_bytes(source_bytes, diag_range.start);
1857 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1858 loop {
1859 if node.kind() == node_kind {
1860 break;
1861 }
1862 node = node.parent()?;
1863 }
1864 let node_start = node.start_byte();
1866 let delete_from = if node_start > 0 {
1867 let mut i = node_start - 1;
1868 while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
1869 i -= 1;
1870 }
1871 if source_bytes[i] == b'\n' {
1872 i
1873 } else {
1874 node_start
1875 }
1876 } else {
1877 node_start
1878 };
1879 let start_pos = bytes_to_pos(source_bytes, delete_from)?;
1880 let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
1881 Some(TextEdit {
1882 range: Range {
1883 start: start_pos,
1884 end: end_pos,
1885 },
1886 new_text: String::new(),
1887 })
1888 }
1889
1890 CodeActionKind::DeleteChildNode {
1895 walk_to,
1896 child_kinds,
1897 } => {
1898 let tree = ts_parse(source)?;
1899 let byte = pos_to_bytes(source_bytes, diag_range.start);
1900 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1901 loop {
1902 if node.kind() == walk_to {
1903 break;
1904 }
1905 node = node.parent()?;
1906 }
1907 let mut cursor = node.walk();
1908 let children: Vec<_> = node.children(&mut cursor).collect();
1909 let child = child_kinds
1910 .iter()
1911 .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
1912 let start = child.start_byte();
1913 let end = if child.end_byte() < source_bytes.len()
1914 && source_bytes[child.end_byte()] == b' '
1915 {
1916 child.end_byte() + 1
1917 } else {
1918 child.end_byte()
1919 };
1920 let start_pos = bytes_to_pos(source_bytes, start)?;
1921 let end_pos = bytes_to_pos(source_bytes, end)?;
1922 Some(TextEdit {
1923 range: Range {
1924 start: start_pos,
1925 end: end_pos,
1926 },
1927 new_text: String::new(),
1928 })
1929 }
1930
1931 CodeActionKind::ReplaceChildNode {
1935 walk_to,
1936 child_kind,
1937 replacement,
1938 } => {
1939 let tree = ts_parse(source)?;
1940 let byte = pos_to_bytes(source_bytes, diag_range.start);
1941 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1942 loop {
1943 if node.kind() == walk_to {
1944 break;
1945 }
1946 node = node.parent()?;
1947 }
1948 let mut cursor = node.walk();
1949 let child = node
1950 .children(&mut cursor)
1951 .find(|c| c.kind() == child_kind)?;
1952 let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
1953 let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
1954 Some(TextEdit {
1955 range: Range {
1956 start: start_pos,
1957 end: end_pos,
1958 },
1959 new_text: replacement.to_string(),
1960 })
1961 }
1962
1963 CodeActionKind::InsertBeforeNode {
1970 walk_to,
1971 before_child,
1972 text,
1973 } => {
1974 let tree = ts_parse(source)?;
1975 let byte = pos_to_bytes(source_bytes, diag_range.start);
1976 let mut node = ts_node_at_byte(tree.root_node(), byte)?;
1977
1978 loop {
1979 if node.kind() == walk_to {
1980 break;
1981 }
1982 node = node.parent()?;
1983 }
1984
1985 let mut cursor = node.walk();
1986 let children: Vec<_> = node.children(&mut cursor).collect();
1987
1988 for target_kind in before_child {
1990 if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
1991 let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
1992 return Some(TextEdit {
1993 range: Range {
1994 start: insert_pos,
1995 end: insert_pos,
1996 },
1997 new_text: text.to_string(),
1998 });
1999 }
2000 }
2001 None
2002 }
2003 }
2004}
2005
2006#[cfg(test)]
2007mod ts_tests {
2008 use super::*;
2009
2010 #[test]
2011 fn test_cursor_context_state_var() {
2012 let source = r#"
2013contract Token {
2014 uint256 public totalSupply;
2015 function mint(uint256 amount) public {
2016 totalSupply += amount;
2017 }
2018}
2019"#;
2020 let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2022 assert_eq!(ctx.name, "totalSupply");
2023 assert_eq!(ctx.function.as_deref(), Some("mint"));
2024 assert_eq!(ctx.contract.as_deref(), Some("Token"));
2025 }
2026
2027 #[test]
2028 fn test_cursor_context_top_level() {
2029 let source = r#"
2030contract Foo {}
2031contract Bar {}
2032"#;
2033 let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2035 assert_eq!(ctx.name, "Foo");
2036 assert!(ctx.function.is_none());
2037 assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2039 }
2040
2041 #[test]
2042 fn test_find_declarations() {
2043 let source = r#"
2044contract Token {
2045 uint256 public totalSupply;
2046 function mint(uint256 amount) public {
2047 totalSupply += amount;
2048 }
2049}
2050"#;
2051 let decls = find_declarations_by_name(source, "totalSupply");
2052 assert_eq!(decls.len(), 1);
2053 assert_eq!(decls[0].kind, "state_variable_declaration");
2054 assert_eq!(decls[0].container.as_deref(), Some("Token"));
2055 }
2056
2057 #[test]
2058 fn test_find_declarations_multiple_contracts() {
2059 let source = r#"
2060contract A {
2061 uint256 public value;
2062}
2063contract B {
2064 uint256 public value;
2065}
2066"#;
2067 let decls = find_declarations_by_name(source, "value");
2068 assert_eq!(decls.len(), 2);
2069 assert_eq!(decls[0].container.as_deref(), Some("A"));
2070 assert_eq!(decls[1].container.as_deref(), Some("B"));
2071 }
2072
2073 #[test]
2074 fn test_find_declarations_enum_value() {
2075 let source = "contract Foo { enum Status { Active, Paused } }";
2076 let decls = find_declarations_by_name(source, "Active");
2077 assert_eq!(decls.len(), 1);
2078 assert_eq!(decls[0].kind, "enum_value");
2079 assert_eq!(decls[0].container.as_deref(), Some("Status"));
2080 }
2081
2082 #[test]
2083 fn test_cursor_context_short_param() {
2084 let source = r#"
2085contract Shop {
2086 uint256 public TAX;
2087 constructor(uint256 price, uint16 tax, uint16 taxBase) {
2088 TAX = tax;
2089 }
2090}
2091"#;
2092 let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2094 assert_eq!(ctx.name, "tax");
2095 assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2096
2097 let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2099 assert_eq!(ctx2.name, "TAX");
2100
2101 let decls = find_declarations_by_name(source, "tax");
2103 assert_eq!(decls.len(), 1);
2104 assert_eq!(decls[0].kind, "parameter");
2105
2106 let decls_tax_base = find_declarations_by_name(source, "taxBase");
2107 assert_eq!(decls_tax_base.len(), 1);
2108 assert_eq!(decls_tax_base[0].kind, "parameter");
2109
2110 let decls_price = find_declarations_by_name(source, "price");
2111 assert_eq!(decls_price.len(), 1);
2112 assert_eq!(decls_price[0].kind, "parameter");
2113
2114 let decls_tax_upper = find_declarations_by_name(source, "TAX");
2116 assert_eq!(decls_tax_upper.len(), 1);
2117 assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2118 }
2119
2120 #[test]
2121 fn test_delete_child_node_2462_constructor_public() {
2122 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";
2125 let diag_range = Range {
2126 start: Position {
2127 line: 8,
2128 character: 4,
2129 },
2130 end: Position {
2131 line: 10,
2132 character: 5,
2133 },
2134 };
2135 let source_bytes = source.as_bytes();
2136 let tree = ts_parse(source).expect("parse failed");
2137 let byte = pos_to_bytes(source_bytes, diag_range.start);
2138 eprintln!("2462 byte offset: {byte}");
2139 if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2140 loop {
2141 eprintln!(
2142 " ancestor: kind={} start={} end={}",
2143 n.kind(),
2144 n.start_byte(),
2145 n.end_byte()
2146 );
2147 if n.kind() == "constructor_definition" {
2148 let mut cursor = n.walk();
2149 for child in n.children(&mut cursor) {
2150 eprintln!(
2151 " child: kind={:?} text={:?}",
2152 child.kind(),
2153 &source[child.start_byte()..child.end_byte()]
2154 );
2155 }
2156 break;
2157 }
2158 match n.parent() {
2159 Some(p) => n = p,
2160 None => break,
2161 }
2162 }
2163 }
2164 let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2165 let edit = code_action_edit(
2166 source,
2167 diag_range,
2168 CodeActionKind::DeleteChildNode {
2169 walk_to: "constructor_definition",
2170 child_kinds: &ck,
2171 },
2172 );
2173 assert!(edit.is_some(), "2462 fix returned None");
2174 let edit = edit.unwrap();
2175 assert_eq!(edit.new_text, "");
2176 let lines: Vec<&str> = source.lines().collect();
2177 let deleted = &lines[edit.range.start.line as usize]
2178 [edit.range.start.character as usize..edit.range.end.character as usize];
2179 assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2180 }
2181
2182 #[test]
2183 fn test_delete_child_node_9239_constructor_private() {
2184 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";
2187 let diag_range = Range {
2188 start: Position {
2189 line: 9,
2190 character: 4,
2191 },
2192 end: Position {
2193 line: 11,
2194 character: 5,
2195 },
2196 };
2197 let ck: Vec<&str> = vec!["modifier_invocation"];
2198 let edit = code_action_edit(
2199 source,
2200 diag_range,
2201 CodeActionKind::DeleteChildNode {
2202 walk_to: "constructor_definition",
2203 child_kinds: &ck,
2204 },
2205 );
2206 assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2207 let edit = edit.unwrap();
2208 assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2210 let lines: Vec<&str> = source.lines().collect();
2212 let line_text = lines[edit.range.start.line as usize];
2213 let deleted =
2214 &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2215 assert!(
2216 deleted.contains("private"),
2217 "expected deleted text to contain 'private', got: {:?}",
2218 deleted
2219 );
2220 }
2221
2222 #[test]
2223 fn test_find_best_declaration_same_contract() {
2224 let source = r#"
2225contract A { uint256 public x; }
2226contract B { uint256 public x; }
2227"#;
2228 let ctx = CursorContext {
2229 name: "x".into(),
2230 function: None,
2231 contract: Some("B".into()),
2232 object: None,
2233 arg_count: None,
2234 arg_types: vec![],
2235 };
2236 let uri = Url::parse("file:///test.sol").unwrap();
2237 let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2238 assert_eq!(loc.range.start.line, 2);
2240 }
2241}
2242