1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use tower_lsp::lsp_types::{
5 CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, Position, Range,
6 TextEdit,
7};
8
9use crate::goto::CHILD_KEYS;
10use crate::hover::build_function_signature;
11use crate::types::{FileId, NodeId, RelPath, SourceLoc, SymbolName, TypeIdentifier};
12use crate::utils::push_if_node_or_array;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct TopLevelImportable {
17 pub name: String,
19 pub declaring_path: String,
21 pub node_type: String,
23 pub kind: CompletionItemKind,
25}
26
27#[derive(Debug, Clone)]
29pub struct ScopedDeclaration {
30 pub name: String,
32 pub type_id: String,
34}
35
36#[derive(Debug, Clone)]
38pub struct ScopeRange {
39 pub node_id: NodeId,
41 pub start: usize,
43 pub end: usize,
45 pub file_id: FileId,
47}
48
49#[derive(Debug)]
51pub struct CompletionCache {
52 pub names: Vec<CompletionItem>,
54
55 pub name_to_type: HashMap<SymbolName, TypeIdentifier>,
57
58 pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
60
61 pub type_to_node: HashMap<TypeIdentifier, NodeId>,
63
64 pub name_to_node_id: HashMap<SymbolName, NodeId>,
66
67 pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
70
71 pub function_return_types: HashMap<(NodeId, String), String>,
74
75 pub using_for: HashMap<TypeIdentifier, Vec<CompletionItem>>,
78
79 pub using_for_wildcard: Vec<CompletionItem>,
81
82 pub general_completions: Vec<CompletionItem>,
85
86 pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
90
91 pub scope_parent: HashMap<NodeId, NodeId>,
94
95 pub scope_ranges: Vec<ScopeRange>,
98
99 pub path_to_file_id: HashMap<RelPath, FileId>,
102
103 pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
107
108 pub contract_kinds: HashMap<NodeId, String>,
112
113 pub top_level_importables_by_name: HashMap<SymbolName, Vec<TopLevelImportable>>,
119
120 pub top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>>,
125}
126
127fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
129 match node_type {
130 "FunctionDefinition" => CompletionItemKind::FUNCTION,
131 "VariableDeclaration" => CompletionItemKind::VARIABLE,
132 "ContractDefinition" => CompletionItemKind::CLASS,
133 "StructDefinition" => CompletionItemKind::STRUCT,
134 "EnumDefinition" => CompletionItemKind::ENUM,
135 "EnumValue" => CompletionItemKind::ENUM_MEMBER,
136 "EventDefinition" => CompletionItemKind::EVENT,
137 "ErrorDefinition" => CompletionItemKind::EVENT,
138 "ModifierDefinition" => CompletionItemKind::METHOD,
139 "ImportDirective" => CompletionItemKind::MODULE,
140 _ => CompletionItemKind::TEXT,
141 }
142}
143
144fn parse_src(node: &Value) -> Option<SourceLoc> {
147 let src = node.get("src").and_then(|v| v.as_str())?;
148 SourceLoc::parse(src)
149}
150
151pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
156 let mut last_id = None;
159 let mut i = 0;
160 let bytes = type_id.as_bytes();
161 while i < bytes.len() {
162 if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
163 i += 2;
164 let start = i;
165 while i < bytes.len() && bytes[i].is_ascii_digit() {
166 i += 1;
167 }
168 if i > start
169 && let Ok(id) = type_id[start..i].parse::<i64>()
170 {
171 last_id = Some(NodeId(id));
172 }
173 } else {
174 i += 1;
175 }
176 }
177 last_id
178}
179
180pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
187 let mut current = type_id;
188
189 loop {
190 if !current.starts_with("t_mapping$_") {
191 let result = current.trim_end_matches("_$");
194 return if result.is_empty() {
195 None
196 } else {
197 Some(result.to_string())
198 };
199 }
200
201 let inner = ¤t["t_mapping$_".len()..];
203
204 let mut depth = 0i32;
208 let bytes = inner.as_bytes();
209 let mut split_pos = None;
210
211 let mut i = 0;
212 while i < bytes.len() {
213 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
214 depth += 1;
215 i += 2;
216 } else if i + 2 < bytes.len()
217 && bytes[i] == b'_'
218 && bytes[i + 1] == b'$'
219 && bytes[i + 2] == b'_'
220 && depth == 0
221 {
222 split_pos = Some(i);
224 break;
225 } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
226 depth -= 1;
227 i += 2;
228 } else {
229 i += 1;
230 }
231 }
232
233 if let Some(pos) = split_pos {
234 current = &inner[pos + 3..];
236 } else {
237 return None;
238 }
239 }
240}
241
242fn count_abi_params(signature: &str) -> usize {
245 let start = match signature.find('(') {
247 Some(i) => i + 1,
248 None => return 0,
249 };
250 let bytes = signature.as_bytes();
251 if start >= bytes.len() {
252 return 0;
253 }
254 if bytes[start] == b')' {
256 return 0;
257 }
258 let mut count = 1; let mut depth = 0;
260 for &b in &bytes[start..] {
261 match b {
262 b'(' => depth += 1,
263 b')' => {
264 if depth == 0 {
265 break;
266 }
267 depth -= 1;
268 }
269 b',' if depth == 0 => count += 1,
270 _ => {}
271 }
272 }
273 count
274}
275
276fn count_signature_params(sig: &str) -> usize {
278 count_abi_params(sig)
279}
280
281fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
282 match node_type {
283 "ContractDefinition"
284 | "StructDefinition"
285 | "EnumDefinition"
286 | "UserDefinedValueTypeDefinition"
287 | "FunctionDefinition" => true,
288 "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
289 _ => false,
290 }
291}
292
293fn build_top_level_importables_by_name(
294 by_file: &HashMap<RelPath, Vec<TopLevelImportable>>,
295) -> HashMap<SymbolName, Vec<TopLevelImportable>> {
296 let mut by_name: HashMap<SymbolName, Vec<TopLevelImportable>> = HashMap::new();
297 for symbols in by_file.values() {
298 for symbol in symbols {
299 by_name
300 .entry(SymbolName::new(symbol.name.clone()))
301 .or_default()
302 .push(symbol.clone());
303 }
304 }
305 by_name
306}
307
308pub fn extract_top_level_importables_for_file(path: &str, ast: &Value) -> Vec<TopLevelImportable> {
313 let mut out: Vec<TopLevelImportable> = Vec::new();
314 let mut stack: Vec<&Value> = vec![ast];
315 let mut source_unit_id: Option<NodeId> = None;
316
317 while let Some(tree) = stack.pop() {
318 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
319 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
320 if node_type == "SourceUnit" {
321 source_unit_id = node_id;
322 }
323 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
324
325 if !name.is_empty()
326 && is_top_level_importable_decl(node_type, tree)
327 && let Some(src_scope) = source_unit_id
328 && tree.get("scope").and_then(|v| v.as_i64()) == Some(src_scope.0)
329 {
330 out.push(TopLevelImportable {
331 name: name.to_string(),
332 declaring_path: path.to_string(),
333 node_type: node_type.to_string(),
334 kind: node_type_to_completion_kind(node_type),
335 });
336 }
337
338 for key in CHILD_KEYS {
339 push_if_node_or_array(tree, key, &mut stack);
340 }
341 }
342
343 out
344}
345
346impl CompletionCache {
347 pub fn replace_top_level_importables_for_path(
350 &mut self,
351 path: String,
352 symbols: Vec<TopLevelImportable>,
353 ) {
354 self.top_level_importables_by_file
355 .insert(RelPath::new(path), symbols);
356 self.top_level_importables_by_name =
357 build_top_level_importables_by_name(&self.top_level_importables_by_file);
358 }
359}
360
361pub fn build_completion_cache(
369 sources: &Value,
370 contracts: Option<&Value>,
371 file_id_remap: Option<&HashMap<u64, FileId>>,
372) -> CompletionCache {
373 let source_count = sources.as_object().map_or(0, |obj| obj.len());
374 let est_names = source_count * 20;
377 let est_contracts = source_count * 5;
378
379 let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
380 let mut seen_names: HashMap<SymbolName, usize> = HashMap::with_capacity(est_names);
381 let mut name_to_type: HashMap<SymbolName, TypeIdentifier> = HashMap::with_capacity(est_names);
382 let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
383 HashMap::with_capacity(est_contracts);
384 let mut type_to_node: HashMap<TypeIdentifier, NodeId> = HashMap::with_capacity(est_contracts);
385 let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
386 HashMap::with_capacity(est_contracts);
387 let mut name_to_node_id: HashMap<SymbolName, NodeId> = HashMap::with_capacity(est_names);
388 let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
389
390 let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
392
393 let mut function_signatures: HashMap<NodeId, HashMap<SymbolName, Vec<String>>> =
395 HashMap::with_capacity(est_contracts);
396
397 let mut function_return_types: HashMap<(NodeId, String), String> =
399 HashMap::with_capacity(source_count * 10);
400
401 let mut using_for: HashMap<TypeIdentifier, Vec<CompletionItem>> =
403 HashMap::with_capacity(source_count);
404 let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
405
406 let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
408
409 let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
411 HashMap::with_capacity(est_contracts);
412 let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
413 let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
414 let mut path_to_file_id: HashMap<RelPath, FileId> = HashMap::with_capacity(source_count);
415 let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
416 HashMap::with_capacity(est_contracts);
417 let mut top_level_importables_by_file: HashMap<RelPath, Vec<TopLevelImportable>> =
418 HashMap::with_capacity(est_names);
419
420 if let Some(sources_obj) = sources.as_object() {
421 for (path, source_data) in sources_obj {
422 if let Some(ast) = source_data.get("ast") {
423 if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
427 let canonical_fid = file_id_remap
428 .and_then(|r| r.get(&fid).copied())
429 .unwrap_or(FileId(fid));
430 path_to_file_id.insert(RelPath::new(path), canonical_fid);
431 }
432 let file_importables = extract_top_level_importables_for_file(path, ast);
433 if !file_importables.is_empty() {
434 top_level_importables_by_file.insert(RelPath::new(path), file_importables);
435 }
436 let mut stack: Vec<&Value> = vec![ast];
437
438 while let Some(tree) = stack.pop() {
439 let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
440 let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
441 let node_id = tree.get("id").and_then(|v| v.as_i64()).map(NodeId);
442
443 let is_scope_node = matches!(
448 node_type,
449 "SourceUnit"
450 | "ContractDefinition"
451 | "FunctionDefinition"
452 | "ModifierDefinition"
453 | "Block"
454 | "UncheckedBlock"
455 );
456 if is_scope_node && let Some(nid) = node_id {
457 if let Some(src_loc) = parse_src(tree) {
458 let canonical_fid = file_id_remap
459 .and_then(|r| r.get(&src_loc.file_id.0).copied())
460 .unwrap_or(src_loc.file_id);
461 scope_ranges.push(ScopeRange {
462 node_id: nid,
463 start: src_loc.offset,
464 end: src_loc.end(),
465 file_id: canonical_fid,
466 });
467 }
468 if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_i64()) {
470 scope_parent.insert(nid, NodeId(parent_id));
471 }
472 }
473
474 if node_type == "ContractDefinition"
476 && let Some(nid) = node_id
477 && let Some(bases) = tree
478 .get("linearizedBaseContracts")
479 .and_then(|v| v.as_array())
480 {
481 let base_ids: Vec<NodeId> = bases
482 .iter()
483 .filter_map(|b| b.as_i64())
484 .map(NodeId)
485 .collect();
486 if !base_ids.is_empty() {
487 linearized_base_contracts.insert(nid, base_ids);
488 }
489 }
490
491 if node_type == "VariableDeclaration"
493 && !name.is_empty()
494 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
495 && let Some(tid) = tree
496 .get("typeDescriptions")
497 .and_then(|td| td.get("typeIdentifier"))
498 .and_then(|v| v.as_str())
499 {
500 scope_declarations
501 .entry(NodeId(scope_raw))
502 .or_default()
503 .push(ScopedDeclaration {
504 name: name.to_string(),
505 type_id: tid.to_string(),
506 });
507 }
508
509 if node_type == "FunctionDefinition"
511 && !name.is_empty()
512 && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_i64())
513 && let Some(tid) = tree
514 .get("typeDescriptions")
515 .and_then(|td| td.get("typeIdentifier"))
516 .and_then(|v| v.as_str())
517 {
518 scope_declarations
519 .entry(NodeId(scope_raw))
520 .or_default()
521 .push(ScopedDeclaration {
522 name: name.to_string(),
523 type_id: tid.to_string(),
524 });
525 }
526
527 if !name.is_empty() && !seen_names.contains_key(name) {
529 let type_string = tree
530 .get("typeDescriptions")
531 .and_then(|td| td.get("typeString"))
532 .and_then(|v| v.as_str())
533 .map(|s| s.to_string());
534
535 let type_id = tree
536 .get("typeDescriptions")
537 .and_then(|td| td.get("typeIdentifier"))
538 .and_then(|v| v.as_str());
539
540 let kind = node_type_to_completion_kind(node_type);
541
542 let item = CompletionItem {
543 label: name.to_string(),
544 kind: Some(kind),
545 detail: type_string,
546 ..Default::default()
547 };
548
549 let idx = names.len();
550 names.push(item);
551 seen_names.insert(SymbolName::new(name), idx);
552
553 if let Some(tid) = type_id {
555 name_to_type.insert(SymbolName::new(name), TypeIdentifier::new(tid));
556 }
557 }
558
559 if node_type == "StructDefinition"
561 && let Some(id) = node_id
562 {
563 let mut members = Vec::new();
564 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
565 for member in member_array {
566 let member_name =
567 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
568 if member_name.is_empty() {
569 continue;
570 }
571 let member_type = member
572 .get("typeDescriptions")
573 .and_then(|td| td.get("typeString"))
574 .and_then(|v| v.as_str())
575 .map(|s| s.to_string());
576
577 members.push(CompletionItem {
578 label: member_name.to_string(),
579 kind: Some(CompletionItemKind::FIELD),
580 detail: member_type,
581 ..Default::default()
582 });
583 }
584 }
585 if !members.is_empty() {
586 node_members.insert(id, members);
587 }
588
589 if let Some(tid) = tree
591 .get("typeDescriptions")
592 .and_then(|td| td.get("typeIdentifier"))
593 .and_then(|v| v.as_str())
594 {
595 type_to_node.insert(TypeIdentifier::new(tid), id);
596 }
597 }
598
599 if node_type == "ContractDefinition"
601 && let Some(id) = node_id
602 {
603 let mut members = Vec::new();
604 let mut fn_sigs: HashMap<SymbolName, Vec<String>> = HashMap::new();
605 if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
606 for member in nodes_array {
607 let member_type = member
608 .get("nodeType")
609 .and_then(|v| v.as_str())
610 .unwrap_or("");
611 let member_name =
612 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
613 if member_name.is_empty() {
614 continue;
615 }
616
617 let (member_detail, label_details) =
619 if member_type == "FunctionDefinition" {
620 if let Some(ret_params) = member
624 .get("returnParameters")
625 .and_then(|rp| rp.get("parameters"))
626 .and_then(|v| v.as_array())
627 && ret_params.len() == 1
628 && let Some(ret_tid) = ret_params[0]
629 .get("typeDescriptions")
630 .and_then(|td| td.get("typeIdentifier"))
631 .and_then(|v| v.as_str())
632 {
633 function_return_types.insert(
634 (id, member_name.to_string()),
635 ret_tid.to_string(),
636 );
637 }
638
639 if let Some(sig) = build_function_signature(member) {
640 fn_sigs
641 .entry(SymbolName::new(member_name))
642 .or_default()
643 .push(sig.clone());
644 (Some(sig), None)
645 } else {
646 (
647 member
648 .get("typeDescriptions")
649 .and_then(|td| td.get("typeString"))
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string()),
652 None,
653 )
654 }
655 } else {
656 (
657 member
658 .get("typeDescriptions")
659 .and_then(|td| td.get("typeString"))
660 .and_then(|v| v.as_str())
661 .map(|s| s.to_string()),
662 None,
663 )
664 };
665
666 let kind = node_type_to_completion_kind(member_type);
667 members.push(CompletionItem {
668 label: member_name.to_string(),
669 kind: Some(kind),
670 detail: member_detail,
671 label_details,
672 ..Default::default()
673 });
674 }
675 }
676 if !members.is_empty() {
677 node_members.insert(id, members);
678 }
679 if !fn_sigs.is_empty() {
680 function_signatures.insert(id, fn_sigs);
681 }
682
683 if let Some(tid) = tree
684 .get("typeDescriptions")
685 .and_then(|td| td.get("typeIdentifier"))
686 .and_then(|v| v.as_str())
687 {
688 type_to_node.insert(TypeIdentifier::new(tid), id);
689 }
690
691 if !name.is_empty() {
693 contract_locations.push((path.clone(), name.to_string(), id));
694 name_to_node_id.insert(SymbolName::new(name), id);
695 }
696
697 if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
699 contract_kinds.insert(id, ck.to_string());
700 }
701 }
702
703 if node_type == "EnumDefinition"
705 && let Some(id) = node_id
706 {
707 let mut members = Vec::new();
708 if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
709 for member in member_array {
710 let member_name =
711 member.get("name").and_then(|v| v.as_str()).unwrap_or("");
712 if member_name.is_empty() {
713 continue;
714 }
715 members.push(CompletionItem {
716 label: member_name.to_string(),
717 kind: Some(CompletionItemKind::ENUM_MEMBER),
718 detail: None,
719 ..Default::default()
720 });
721 }
722 }
723 if !members.is_empty() {
724 node_members.insert(id, members);
725 }
726
727 if let Some(tid) = tree
728 .get("typeDescriptions")
729 .and_then(|td| td.get("typeIdentifier"))
730 .and_then(|v| v.as_str())
731 {
732 type_to_node.insert(TypeIdentifier::new(tid), id);
733 }
734 }
735
736 if node_type == "UsingForDirective" {
738 let target_type = tree.get("typeName").and_then(|tn| {
740 tn.get("typeDescriptions")
741 .and_then(|td| td.get("typeIdentifier"))
742 .and_then(|v| v.as_str())
743 .map(|s| s.to_string())
744 });
745
746 if let Some(lib) = tree.get("libraryName") {
748 if let Some(lib_id) =
749 lib.get("referencedDeclaration").and_then(|v| v.as_i64())
750 {
751 using_for_directives.push((NodeId(lib_id), target_type));
752 }
753 }
754 else if let Some(func_list) =
758 tree.get("functionList").and_then(|v| v.as_array())
759 {
760 for entry in func_list {
761 if entry.get("operator").is_some() {
763 continue;
764 }
765 if let Some(def) = entry.get("definition") {
766 let fn_name =
767 def.get("name").and_then(|v| v.as_str()).unwrap_or("");
768 if !fn_name.is_empty() {
769 let items = if let Some(ref tid) = target_type {
770 using_for
771 .entry(TypeIdentifier::new(tid.clone()))
772 .or_default()
773 } else {
774 &mut using_for_wildcard
775 };
776 items.push(CompletionItem {
777 label: fn_name.to_string(),
778 kind: Some(CompletionItemKind::FUNCTION),
779 detail: None,
780 ..Default::default()
781 });
782 }
783 }
784 }
785 }
786 }
787
788 for key in CHILD_KEYS {
790 push_if_node_or_array(tree, key, &mut stack);
791 }
792 }
793 }
794 }
795 }
796
797 for (lib_id, target_type) in &using_for_directives {
800 if let Some(lib_members) = node_members.get(lib_id) {
801 let items: Vec<CompletionItem> = lib_members
802 .iter()
803 .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
804 .cloned()
805 .collect();
806 if !items.is_empty() {
807 if let Some(tid) = target_type {
808 using_for
809 .entry(TypeIdentifier::new(tid.clone()))
810 .or_default()
811 .extend(items);
812 } else {
813 using_for_wildcard.extend(items);
814 }
815 }
816 }
817 }
818
819 if let Some(contracts_val) = contracts
821 && let Some(contracts_obj) = contracts_val.as_object()
822 {
823 for (path, contract_name, node_id) in &contract_locations {
824 let fn_sigs = function_signatures.get(node_id);
826
827 if let Some(path_entry) = contracts_obj.get(path)
828 && let Some(contract_entry) = path_entry.get(contract_name)
829 && let Some(evm) = contract_entry.get("evm")
830 && let Some(methods) = evm.get("methodIdentifiers")
831 && let Some(methods_obj) = methods.as_object()
832 {
833 let mut items: Vec<CompletionItem> = Vec::new();
834 for (signature, selector_val) in methods_obj {
835 let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
838 let selector_str = selector_val
839 .as_str()
840 .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
841 .unwrap_or_default();
842
843 let description =
845 fn_sigs
846 .and_then(|sigs| sigs.get(&fn_name))
847 .and_then(|sig_list| {
848 if sig_list.len() == 1 {
849 Some(sig_list[0].clone())
851 } else {
852 let abi_param_count = count_abi_params(signature);
854 sig_list
855 .iter()
856 .find(|s| count_signature_params(s) == abi_param_count)
857 .cloned()
858 }
859 });
860
861 items.push(CompletionItem {
862 label: fn_name,
863 kind: Some(CompletionItemKind::FUNCTION),
864 detail: Some(signature.clone()),
865 label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
866 detail: Some(selector_str),
867 description,
868 }),
869 ..Default::default()
870 });
871 }
872 if !items.is_empty() {
873 method_identifiers.insert(*node_id, items);
874 }
875 }
876 }
877 }
878
879 let mut general_completions = names.clone();
881 general_completions.extend(get_static_completions());
882
883 scope_ranges.sort_by_key(|r| r.end - r.start);
885
886 let orphan_ids: Vec<NodeId> = scope_ranges
892 .iter()
893 .filter(|r| !scope_parent.contains_key(&r.node_id))
894 .map(|r| r.node_id)
895 .collect();
896 let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
898 .iter()
899 .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
900 .collect();
901 for orphan_id in &orphan_ids {
902 if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
903 let parent = scope_ranges
906 .iter()
907 .find(|r| {
908 r.node_id != *orphan_id
909 && r.file_id == file_id
910 && r.start <= start
911 && r.end >= end
912 && (r.end - r.start) > (end - start)
913 })
914 .map(|r| r.node_id);
915 if let Some(parent_id) = parent {
916 scope_parent.insert(*orphan_id, parent_id);
917 }
918 }
919 }
920
921 let top_level_importables_by_name =
922 build_top_level_importables_by_name(&top_level_importables_by_file);
923
924 CompletionCache {
925 names,
926 name_to_type,
927 node_members,
928 type_to_node,
929 name_to_node_id,
930 method_identifiers,
931 function_return_types,
932 using_for,
933 using_for_wildcard,
934 general_completions,
935 scope_declarations,
936 scope_parent,
937 scope_ranges,
938 path_to_file_id,
939 linearized_base_contracts,
940 contract_kinds,
941 top_level_importables_by_name,
942 top_level_importables_by_file,
943 }
944}
945
946fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
948 let items = match name {
949 "msg" => vec![
950 ("data", "bytes calldata"),
951 ("sender", "address"),
952 ("sig", "bytes4"),
953 ("value", "uint256"),
954 ],
955 "block" => vec![
956 ("basefee", "uint256"),
957 ("blobbasefee", "uint256"),
958 ("chainid", "uint256"),
959 ("coinbase", "address payable"),
960 ("difficulty", "uint256"),
961 ("gaslimit", "uint256"),
962 ("number", "uint256"),
963 ("prevrandao", "uint256"),
964 ("timestamp", "uint256"),
965 ],
966 "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
967 "abi" => vec![
968 ("decode(bytes memory, (...))", "..."),
969 ("encode(...)", "bytes memory"),
970 ("encodePacked(...)", "bytes memory"),
971 ("encodeWithSelector(bytes4, ...)", "bytes memory"),
972 ("encodeWithSignature(string memory, ...)", "bytes memory"),
973 ("encodeCall(function, (...))", "bytes memory"),
974 ],
975 "type" => vec![
978 ("name", "string"),
979 ("creationCode", "bytes memory"),
980 ("runtimeCode", "bytes memory"),
981 ("interfaceId", "bytes4"),
982 ("min", "T"),
983 ("max", "T"),
984 ],
985 "bytes" => vec![("concat(...)", "bytes memory")],
987 "string" => vec![("concat(...)", "string memory")],
988 _ => return None,
989 };
990
991 Some(
992 items
993 .into_iter()
994 .map(|(label, detail)| CompletionItem {
995 label: label.to_string(),
996 kind: Some(CompletionItemKind::PROPERTY),
997 detail: Some(detail.to_string()),
998 ..Default::default()
999 })
1000 .collect(),
1001 )
1002}
1003
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1007enum TypeMetaKind {
1008 Contract,
1010 Interface,
1012 IntegerType,
1014 Unknown,
1016}
1017
1018fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
1020 if arg == "int" || arg == "uint" {
1022 return TypeMetaKind::IntegerType;
1023 }
1024 if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1025 && let Ok(n) = suffix.parse::<u16>()
1026 && (8..=256).contains(&n)
1027 && n % 8 == 0
1028 {
1029 return TypeMetaKind::IntegerType;
1030 }
1031
1032 if let Some(c) = cache
1034 && let Some(&node_id) = c.name_to_node_id.get(arg)
1035 {
1036 return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1037 Some("interface") => TypeMetaKind::Interface,
1038 Some("library") => TypeMetaKind::Contract, _ => TypeMetaKind::Contract,
1040 };
1041 }
1042
1043 TypeMetaKind::Unknown
1044}
1045
1046fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1048 let kind = match arg {
1049 Some(a) => classify_type_arg(a, cache),
1050 None => TypeMetaKind::Unknown,
1051 };
1052
1053 let items: Vec<(&str, &str)> = match kind {
1054 TypeMetaKind::Contract => vec![
1055 ("name", "string"),
1056 ("creationCode", "bytes memory"),
1057 ("runtimeCode", "bytes memory"),
1058 ],
1059 TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1060 TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1061 TypeMetaKind::Unknown => vec![
1062 ("name", "string"),
1063 ("creationCode", "bytes memory"),
1064 ("runtimeCode", "bytes memory"),
1065 ("interfaceId", "bytes4"),
1066 ("min", "T"),
1067 ("max", "T"),
1068 ],
1069 };
1070
1071 items
1072 .into_iter()
1073 .map(|(label, detail)| CompletionItem {
1074 label: label.to_string(),
1075 kind: Some(CompletionItemKind::PROPERTY),
1076 detail: Some(detail.to_string()),
1077 ..Default::default()
1078 })
1079 .collect()
1080}
1081
1082fn address_members() -> Vec<CompletionItem> {
1084 [
1085 ("balance", "uint256", CompletionItemKind::PROPERTY),
1086 ("code", "bytes memory", CompletionItemKind::PROPERTY),
1087 ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1088 ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1089 ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1090 (
1091 "call(bytes memory)",
1092 "(bool, bytes memory)",
1093 CompletionItemKind::FUNCTION,
1094 ),
1095 (
1096 "delegatecall(bytes memory)",
1097 "(bool, bytes memory)",
1098 CompletionItemKind::FUNCTION,
1099 ),
1100 (
1101 "staticcall(bytes memory)",
1102 "(bool, bytes memory)",
1103 CompletionItemKind::FUNCTION,
1104 ),
1105 ]
1106 .iter()
1107 .map(|(label, detail, kind)| CompletionItem {
1108 label: label.to_string(),
1109 kind: Some(*kind),
1110 detail: if detail.is_empty() {
1111 None
1112 } else {
1113 Some(detail.to_string())
1114 },
1115 ..Default::default()
1116 })
1117 .collect()
1118}
1119
1120#[derive(Debug, Clone, PartialEq)]
1122pub enum AccessKind {
1123 Plain,
1125 Call,
1127 Index,
1129}
1130
1131#[derive(Debug, Clone, PartialEq)]
1133pub struct DotSegment {
1134 pub name: String,
1135 pub kind: AccessKind,
1136 pub call_args: Option<String>,
1139}
1140
1141fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1145 let close = bytes[pos];
1146 let open = match close {
1147 b')' => b'(',
1148 b']' => b'[',
1149 _ => return pos,
1150 };
1151 let mut depth = 1u32;
1152 let mut i = pos;
1153 while i > 0 && depth > 0 {
1154 i -= 1;
1155 if bytes[i] == close {
1156 depth += 1;
1157 } else if bytes[i] == open {
1158 depth -= 1;
1159 }
1160 }
1161 i
1162}
1163
1164pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1169 let col = character as usize;
1170 if col == 0 {
1171 return vec![];
1172 }
1173
1174 let bytes = line.as_bytes();
1175 let mut segments: Vec<DotSegment> = Vec::new();
1176
1177 let mut pos = col;
1179 if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1180 pos -= 1;
1181 }
1182
1183 loop {
1184 if pos == 0 {
1185 break;
1186 }
1187
1188 let (kind, call_args) = if bytes[pos - 1] == b')' {
1190 let close = pos - 1; pos = skip_brackets_backwards(bytes, close);
1192 let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1194 .trim()
1195 .to_string();
1196 let args = if args_text.is_empty() {
1197 None
1198 } else {
1199 Some(args_text)
1200 };
1201 (AccessKind::Call, args)
1202 } else if bytes[pos - 1] == b']' {
1203 pos -= 1; pos = skip_brackets_backwards(bytes, pos);
1205 (AccessKind::Index, None)
1206 } else {
1207 (AccessKind::Plain, None)
1208 };
1209
1210 let end = pos;
1212 while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1213 pos -= 1;
1214 }
1215
1216 if pos == end {
1217 break;
1219 }
1220
1221 let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1222 segments.push(DotSegment {
1223 name,
1224 kind,
1225 call_args,
1226 });
1227
1228 if pos > 0 && bytes[pos - 1] == b'.' {
1230 pos -= 1; } else {
1232 break;
1233 }
1234 }
1235
1236 segments.reverse(); segments
1238}
1239
1240pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1243 let segments = parse_dot_chain(line, character);
1244 segments.last().map(|s| s.name.clone())
1245}
1246
1247#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1248Solidity AST uses different suffixes in different contexts:
1249 - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1250 - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1251 - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1252All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1253fn strip_type_suffix(type_id: &str) -> &str {
1254 let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1255 s.strip_suffix("_storage")
1256 .or_else(|| s.strip_suffix("_memory"))
1257 .or_else(|| s.strip_suffix("_calldata"))
1258 .unwrap_or(s)
1259}
1260
1261fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1265 if let Some(items) = cache.using_for.get(type_id) {
1267 return items.clone();
1268 }
1269
1270 let base = strip_type_suffix(type_id);
1272 let variants = [
1273 base.to_string(),
1274 format!("{}_storage", base),
1275 format!("{}_storage_ptr", base),
1276 format!("{}_memory", base),
1277 format!("{}_memory_ptr", base),
1278 format!("{}_calldata", base),
1279 ];
1280 for variant in &variants {
1281 if variant.as_str() != type_id
1282 && let Some(items) = cache.using_for.get(variant.as_str())
1283 {
1284 return items.clone();
1285 }
1286 }
1287
1288 vec![]
1289}
1290
1291fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1294 if type_id == "t_address" || type_id == "t_address_payable" {
1296 let mut items = address_members();
1297 if let Some(uf) = cache.using_for.get(type_id) {
1299 items.extend(uf.iter().cloned());
1300 }
1301 items.extend(cache.using_for_wildcard.iter().cloned());
1302 return items;
1303 }
1304
1305 let resolved_node_id = extract_node_id_from_type(type_id)
1306 .or_else(|| cache.type_to_node.get(type_id).copied())
1307 .or_else(|| {
1308 type_id
1310 .strip_prefix("__node_id_")
1311 .and_then(|s| s.parse::<i64>().ok())
1312 .map(NodeId)
1313 });
1314
1315 let mut items = Vec::new();
1316 let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1317
1318 if let Some(node_id) = resolved_node_id {
1319 if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1321 for item in method_items {
1322 seen_labels.insert(item.label.clone());
1323 items.push(item.clone());
1324 }
1325 }
1326
1327 if let Some(members) = cache.node_members.get(&node_id) {
1329 for item in members {
1330 if !seen_labels.contains(&item.label) {
1331 seen_labels.insert(item.label.clone());
1332 items.push(item.clone());
1333 }
1334 }
1335 }
1336 }
1337
1338 let is_contract_name = resolved_node_id
1342 .map(|nid| cache.contract_kinds.contains_key(&nid))
1343 .unwrap_or(false);
1344
1345 if !is_contract_name {
1346 let uf_items = lookup_using_for(cache, type_id);
1348 for item in &uf_items {
1349 if !seen_labels.contains(&item.label) {
1350 seen_labels.insert(item.label.clone());
1351 items.push(item.clone());
1352 }
1353 }
1354
1355 for item in &cache.using_for_wildcard {
1357 if !seen_labels.contains(&item.label) {
1358 seen_labels.insert(item.label.clone());
1359 items.push(item.clone());
1360 }
1361 }
1362 }
1363
1364 items
1365}
1366
1367fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1369 if let Some(tid) = cache.name_to_type.get(name) {
1371 return Some(tid.to_string());
1372 }
1373 if let Some(node_id) = cache.name_to_node_id.get(name) {
1375 for (tid, nid) in &cache.type_to_node {
1377 if nid == node_id {
1378 return Some(tid.to_string());
1379 }
1380 }
1381 return Some(format!("__node_id_{}", node_id));
1383 }
1384 None
1385}
1386
1387pub fn find_innermost_scope(
1391 cache: &CompletionCache,
1392 byte_pos: usize,
1393 file_id: FileId,
1394) -> Option<NodeId> {
1395 cache
1397 .scope_ranges
1398 .iter()
1399 .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1400 .map(|r| r.node_id)
1401}
1402
1403pub fn resolve_name_in_scope(
1412 cache: &CompletionCache,
1413 name: &str,
1414 byte_pos: usize,
1415 file_id: FileId,
1416) -> Option<String> {
1417 let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1418
1419 loop {
1421 if let Some(decls) = cache.scope_declarations.get(¤t_scope) {
1423 for decl in decls {
1424 if decl.name == name {
1425 return Some(decl.type_id.clone());
1426 }
1427 }
1428 }
1429
1430 if let Some(bases) = cache.linearized_base_contracts.get(¤t_scope) {
1434 for &base_id in bases.iter().skip(1) {
1435 if let Some(decls) = cache.scope_declarations.get(&base_id) {
1436 for decl in decls {
1437 if decl.name == name {
1438 return Some(decl.type_id.clone());
1439 }
1440 }
1441 }
1442 }
1443 }
1444
1445 match cache.scope_parent.get(¤t_scope) {
1447 Some(&parent_id) => current_scope = parent_id,
1448 None => break, }
1450 }
1451
1452 resolve_name_to_type_id(cache, name)
1455}
1456
1457fn resolve_member_type(
1462 cache: &CompletionCache,
1463 context_type_id: &str,
1464 member_name: &str,
1465 kind: &AccessKind,
1466) -> Option<String> {
1467 let resolved_node_id = extract_node_id_from_type(context_type_id)
1468 .or_else(|| cache.type_to_node.get(context_type_id).copied())
1469 .or_else(|| {
1470 context_type_id
1472 .strip_prefix("__node_id_")
1473 .and_then(|s| s.parse::<i64>().ok())
1474 .map(NodeId)
1475 });
1476
1477 let node_id = resolved_node_id?;
1478
1479 match kind {
1480 AccessKind::Call => {
1481 cache
1483 .function_return_types
1484 .get(&(node_id, member_name.to_string()))
1485 .cloned()
1486 }
1487 AccessKind::Index => {
1488 if let Some(members) = cache.node_members.get(&node_id) {
1490 for member in members {
1491 if member.label == member_name {
1492 if let Some(tid) = cache.name_to_type.get(member_name) {
1494 if tid.starts_with("t_mapping") {
1495 return extract_mapping_value_type(tid.as_str());
1496 }
1497 return Some(tid.to_string());
1498 }
1499 }
1500 }
1501 }
1502 if let Some(tid) = cache.name_to_type.get(member_name)
1504 && tid.starts_with("t_mapping")
1505 {
1506 return extract_mapping_value_type(tid.as_str());
1507 }
1508 None
1509 }
1510 AccessKind::Plain => {
1511 cache
1513 .name_to_type
1514 .get(member_name)
1515 .map(|tid| tid.to_string())
1516 }
1517 }
1518}
1519
1520pub struct ScopeContext {
1524 pub byte_pos: usize,
1526 pub file_id: FileId,
1528}
1529
1530fn resolve_name(
1534 cache: &CompletionCache,
1535 name: &str,
1536 scope_ctx: Option<&ScopeContext>,
1537) -> Option<String> {
1538 if let Some(ctx) = scope_ctx {
1539 resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1540 } else {
1541 resolve_name_to_type_id(cache, name)
1542 }
1543}
1544
1545pub fn get_dot_completions(
1547 cache: &CompletionCache,
1548 identifier: &str,
1549 scope_ctx: Option<&ScopeContext>,
1550) -> Vec<CompletionItem> {
1551 if let Some(items) = magic_members(identifier) {
1553 return items;
1554 }
1555
1556 let type_id = resolve_name(cache, identifier, scope_ctx);
1558
1559 if let Some(tid) = type_id {
1560 return completions_for_type(cache, &tid);
1561 }
1562
1563 vec![]
1564}
1565
1566pub fn get_chain_completions(
1569 cache: &CompletionCache,
1570 chain: &[DotSegment],
1571 scope_ctx: Option<&ScopeContext>,
1572) -> Vec<CompletionItem> {
1573 if chain.is_empty() {
1574 return vec![];
1575 }
1576
1577 if chain.len() == 1 {
1579 let seg = &chain[0];
1580
1581 match seg.kind {
1583 AccessKind::Plain => {
1584 return get_dot_completions(cache, &seg.name, scope_ctx);
1585 }
1586 AccessKind::Call => {
1587 if seg.name == "type" {
1589 return type_meta_members(seg.call_args.as_deref(), Some(cache));
1590 }
1591 if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1594 return completions_for_type(cache, &type_id);
1595 }
1596 for ((_, fn_name), ret_type) in &cache.function_return_types {
1598 if fn_name == &seg.name {
1599 return completions_for_type(cache, ret_type);
1600 }
1601 }
1602 return vec![];
1603 }
1604 AccessKind::Index => {
1605 if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1607 && tid.starts_with("t_mapping")
1608 && let Some(val_type) = extract_mapping_value_type(&tid)
1609 {
1610 return completions_for_type(cache, &val_type);
1611 }
1612 return vec![];
1613 }
1614 }
1615 }
1616
1617 let first = &chain[0];
1620 let mut current_type = match first.kind {
1621 AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1622 AccessKind::Call => {
1623 resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1625 cache
1626 .function_return_types
1627 .iter()
1628 .find(|((_, fn_name), _)| fn_name == &first.name)
1629 .map(|(_, ret_type)| ret_type.clone())
1630 })
1631 }
1632 AccessKind::Index => {
1633 resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1635 if tid.starts_with("t_mapping") {
1636 extract_mapping_value_type(&tid)
1637 } else {
1638 Some(tid)
1639 }
1640 })
1641 }
1642 };
1643
1644 for seg in &chain[1..] {
1646 let ctx_type = match ¤t_type {
1647 Some(t) => t.clone(),
1648 None => return vec![],
1649 };
1650
1651 current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1652 }
1653
1654 match current_type {
1656 Some(tid) => completions_for_type(cache, &tid),
1657 None => vec![],
1658 }
1659}
1660
1661pub fn get_static_completions() -> Vec<CompletionItem> {
1664 let mut items = Vec::new();
1665
1666 for kw in SOLIDITY_KEYWORDS {
1668 items.push(CompletionItem {
1669 label: kw.to_string(),
1670 kind: Some(CompletionItemKind::KEYWORD),
1671 ..Default::default()
1672 });
1673 }
1674
1675 for (name, detail) in MAGIC_GLOBALS {
1677 items.push(CompletionItem {
1678 label: name.to_string(),
1679 kind: Some(CompletionItemKind::VARIABLE),
1680 detail: Some(detail.to_string()),
1681 ..Default::default()
1682 });
1683 }
1684
1685 for (name, detail) in GLOBAL_FUNCTIONS {
1687 items.push(CompletionItem {
1688 label: name.to_string(),
1689 kind: Some(CompletionItemKind::FUNCTION),
1690 detail: Some(detail.to_string()),
1691 ..Default::default()
1692 });
1693 }
1694
1695 for (name, detail) in ETHER_UNITS {
1697 items.push(CompletionItem {
1698 label: name.to_string(),
1699 kind: Some(CompletionItemKind::UNIT),
1700 detail: Some(detail.to_string()),
1701 ..Default::default()
1702 });
1703 }
1704
1705 for (name, detail) in TIME_UNITS {
1707 items.push(CompletionItem {
1708 label: name.to_string(),
1709 kind: Some(CompletionItemKind::UNIT),
1710 detail: Some(detail.to_string()),
1711 ..Default::default()
1712 });
1713 }
1714
1715 items
1716}
1717
1718pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1720 let mut items = cache.names.clone();
1721 items.extend(get_static_completions());
1722 items
1723}
1724
1725pub fn append_auto_import_candidates_last(
1731 mut base: Vec<CompletionItem>,
1732 mut auto_import_candidates: Vec<CompletionItem>,
1733) -> Vec<CompletionItem> {
1734 let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1735 for item in &auto_import_candidates {
1736 let entry = unique_label_edits
1737 .entry(item.label.clone())
1738 .or_insert_with(|| item.additional_text_edits.clone());
1739 if *entry != item.additional_text_edits {
1740 *entry = None;
1741 }
1742 }
1743
1744 for item in &mut base {
1748 if item.additional_text_edits.is_none()
1749 && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1750 {
1751 item.additional_text_edits = Some(edits.clone());
1752 }
1753 }
1754
1755 for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1756 if item.sort_text.is_none() {
1757 item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1758 }
1759 }
1760
1761 base.extend(auto_import_candidates);
1762 base
1763}
1764
1765pub fn top_level_importable_completion_candidates(
1770 cache: &CompletionCache,
1771 current_file_path: Option<&str>,
1772 source_text: &str,
1773) -> Vec<CompletionItem> {
1774 let mut out = Vec::new();
1775 for symbols in cache.top_level_importables_by_name.values() {
1776 for symbol in symbols {
1777 if let Some(cur) = current_file_path
1778 && cur == symbol.declaring_path
1779 {
1780 continue;
1781 }
1782
1783 let import_path = match current_file_path.and_then(|cur| {
1784 to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
1785 }) {
1786 Some(p) => p,
1787 None => continue,
1788 };
1789
1790 if import_statement_already_present(source_text, &symbol.name, &import_path) {
1791 continue;
1792 }
1793
1794 let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
1795 out.push(CompletionItem {
1796 label: symbol.name.clone(),
1797 kind: Some(symbol.kind),
1798 detail: Some(format!("{} ({import_path})", symbol.node_type)),
1799 additional_text_edits: import_edit.map(|e| vec![e]),
1800 ..Default::default()
1801 });
1802 }
1803 }
1804 out
1805}
1806
1807fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
1808 let from_dir = current_file.parent()?;
1809 let rel = pathdiff::diff_paths(target_file, from_dir)?;
1810 let mut s = rel.to_string_lossy().replace('\\', "/");
1811 if !s.starts_with("./") && !s.starts_with("../") {
1812 s = format!("./{s}");
1813 }
1814 Some(s)
1815}
1816
1817fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
1818 let named = format!("import {{{symbol}}} from \"{import_path}\";");
1819 let full = format!("import \"{import_path}\";");
1820 source_text.contains(&named) || source_text.contains(&full)
1821}
1822
1823fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
1824 let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
1825 let lines: Vec<&str> = source_text.lines().collect();
1826
1827 let last_import_line = lines
1828 .iter()
1829 .enumerate()
1830 .filter(|(_, line)| line.trim_start().starts_with("import "))
1831 .map(|(idx, _)| idx)
1832 .last();
1833
1834 let insert_line = if let Some(idx) = last_import_line {
1835 idx + 1
1836 } else if let Some(idx) = lines
1837 .iter()
1838 .enumerate()
1839 .filter(|(_, line)| line.trim_start().starts_with("pragma "))
1840 .map(|(idx, _)| idx)
1841 .last()
1842 {
1843 idx + 1
1844 } else {
1845 0
1846 };
1847
1848 Some(TextEdit {
1849 range: Range {
1850 start: Position {
1851 line: insert_line as u32,
1852 character: 0,
1853 },
1854 end: Position {
1855 line: insert_line as u32,
1856 character: 0,
1857 },
1858 },
1859 new_text: import_stmt,
1860 })
1861}
1862
1863pub fn handle_completion_with_tail_candidates(
1868 cache: Option<&CompletionCache>,
1869 source_text: &str,
1870 position: Position,
1871 trigger_char: Option<&str>,
1872 file_id: Option<FileId>,
1873 tail_candidates: Vec<CompletionItem>,
1874) -> Option<CompletionResponse> {
1875 let lines: Vec<&str> = source_text.lines().collect();
1876 let line = lines.get(position.line as usize)?;
1877
1878 let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
1880 let line_start_byte: usize = source_text[..abs_byte]
1881 .rfind('\n')
1882 .map(|i| i + 1)
1883 .unwrap_or(0);
1884 let col_byte = (abs_byte - line_start_byte) as u32;
1885
1886 let scope_ctx = file_id.map(|fid| ScopeContext {
1888 byte_pos: abs_byte,
1889 file_id: fid,
1890 });
1891
1892 let items = if trigger_char == Some(".") {
1893 let chain = parse_dot_chain(line, col_byte);
1894 if chain.is_empty() {
1895 return None;
1896 }
1897 match cache {
1898 Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
1899 None => {
1900 if chain.len() == 1 {
1902 let seg = &chain[0];
1903 if seg.name == "type" && seg.kind == AccessKind::Call {
1904 type_meta_members(seg.call_args.as_deref(), None)
1906 } else if seg.kind == AccessKind::Plain {
1907 magic_members(&seg.name).unwrap_or_default()
1908 } else {
1909 vec![]
1910 }
1911 } else {
1912 vec![]
1913 }
1914 }
1915 }
1916 } else {
1917 match cache {
1918 Some(c) => {
1919 append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
1920 }
1921 None => get_static_completions(),
1922 }
1923 };
1924
1925 Some(CompletionResponse::List(CompletionList {
1926 is_incomplete: cache.is_none(),
1927 items,
1928 }))
1929}
1930
1931pub fn handle_completion(
1941 cache: Option<&CompletionCache>,
1942 source_text: &str,
1943 position: Position,
1944 trigger_char: Option<&str>,
1945 file_id: Option<FileId>,
1946) -> Option<CompletionResponse> {
1947 handle_completion_with_tail_candidates(
1948 cache,
1949 source_text,
1950 position,
1951 trigger_char,
1952 file_id,
1953 vec![],
1954 )
1955}
1956
1957const SOLIDITY_KEYWORDS: &[&str] = &[
1958 "abstract",
1959 "address",
1960 "assembly",
1961 "bool",
1962 "break",
1963 "bytes",
1964 "bytes1",
1965 "bytes4",
1966 "bytes32",
1967 "calldata",
1968 "constant",
1969 "constructor",
1970 "continue",
1971 "contract",
1972 "delete",
1973 "do",
1974 "else",
1975 "emit",
1976 "enum",
1977 "error",
1978 "event",
1979 "external",
1980 "fallback",
1981 "false",
1982 "for",
1983 "function",
1984 "if",
1985 "immutable",
1986 "import",
1987 "indexed",
1988 "int8",
1989 "int24",
1990 "int128",
1991 "int256",
1992 "interface",
1993 "internal",
1994 "library",
1995 "mapping",
1996 "memory",
1997 "modifier",
1998 "new",
1999 "override",
2000 "payable",
2001 "pragma",
2002 "private",
2003 "public",
2004 "pure",
2005 "receive",
2006 "return",
2007 "returns",
2008 "revert",
2009 "storage",
2010 "string",
2011 "struct",
2012 "true",
2013 "type",
2014 "uint8",
2015 "uint24",
2016 "uint128",
2017 "uint160",
2018 "uint256",
2019 "unchecked",
2020 "using",
2021 "view",
2022 "virtual",
2023 "while",
2024];
2025
2026const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2028
2029const TIME_UNITS: &[(&str, &str)] = &[
2031 ("seconds", "1"),
2032 ("minutes", "60 seconds"),
2033 ("hours", "3600 seconds"),
2034 ("days", "86400 seconds"),
2035 ("weeks", "604800 seconds"),
2036];
2037
2038const MAGIC_GLOBALS: &[(&str, &str)] = &[
2039 ("msg", "msg"),
2040 ("block", "block"),
2041 ("tx", "tx"),
2042 ("abi", "abi"),
2043 ("this", "address"),
2044 ("super", "contract"),
2045 ("type", "type information"),
2046];
2047
2048const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2049 ("addmod(uint256, uint256, uint256)", "uint256"),
2051 ("mulmod(uint256, uint256, uint256)", "uint256"),
2052 ("keccak256(bytes memory)", "bytes32"),
2053 ("sha256(bytes memory)", "bytes32"),
2054 ("ripemd160(bytes memory)", "bytes20"),
2055 (
2056 "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2057 "address",
2058 ),
2059 ("blockhash(uint256 blockNumber)", "bytes32"),
2061 ("blobhash(uint256 index)", "bytes32"),
2062 ("gasleft()", "uint256"),
2063 ("assert(bool condition)", ""),
2065 ("require(bool condition)", ""),
2066 ("require(bool condition, string memory message)", ""),
2067 ("revert()", ""),
2068 ("revert(string memory reason)", ""),
2069 ("selfdestruct(address payable recipient)", ""),
2071];
2072
2073pub fn all_sol_import_paths(
2089 current_file: &Path,
2090 project_root: &Path,
2091 remappings: &[String],
2092 typed_range: Option<(u32, u32, u32)>,
2093) -> Vec<CompletionItem> {
2094 let current_dir = match current_file.parent() {
2095 Some(d) => d,
2096 None => return vec![],
2097 };
2098
2099 let parsed_remappings: Vec<(String, std::path::PathBuf)> = remappings
2104 .iter()
2105 .filter_map(|r| {
2106 let mut it = r.splitn(2, '=');
2107 let prefix = it.next()?.to_string();
2108 let target = it.next()?;
2109 if prefix.is_empty() || target.is_empty() {
2110 return None;
2111 }
2112 let raw = project_root.join(target);
2113 let canonical = raw.canonicalize().unwrap_or(raw);
2114 Some((prefix, canonical))
2115 })
2116 .collect();
2117
2118 let skip_dirs: &[&str] = &["out", "cache", "node_modules", ".git"];
2119 let mut items = Vec::new();
2120
2121 collect_sol_files(
2122 project_root,
2123 current_dir,
2124 &parsed_remappings,
2125 skip_dirs,
2126 typed_range,
2127 &mut items,
2128 );
2129 items.sort_by(|a, b| a.label.cmp(&b.label));
2130 items
2131}
2132
2133fn collect_sol_files(
2134 dir: &Path,
2135 current_dir: &Path,
2136 remappings: &[(String, std::path::PathBuf)],
2137 skip_dirs: &[&str],
2138 typed_range: Option<(u32, u32, u32)>,
2139 out: &mut Vec<CompletionItem>,
2140) {
2141 let entries = match std::fs::read_dir(dir) {
2142 Ok(e) => e,
2143 Err(_) => return,
2144 };
2145 for entry in entries.flatten() {
2146 let path = entry.path();
2147 let name = entry.file_name();
2148 let name_str = name.to_string_lossy();
2149
2150 if name_str.starts_with('.') {
2151 continue;
2152 }
2153
2154 if path.is_dir() {
2155 if skip_dirs.contains(&name_str.as_ref()) {
2156 continue;
2157 }
2158 collect_sol_files(&path, current_dir, remappings, skip_dirs, typed_range, out);
2159 continue;
2160 }
2161
2162 if !path.is_file() || !name_str.ends_with(".sol") {
2163 continue;
2164 }
2165
2166 if let Some(rel) = pathdiff::diff_paths(&path, current_dir) {
2168 let s = rel.to_string_lossy().to_string();
2169 let label = if s.starts_with("../") || s.starts_with("./") {
2170 s
2171 } else {
2172 format!("./{s}")
2173 };
2174 out.push(make_import_item(label, typed_range));
2175 }
2176
2177 for (prefix, target_abs) in remappings {
2183 if let Ok(suffix) = path.strip_prefix(target_abs) {
2184 let suffix_str = suffix
2185 .to_string_lossy()
2186 .trim_start_matches(['/', '\\'])
2187 .to_string();
2188 let label = format!("{}{}", prefix, suffix_str);
2189 out.push(make_import_item(label, typed_range));
2190 }
2191 }
2192 }
2193}
2194
2195fn make_import_item(label: String, typed_range: Option<(u32, u32, u32)>) -> CompletionItem {
2196 let text_edit = typed_range.map(|(line, start_col, end_col)| {
2203 tower_lsp::lsp_types::CompletionTextEdit::Edit(TextEdit {
2204 range: Range {
2205 start: Position {
2206 line,
2207 character: start_col,
2208 },
2209 end: Position {
2210 line,
2211 character: end_col,
2212 },
2213 },
2214 new_text: label.clone(),
2215 })
2216 });
2217 CompletionItem {
2218 label: label.clone(),
2219 filter_text: Some(label.clone()),
2220 text_edit,
2221 kind: Some(CompletionItemKind::FILE),
2222 ..Default::default()
2223 }
2224}
2225
2226#[cfg(test)]
2227mod tests {
2228 use super::{
2229 CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2230 build_completion_cache, extract_top_level_importables_for_file,
2231 };
2232 use crate::types::SymbolName;
2233 use serde_json::json;
2234 use std::collections::HashMap;
2235 use tower_lsp::lsp_types::CompletionItemKind;
2236 use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2237
2238 fn empty_cache() -> CompletionCache {
2239 CompletionCache {
2240 names: vec![],
2241 name_to_type: HashMap::new(),
2242 node_members: HashMap::new(),
2243 type_to_node: HashMap::new(),
2244 name_to_node_id: HashMap::new(),
2245 method_identifiers: HashMap::new(),
2246 function_return_types: HashMap::new(),
2247 using_for: HashMap::new(),
2248 using_for_wildcard: vec![],
2249 general_completions: vec![],
2250 scope_declarations: HashMap::new(),
2251 scope_parent: HashMap::new(),
2252 scope_ranges: vec![],
2253 path_to_file_id: HashMap::new(),
2254 linearized_base_contracts: HashMap::new(),
2255 contract_kinds: HashMap::new(),
2256 top_level_importables_by_name: HashMap::new(),
2257 top_level_importables_by_file: HashMap::new(),
2258 }
2259 }
2260
2261 #[test]
2262 fn top_level_importables_include_only_direct_declared_symbols() {
2263 let sources = json!({
2264 "/tmp/A.sol": {
2265 "id": 0,
2266 "ast": {
2267 "id": 1,
2268 "nodeType": "SourceUnit",
2269 "src": "0:100:0",
2270 "nodes": [
2271 { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2272 { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2273 { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2274 ] },
2275 { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2276 { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2277 { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2278 { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2279 { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2280 { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2281 ]
2282 }
2283 }
2284 });
2285
2286 let cache = build_completion_cache(&sources, None, None);
2287 let map = &cache.top_level_importables_by_name;
2288 let by_file = &cache.top_level_importables_by_file;
2289
2290 assert!(map.contains_key("C"));
2291 assert!(map.contains_key("S"));
2292 assert!(map.contains_key("E"));
2293 assert!(map.contains_key("Wad"));
2294 assert!(map.contains_key("freeFn"));
2295 assert!(map.contains_key("TOP_CONST"));
2296
2297 assert!(!map.contains_key("Alias"));
2298 assert!(!map.contains_key("inside"));
2299 assert!(!map.contains_key("TOP_VAR"));
2300
2301 let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2302 let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2303 assert!(file_names.contains(&"C"));
2304 assert!(file_names.contains(&"TOP_CONST"));
2305 assert!(!file_names.contains(&"Alias"));
2306 }
2307
2308 #[test]
2309 fn top_level_importables_keep_multiple_declarations_for_same_name() {
2310 let sources = json!({
2311 "/tmp/A.sol": {
2312 "id": 0,
2313 "ast": {
2314 "id": 1,
2315 "nodeType": "SourceUnit",
2316 "src": "0:100:0",
2317 "nodes": [
2318 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2319 ]
2320 }
2321 },
2322 "/tmp/B.sol": {
2323 "id": 1,
2324 "ast": {
2325 "id": 2,
2326 "nodeType": "SourceUnit",
2327 "src": "0:100:1",
2328 "nodes": [
2329 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2330 ]
2331 }
2332 }
2333 });
2334
2335 let cache = build_completion_cache(&sources, None, None);
2336 let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2337 assert_eq!(entries.len(), 2);
2338 }
2339
2340 #[test]
2341 fn extract_top_level_importables_for_file_finds_expected_symbols() {
2342 let ast = json!({
2343 "id": 1,
2344 "nodeType": "SourceUnit",
2345 "src": "0:100:0",
2346 "nodes": [
2347 { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2348 { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2349 { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2350 ]
2351 });
2352
2353 let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2354 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2355 assert!(names.contains(&"f"));
2356 assert!(names.contains(&"K"));
2357 assert!(!names.contains(&"V"));
2358 }
2359
2360 #[test]
2361 fn top_level_importables_can_be_replaced_per_file() {
2362 let sources = json!({
2363 "/tmp/A.sol": {
2364 "id": 0,
2365 "ast": {
2366 "id": 1,
2367 "nodeType": "SourceUnit",
2368 "src": "0:100:0",
2369 "nodes": [
2370 { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2371 ]
2372 }
2373 },
2374 "/tmp/B.sol": {
2375 "id": 1,
2376 "ast": {
2377 "id": 2,
2378 "nodeType": "SourceUnit",
2379 "src": "0:100:1",
2380 "nodes": [
2381 { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2382 ]
2383 }
2384 }
2385 });
2386
2387 let mut cache = build_completion_cache(&sources, None, None);
2388 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2389
2390 cache.replace_top_level_importables_for_path(
2391 "/tmp/A.sol".to_string(),
2392 vec![TopLevelImportable {
2393 name: "newA".to_string(),
2394 declaring_path: "/tmp/A.sol".to_string(),
2395 node_type: "FunctionDefinition".to_string(),
2396 kind: CompletionItemKind::FUNCTION,
2397 }],
2398 );
2399 assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2400 assert!(cache.top_level_importables_by_name.contains_key("newA"));
2401
2402 cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2403 assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2404 }
2405
2406 #[test]
2407 fn append_auto_import_candidates_last_sets_tail_sort_text() {
2408 let base = vec![CompletionItem {
2409 label: "localVar".to_string(),
2410 ..Default::default()
2411 }];
2412 let auto = vec![CompletionItem {
2413 label: "ImportMe".to_string(),
2414 ..Default::default()
2415 }];
2416
2417 let out = append_auto_import_candidates_last(base, auto);
2418 assert_eq!(out.len(), 2);
2419 assert_eq!(out[1].label, "ImportMe");
2420 assert!(
2421 out[1]
2422 .sort_text
2423 .as_deref()
2424 .is_some_and(|s| s.starts_with("zz_autoimport_"))
2425 );
2426 }
2427
2428 #[test]
2429 fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2430 let base = vec![CompletionItem {
2431 label: "B".to_string(),
2432 ..Default::default()
2433 }];
2434 let auto = vec![
2435 CompletionItem {
2436 label: "B".to_string(),
2437 detail: Some("ContractDefinition (./B.sol)".to_string()),
2438 ..Default::default()
2439 },
2440 CompletionItem {
2441 label: "B".to_string(),
2442 detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2443 ..Default::default()
2444 },
2445 ];
2446
2447 let out = append_auto_import_candidates_last(base, auto);
2448 assert_eq!(out.len(), 3);
2449 }
2450
2451 #[test]
2452 fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2453 let base = vec![CompletionItem {
2454 label: "B".to_string(),
2455 ..Default::default()
2456 }];
2457 let auto = vec![CompletionItem {
2458 label: "B".to_string(),
2459 additional_text_edits: Some(vec![TextEdit {
2460 range: Range {
2461 start: Position {
2462 line: 0,
2463 character: 0,
2464 },
2465 end: Position {
2466 line: 0,
2467 character: 0,
2468 },
2469 },
2470 new_text: "import {B} from \"./B.sol\";\n".to_string(),
2471 }]),
2472 ..Default::default()
2473 }];
2474 let out = append_auto_import_candidates_last(base, auto);
2475 assert!(
2476 out[0].additional_text_edits.is_some(),
2477 "base item should inherit unique import edit"
2478 );
2479 }
2480
2481 #[test]
2482 fn top_level_importable_candidates_include_import_edit() {
2483 let mut cache = empty_cache();
2484 cache.top_level_importables_by_name.insert(
2485 SymbolName::new("B"),
2486 vec![TopLevelImportable {
2487 name: "B".to_string(),
2488 declaring_path: "/tmp/example/B.sol".to_string(),
2489 node_type: "ContractDefinition".to_string(),
2490 kind: CompletionItemKind::CLASS,
2491 }],
2492 );
2493
2494 let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2495 let items = super::top_level_importable_completion_candidates(
2496 &cache,
2497 Some("/tmp/example/A.sol"),
2498 source,
2499 );
2500 assert_eq!(items.len(), 1);
2501 let edit_text = items[0]
2502 .additional_text_edits
2503 .as_ref()
2504 .and_then(|edits| edits.first())
2505 .map(|e| e.new_text.clone())
2506 .unwrap_or_default();
2507 assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2508 }
2509
2510 #[test]
2511 fn handle_completion_general_path_keeps_base_items() {
2512 let mut cache = empty_cache();
2513 cache.general_completions = vec![CompletionItem {
2514 label: "A".to_string(),
2515 ..Default::default()
2516 }];
2517
2518 let resp = super::handle_completion(
2519 Some(&cache),
2520 "contract X {}",
2521 Position {
2522 line: 0,
2523 character: 0,
2524 },
2525 None,
2526 None,
2527 );
2528 match resp {
2529 Some(CompletionResponse::List(list)) => {
2530 assert_eq!(list.items.len(), 1);
2531 assert_eq!(list.items[0].label, "A");
2532 }
2533 _ => panic!("expected completion list"),
2534 }
2535 }
2536
2537 #[test]
2540 fn make_import_item_has_filter_text_and_text_edit() {
2541 let item = super::make_import_item("./src/Pool.sol".to_string(), Some((0, 8, 12)));
2542 assert_eq!(item.filter_text.as_deref(), Some("./src/Pool.sol"));
2543 assert!(item.text_edit.is_some(), "text_edit should be set");
2544 assert!(
2545 item.insert_text.is_none(),
2546 "insert_text should be absent when text_edit is set"
2547 );
2548 }
2549
2550 #[test]
2551 fn all_sol_import_paths_no_panic_missing_dir() {
2552 let current = std::path::Path::new("/nonexistent/src/Foo.sol");
2553 let root = std::path::Path::new("/nonexistent");
2554 let items = super::all_sol_import_paths(current, root, &[], None);
2555 assert!(items.is_empty());
2556 }
2557
2558 #[test]
2559 fn all_sol_import_paths_relative_labels() {
2560 let root = std::path::Path::new("example");
2562 let current = root.join("A.sol");
2563 let items = super::all_sol_import_paths(¤t, root, &[], None);
2564 for item in &items {
2566 assert!(
2567 item.label.starts_with("./") || item.label.starts_with("../"),
2568 "label should be relative: {}",
2569 item.label
2570 );
2571 assert!(
2572 item.label.ends_with(".sol"),
2573 "label should end with .sol: {}",
2574 item.label
2575 );
2576 }
2577 assert!(!items.is_empty(), "should find at least one .sol file");
2578 }
2579}