1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{
4 Documentation, Hover, HoverContents, MarkupContent, MarkupKind, ParameterInformation,
5 ParameterLabel, Position, SignatureHelp, SignatureInformation, Url,
6};
7
8#[cfg(test)]
9use crate::goto::CHILD_KEYS;
10use crate::goto::pos_to_bytes;
11use crate::references::{byte_to_decl_via_external_refs, byte_to_id};
12#[cfg(test)]
13use crate::types::NodeId;
14use crate::types::{EventSelector, FuncSelector, Selector};
15
16#[derive(Debug, Clone, Default)]
20pub struct DocEntry {
21 pub notice: Option<String>,
23 pub details: Option<String>,
25 pub params: Vec<(String, String)>,
27 pub returns: Vec<(String, String)>,
29 pub title: Option<String>,
31 pub author: Option<String>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum DocKey {
38 Func(FuncSelector),
40 Event(EventSelector),
42 Contract(String),
44 StateVar(String),
46 Method(String),
48}
49
50pub type DocIndex = HashMap<DocKey, DocEntry>;
54
55pub fn build_doc_index(ast_data: &Value) -> DocIndex {
60 let mut index = DocIndex::new();
61
62 let contracts = match ast_data.get("contracts").and_then(|c| c.as_object()) {
63 Some(c) => c,
64 None => return index,
65 };
66
67 for (path, names) in contracts {
68 let names_obj = match names.as_object() {
69 Some(n) => n,
70 None => continue,
71 };
72
73 for (name, contract) in names_obj {
74 let userdoc = contract.get("userdoc");
75 let devdoc = contract.get("devdoc");
76 let method_ids = contract
77 .get("evm")
78 .and_then(|e| e.get("methodIdentifiers"))
79 .and_then(|m| m.as_object());
80
81 let sig_to_selector: HashMap<&str, &str> = method_ids
83 .map(|mi| {
84 mi.iter()
85 .filter_map(|(sig, sel)| sel.as_str().map(|s| (sig.as_str(), s)))
86 .collect()
87 })
88 .unwrap_or_default();
89
90 let mut contract_entry = DocEntry::default();
92 if let Some(ud) = userdoc {
93 contract_entry.notice = ud
94 .get("notice")
95 .and_then(|v| v.as_str())
96 .map(|s| s.to_string());
97 }
98 if let Some(dd) = devdoc {
99 contract_entry.title = dd
100 .get("title")
101 .and_then(|v| v.as_str())
102 .map(|s| s.to_string());
103 contract_entry.details = dd
104 .get("details")
105 .and_then(|v| v.as_str())
106 .map(|s| s.to_string());
107 contract_entry.author = dd
108 .get("author")
109 .and_then(|v| v.as_str())
110 .map(|s| s.to_string());
111 }
112 if contract_entry.notice.is_some()
113 || contract_entry.title.is_some()
114 || contract_entry.details.is_some()
115 {
116 let key = DocKey::Contract(format!("{path}:{name}"));
117 index.insert(key, contract_entry);
118 }
119
120 let ud_methods = userdoc
122 .and_then(|u| u.get("methods"))
123 .and_then(|m| m.as_object());
124 let dd_methods = devdoc
125 .and_then(|d| d.get("methods"))
126 .and_then(|m| m.as_object());
127
128 let mut all_sigs: Vec<&str> = Vec::new();
130 if let Some(um) = ud_methods {
131 all_sigs.extend(um.keys().map(|k| k.as_str()));
132 }
133 if let Some(dm) = dd_methods {
134 for k in dm.keys() {
135 if !all_sigs.contains(&k.as_str()) {
136 all_sigs.push(k.as_str());
137 }
138 }
139 }
140
141 for sig in &all_sigs {
142 let mut entry = DocEntry::default();
143
144 if let Some(um) = ud_methods
146 && let Some(method) = um.get(*sig)
147 {
148 entry.notice = method
149 .get("notice")
150 .and_then(|v| v.as_str())
151 .map(|s| s.to_string());
152 }
153
154 if let Some(dm) = dd_methods
156 && let Some(method) = dm.get(*sig)
157 {
158 entry.details = method
159 .get("details")
160 .and_then(|v| v.as_str())
161 .map(|s| s.to_string());
162
163 if let Some(params) = method.get("params").and_then(|p| p.as_object()) {
164 for (pname, pdesc) in params {
165 if let Some(desc) = pdesc.as_str() {
166 entry.params.push((pname.clone(), desc.to_string()));
167 }
168 }
169 }
170
171 if let Some(returns) = method.get("returns").and_then(|r| r.as_object()) {
172 for (rname, rdesc) in returns {
173 if let Some(desc) = rdesc.as_str() {
174 entry.returns.push((rname.clone(), desc.to_string()));
175 }
176 }
177 }
178 }
179
180 if entry.notice.is_none()
181 && entry.details.is_none()
182 && entry.params.is_empty()
183 && entry.returns.is_empty()
184 {
185 continue;
186 }
187
188 if let Some(selector) = sig_to_selector.get(sig) {
190 let key = DocKey::Func(FuncSelector::new(*selector));
191 index.insert(key, entry);
192 } else {
193 let fn_name = sig.split('(').next().unwrap_or(sig);
196 let key = DocKey::Method(format!("{path}:{name}:{fn_name}"));
197 index.insert(key, entry);
198 }
199 }
200
201 let ud_errors = userdoc
203 .and_then(|u| u.get("errors"))
204 .and_then(|e| e.as_object());
205 let dd_errors = devdoc
206 .and_then(|d| d.get("errors"))
207 .and_then(|e| e.as_object());
208
209 let mut all_error_sigs: Vec<&str> = Vec::new();
210 if let Some(ue) = ud_errors {
211 all_error_sigs.extend(ue.keys().map(|k| k.as_str()));
212 }
213 if let Some(de) = dd_errors {
214 for k in de.keys() {
215 if !all_error_sigs.contains(&k.as_str()) {
216 all_error_sigs.push(k.as_str());
217 }
218 }
219 }
220
221 for sig in &all_error_sigs {
222 let mut entry = DocEntry::default();
223
224 if let Some(ue) = ud_errors
226 && let Some(arr) = ue.get(*sig).and_then(|v| v.as_array())
227 && let Some(first) = arr.first()
228 {
229 entry.notice = first
230 .get("notice")
231 .and_then(|v| v.as_str())
232 .map(|s| s.to_string());
233 }
234
235 if let Some(de) = dd_errors
237 && let Some(arr) = de.get(*sig).and_then(|v| v.as_array())
238 && let Some(first) = arr.first()
239 {
240 entry.details = first
241 .get("details")
242 .and_then(|v| v.as_str())
243 .map(|s| s.to_string());
244 if let Some(params) = first.get("params").and_then(|p| p.as_object()) {
245 for (pname, pdesc) in params {
246 if let Some(desc) = pdesc.as_str() {
247 entry.params.push((pname.clone(), desc.to_string()));
248 }
249 }
250 }
251 }
252
253 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
254 continue;
255 }
256
257 let selector = FuncSelector::new(compute_selector(sig));
260 index.insert(DocKey::Func(selector), entry);
261 }
262
263 let ud_events = userdoc
265 .and_then(|u| u.get("events"))
266 .and_then(|e| e.as_object());
267 let dd_events = devdoc
268 .and_then(|d| d.get("events"))
269 .and_then(|e| e.as_object());
270
271 let mut all_event_sigs: Vec<&str> = Vec::new();
272 if let Some(ue) = ud_events {
273 all_event_sigs.extend(ue.keys().map(|k| k.as_str()));
274 }
275 if let Some(de) = dd_events {
276 for k in de.keys() {
277 if !all_event_sigs.contains(&k.as_str()) {
278 all_event_sigs.push(k.as_str());
279 }
280 }
281 }
282
283 for sig in &all_event_sigs {
284 let mut entry = DocEntry::default();
285
286 if let Some(ue) = ud_events
287 && let Some(ev) = ue.get(*sig)
288 {
289 entry.notice = ev
290 .get("notice")
291 .and_then(|v| v.as_str())
292 .map(|s| s.to_string());
293 }
294
295 if let Some(de) = dd_events
296 && let Some(ev) = de.get(*sig)
297 {
298 entry.details = ev
299 .get("details")
300 .and_then(|v| v.as_str())
301 .map(|s| s.to_string());
302 if let Some(params) = ev.get("params").and_then(|p| p.as_object()) {
303 for (pname, pdesc) in params {
304 if let Some(desc) = pdesc.as_str() {
305 entry.params.push((pname.clone(), desc.to_string()));
306 }
307 }
308 }
309 }
310
311 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
312 continue;
313 }
314
315 let topic = EventSelector::new(compute_event_topic(sig));
317 index.insert(DocKey::Event(topic), entry);
318 }
319
320 if let Some(dd) = devdoc
322 && let Some(state_vars) = dd.get("stateVariables").and_then(|s| s.as_object())
323 {
324 for (var_name, var_doc) in state_vars {
325 let mut entry = DocEntry {
326 details: var_doc
327 .get("details")
328 .and_then(|v| v.as_str())
329 .map(|s| s.to_string()),
330 ..DocEntry::default()
331 };
332
333 if let Some(returns) = var_doc.get("return").and_then(|v| v.as_str()) {
334 entry.returns.push(("_0".to_string(), returns.to_string()));
335 }
336 if let Some(returns) = var_doc.get("returns").and_then(|r| r.as_object()) {
337 for (rname, rdesc) in returns {
338 if let Some(desc) = rdesc.as_str() {
339 entry.returns.push((rname.clone(), desc.to_string()));
340 }
341 }
342 }
343
344 if entry.details.is_some() || !entry.returns.is_empty() {
345 let key = DocKey::StateVar(format!("{path}:{name}:{var_name}"));
346 index.insert(key, entry);
347 }
348 }
349 }
350 }
351 }
352
353 index
354}
355
356fn compute_selector(sig: &str) -> String {
360 use tiny_keccak::{Hasher, Keccak};
361 let mut hasher = Keccak::v256();
362 hasher.update(sig.as_bytes());
363 let mut output = [0u8; 32];
364 hasher.finalize(&mut output);
365 hex::encode(&output[..4])
366}
367
368fn compute_event_topic(sig: &str) -> String {
372 use tiny_keccak::{Hasher, Keccak};
373 let mut hasher = Keccak::v256();
374 hasher.update(sig.as_bytes());
375 let mut output = [0u8; 32];
376 hasher.finalize(&mut output);
377 hex::encode(output)
378}
379
380pub fn lookup_doc_entry_typed(
388 doc_index: &DocIndex,
389 decl: &crate::solc_ast::DeclNode,
390 decl_index: &std::collections::HashMap<crate::types::NodeId, crate::solc_ast::DeclNode>,
391 node_id_to_source_path: &std::collections::HashMap<crate::types::NodeId, crate::types::AbsPath>,
392) -> Option<DocEntry> {
393 use crate::solc_ast::DeclNode;
394
395 match decl {
396 DeclNode::FunctionDefinition(_) | DeclNode::VariableDeclaration(_) => {
397 if let Some(sel) = decl.selector() {
399 let key = DocKey::Func(FuncSelector::new(sel));
400 if let Some(entry) = doc_index.get(&key) {
401 return Some(entry.clone());
402 }
403 }
404
405 if matches!(decl, DeclNode::VariableDeclaration(_))
407 && let var_name = decl.name()
408 && let Some(scope_id) = decl.scope()
409 && let Some(scope_decl) = decl_index.get(&crate::types::NodeId(scope_id))
410 && let Some(path) = node_id_to_source_path.get(&crate::types::NodeId(scope_id))
411 {
412 let contract_name = scope_decl.name();
413 let key = DocKey::StateVar(format!("{path}:{contract_name}:{var_name}"));
414 if let Some(entry) = doc_index.get(&key) {
415 return Some(entry.clone());
416 }
417 }
418
419 let fn_name = decl.name();
421 let scope_id = decl.scope()?;
422 let scope_decl = decl_index.get(&crate::types::NodeId(scope_id))?;
423 let contract_name = scope_decl.name();
424 let path = node_id_to_source_path.get(&crate::types::NodeId(scope_id))?;
425 let key = DocKey::Method(format!("{path}:{contract_name}:{fn_name}"));
426 doc_index.get(&key).cloned()
427 }
428 DeclNode::ErrorDefinition(_) => {
429 let sel = decl.selector()?;
430 let key = DocKey::Func(FuncSelector::new(sel));
431 doc_index.get(&key).cloned()
432 }
433 DeclNode::EventDefinition(_) => {
434 let sel = decl.selector()?;
435 let key = DocKey::Event(EventSelector::new(sel));
436 doc_index.get(&key).cloned()
437 }
438 DeclNode::ContractDefinition(_) => {
439 let contract_name = decl.name();
440 let node_id = decl.id();
441 let path = node_id_to_source_path.get(&crate::types::NodeId(node_id))?;
442 let key = DocKey::Contract(format!("{path}:{contract_name}"));
443 doc_index.get(&key).cloned()
444 }
445 _ => None,
446 }
447}
448
449pub fn lookup_param_doc_typed(
458 doc_index: &DocIndex,
459 decl: &crate::solc_ast::DeclNode,
460 decl_index: &std::collections::HashMap<crate::types::NodeId, crate::solc_ast::DeclNode>,
461 node_id_to_source_path: &std::collections::HashMap<crate::types::NodeId, crate::types::AbsPath>,
462) -> Option<String> {
463 use crate::solc_ast::DeclNode;
464
465 let var = match decl {
467 DeclNode::VariableDeclaration(v) => v,
468 _ => return None,
469 };
470
471 let param_name = &var.name;
472 if param_name.is_empty() {
473 return None;
474 }
475
476 let scope_id = var.scope?;
478 let parent = decl_index.get(&crate::types::NodeId(scope_id))?;
479
480 if !matches!(
482 parent,
483 DeclNode::FunctionDefinition(_)
484 | DeclNode::ErrorDefinition(_)
485 | DeclNode::EventDefinition(_)
486 | DeclNode::ModifierDefinition(_)
487 ) {
488 return None;
489 }
490
491 let is_return = if let Some(ret_params) = parent.return_parameters() {
493 ret_params.parameters.iter().any(|p| p.id == var.id)
494 } else {
495 false
496 };
497
498 if let Some(parent_doc) =
500 lookup_doc_entry_typed(doc_index, parent, decl_index, node_id_to_source_path)
501 {
502 if is_return {
503 for (rname, rdesc) in &parent_doc.returns {
504 if rname == param_name {
505 return Some(rdesc.clone());
506 }
507 }
508 } else {
509 for (pname, pdesc) in &parent_doc.params {
510 if pname == param_name {
511 return Some(pdesc.clone());
512 }
513 }
514 }
515 }
516
517 if let Some(doc_text) = parent.extract_doc_text() {
519 let resolved = if doc_text.contains("@inheritdoc") {
520 resolve_inheritdoc_typed(parent, &doc_text, decl_index)
521 } else {
522 None
523 };
524 let text = resolved.as_deref().unwrap_or(&doc_text);
525
526 let tag = if is_return { "@return " } else { "@param " };
527 for line in text.lines() {
528 let trimmed = line.trim().trim_start_matches('*').trim();
529 if let Some(rest) = trimmed.strip_prefix(tag) {
530 if let Some((name, desc)) = rest.split_once(' ') {
531 if name == param_name {
532 return Some(desc.to_string());
533 }
534 } else if rest == param_name {
535 return Some(String::new());
536 }
537 }
538 }
539 }
540
541 None
542}
543
544pub fn format_doc_entry(entry: &DocEntry) -> String {
546 let mut lines: Vec<String> = Vec::new();
547
548 if let Some(title) = &entry.title {
550 lines.push(format!("**{title}**"));
551 lines.push(String::new());
552 }
553
554 if let Some(notice) = &entry.notice {
556 lines.push(notice.clone());
557 }
558
559 if let Some(author) = &entry.author {
561 lines.push(format!("*@author {author}*"));
562 }
563
564 if let Some(details) = &entry.details {
566 lines.push(String::new());
567 lines.push("**@dev**".to_string());
568 lines.push(format!("*{details}*"));
569 }
570
571 if !entry.params.is_empty() {
573 lines.push(String::new());
574 lines.push("**Parameters:**".to_string());
575 for (name, desc) in &entry.params {
576 lines.push(format!("- `{name}` — {desc}"));
577 }
578 }
579
580 if !entry.returns.is_empty() {
582 lines.push(String::new());
583 lines.push("**Returns:**".to_string());
584 for (name, desc) in &entry.returns {
585 if name.starts_with('_') && name.len() <= 3 {
586 lines.push(format!("- {desc}"));
588 } else {
589 lines.push(format!("- `{name}` — {desc}"));
590 }
591 }
592 }
593
594 lines.join("\n")
595}
596
597#[cfg(test)]
602fn find_node_by_id(sources: &Value, target_id: NodeId) -> Option<&Value> {
603 let sources_obj = sources.as_object()?;
604 for (_path, source_data) in sources_obj {
605 let ast = source_data.get("ast")?;
606
607 if ast.get("id").and_then(|v| v.as_i64()) == Some(target_id.0) {
609 return Some(ast);
610 }
611
612 let mut stack = vec![ast];
613 while let Some(node) = stack.pop() {
614 if node.get("id").and_then(|v| v.as_i64()) == Some(target_id.0) {
615 return Some(node);
616 }
617 for key in CHILD_KEYS {
618 if let Some(value) = node.get(key) {
619 match value {
620 Value::Array(arr) => stack.extend(arr.iter()),
621 Value::Object(_) => stack.push(value),
622 _ => {}
623 }
624 }
625 }
626 }
627 }
628 None
629}
630
631pub fn extract_documentation(node: &Value) -> Option<String> {
634 let doc = node.get("documentation")?;
635 match doc {
636 Value::Object(_) => doc
637 .get("text")
638 .and_then(|v| v.as_str())
639 .map(|s| s.to_string()),
640 Value::String(s) => Some(s.clone()),
641 _ => None,
642 }
643}
644
645pub fn extract_selector(node: &Value) -> Option<Selector> {
650 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
651 match node_type {
652 "FunctionDefinition" | "VariableDeclaration" => node
653 .get("functionSelector")
654 .and_then(|v| v.as_str())
655 .map(|s| Selector::Func(FuncSelector::new(s))),
656 "ErrorDefinition" => node
657 .get("errorSelector")
658 .and_then(|v| v.as_str())
659 .map(|s| Selector::Func(FuncSelector::new(s))),
660 "EventDefinition" => node
661 .get("eventSelector")
662 .and_then(|v| v.as_str())
663 .map(|s| Selector::Event(EventSelector::new(s))),
664 _ => None,
665 }
666}
667
668pub fn resolve_inheritdoc_typed(
671 decl: &crate::solc_ast::DeclNode,
672 doc_text: &str,
673 decl_index: &std::collections::HashMap<crate::types::NodeId, crate::solc_ast::DeclNode>,
674) -> Option<String> {
675 use crate::solc_ast::DeclNode;
676
677 let parent_name = doc_text
679 .lines()
680 .find_map(|line| {
681 let trimmed = line.trim().trim_start_matches('*').trim();
682 trimmed.strip_prefix("@inheritdoc ")
683 })?
684 .trim();
685
686 let impl_selector = decl.extract_typed_selector()?;
688
689 let scope_id = decl.scope()?;
691
692 let scope_decl = decl_index.get(&crate::types::NodeId(scope_id))?;
694 let scope_contract = match scope_decl {
695 DeclNode::ContractDefinition(c) => c,
696 _ => return None,
697 };
698
699 let parent_id = scope_contract.base_contracts.iter().find_map(|base| {
701 if base.base_name.name == parent_name {
702 base.base_name.referenced_declaration
703 } else {
704 None
705 }
706 })?;
707
708 let parent_decl = decl_index.get(&crate::types::NodeId(parent_id))?;
710 let parent_contract = match parent_decl {
711 DeclNode::ContractDefinition(c) => c,
712 _ => return None,
713 };
714
715 for child in &parent_contract.nodes {
717 if let Some(child_sel_str) = child.selector() {
718 let child_matches = match &impl_selector {
720 crate::types::Selector::Func(fs) => child_sel_str == fs.as_hex(),
721 crate::types::Selector::Event(es) => child_sel_str == es.as_hex(),
722 };
723 if child_matches {
724 return child.documentation_text();
725 }
726 }
727 }
728
729 None
730}
731
732pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
736 let mut lines: Vec<String> = Vec::new();
737 let mut in_params = false;
738 let mut in_returns = false;
739
740 for raw_line in text.lines() {
741 let line = raw_line.trim().trim_start_matches('*').trim();
742 if line.is_empty() {
743 continue;
744 }
745
746 if let Some(rest) = line.strip_prefix("@title ") {
747 in_params = false;
748 in_returns = false;
749 lines.push(format!("**{rest}**"));
750 lines.push(String::new());
751 } else if let Some(rest) = line.strip_prefix("@notice ") {
752 in_params = false;
753 in_returns = false;
754 lines.push(rest.to_string());
755 } else if let Some(rest) = line.strip_prefix("@dev ") {
756 in_params = false;
757 in_returns = false;
758 lines.push(String::new());
759 lines.push("**@dev**".to_string());
760 lines.push(format!("*{rest}*"));
761 } else if let Some(rest) = line.strip_prefix("@param ") {
762 if !in_params {
763 in_params = true;
764 in_returns = false;
765 lines.push(String::new());
766 lines.push("**Parameters:**".to_string());
767 }
768 if let Some((name, desc)) = rest.split_once(' ') {
769 lines.push(format!("- `{name}` — {desc}"));
770 } else {
771 lines.push(format!("- `{rest}`"));
772 }
773 } else if let Some(rest) = line.strip_prefix("@return ") {
774 if !in_returns {
775 in_returns = true;
776 in_params = false;
777 lines.push(String::new());
778 lines.push("**Returns:**".to_string());
779 }
780 if let Some((name, desc)) = rest.split_once(' ') {
781 lines.push(format!("- `{name}` — {desc}"));
782 } else {
783 lines.push(format!("- `{rest}`"));
784 }
785 } else if let Some(rest) = line.strip_prefix("@author ") {
786 in_params = false;
787 in_returns = false;
788 lines.push(format!("*@author {rest}*"));
789 } else if line.starts_with("@inheritdoc ") {
790 if let Some(inherited) = inherited_doc {
792 let formatted = format_natspec(inherited, None);
794 if !formatted.is_empty() {
795 lines.push(formatted);
796 }
797 } else {
798 let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
799 lines.push(format!("*Inherits documentation from `{parent}`*"));
800 }
801 } else if line.starts_with('@') {
802 in_params = false;
804 in_returns = false;
805 if let Some((tag, rest)) = line.split_once(' ') {
806 lines.push(String::new());
807 lines.push(format!("**{tag}**"));
808 lines.push(format!("*{rest}*"));
809 } else {
810 lines.push(String::new());
811 lines.push(format!("**{line}**"));
812 }
813 } else {
814 lines.push(line.to_string());
816 }
817 }
818
819 lines.join("\n")
820}
821
822fn format_parameters(params_node: Option<&Value>) -> String {
824 let params_node = match params_node {
825 Some(v) => v,
826 None => return String::new(),
827 };
828 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
829 Some(arr) => arr,
830 None => return String::new(),
831 };
832
833 let parts: Vec<String> = params
834 .iter()
835 .map(|p| {
836 let type_str = p
837 .get("typeDescriptions")
838 .and_then(|v| v.get("typeString"))
839 .and_then(|v| v.as_str())
840 .unwrap_or("?");
841 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
842 let storage = p
843 .get("storageLocation")
844 .and_then(|v| v.as_str())
845 .unwrap_or("default");
846
847 if name.is_empty() {
848 type_str.to_string()
849 } else if storage != "default" {
850 format!("{type_str} {storage} {name}")
851 } else {
852 format!("{type_str} {name}")
853 }
854 })
855 .collect();
856
857 parts.join(", ")
858}
859
860pub(crate) fn build_function_signature(node: &Value) -> Option<String> {
862 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
863 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
864
865 match node_type {
866 "FunctionDefinition" => {
867 let kind = node
868 .get("kind")
869 .and_then(|v| v.as_str())
870 .unwrap_or("function");
871 let visibility = node
872 .get("visibility")
873 .and_then(|v| v.as_str())
874 .unwrap_or("");
875 let state_mutability = node
876 .get("stateMutability")
877 .and_then(|v| v.as_str())
878 .unwrap_or("");
879
880 let params = format_parameters(node.get("parameters"));
881 let returns = format_parameters(node.get("returnParameters"));
882
883 let mut sig = match kind {
884 "constructor" => format!("constructor({params})"),
885 "receive" => "receive() external payable".to_string(),
886 "fallback" => format!("fallback({params})"),
887 _ => format!("function {name}({params})"),
888 };
889
890 if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
891 sig.push_str(&format!(" {visibility}"));
892 }
893 if !state_mutability.is_empty() && state_mutability != "nonpayable" {
894 sig.push_str(&format!(" {state_mutability}"));
895 }
896 if !returns.is_empty() {
897 sig.push_str(&format!(" returns ({returns})"));
898 }
899 Some(sig)
900 }
901 "ModifierDefinition" => {
902 let params = format_parameters(node.get("parameters"));
903 Some(format!("modifier {name}({params})"))
904 }
905 "EventDefinition" => {
906 let params = format_parameters(node.get("parameters"));
907 Some(format!("event {name}({params})"))
908 }
909 "ErrorDefinition" => {
910 let params = format_parameters(node.get("parameters"));
911 Some(format!("error {name}({params})"))
912 }
913 "VariableDeclaration" => {
914 let type_str = node
915 .get("typeDescriptions")
916 .and_then(|v| v.get("typeString"))
917 .and_then(|v| v.as_str())
918 .unwrap_or("unknown");
919 let visibility = node
920 .get("visibility")
921 .and_then(|v| v.as_str())
922 .unwrap_or("");
923 let mutability = node
924 .get("mutability")
925 .and_then(|v| v.as_str())
926 .unwrap_or("");
927
928 let mut sig = type_str.to_string();
929 if !visibility.is_empty() {
930 sig.push_str(&format!(" {visibility}"));
931 }
932 if mutability == "constant" || mutability == "immutable" {
933 sig.push_str(&format!(" {mutability}"));
934 }
935 sig.push_str(&format!(" {name}"));
936 Some(sig)
937 }
938 "ContractDefinition" => {
939 let contract_kind = node
940 .get("contractKind")
941 .and_then(|v| v.as_str())
942 .unwrap_or("contract");
943
944 let mut sig = format!("{contract_kind} {name}");
945
946 if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array())
948 && !bases.is_empty()
949 {
950 let base_names: Vec<&str> = bases
951 .iter()
952 .filter_map(|b| {
953 b.get("baseName")
954 .and_then(|bn| bn.get("name"))
955 .and_then(|n| n.as_str())
956 })
957 .collect();
958 if !base_names.is_empty() {
959 sig.push_str(&format!(" is {}", base_names.join(", ")));
960 }
961 }
962 Some(sig)
963 }
964 "StructDefinition" => {
965 let mut sig = format!("struct {name} {{\n");
966 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
967 for member in members {
968 let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
969 let mtype = member
970 .get("typeDescriptions")
971 .and_then(|v| v.get("typeString"))
972 .and_then(|v| v.as_str())
973 .unwrap_or("?");
974 sig.push_str(&format!(" {mtype} {mname};\n"));
975 }
976 }
977 sig.push('}');
978 Some(sig)
979 }
980 "EnumDefinition" => {
981 let mut sig = format!("enum {name} {{\n");
982 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
983 let names: Vec<&str> = members
984 .iter()
985 .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
986 .collect();
987 for n in &names {
988 sig.push_str(&format!(" {n},\n"));
989 }
990 }
991 sig.push('}');
992 Some(sig)
993 }
994 "UserDefinedValueTypeDefinition" => {
995 let underlying = node
996 .get("underlyingType")
997 .and_then(|v| v.get("typeDescriptions"))
998 .and_then(|v| v.get("typeString"))
999 .and_then(|v| v.as_str())
1000 .unwrap_or("unknown");
1001 Some(format!("type {name} is {underlying}"))
1002 }
1003 _ => None,
1004 }
1005}
1006
1007fn find_mapping_decl_typed<'a>(
1014 decl_index: &'a std::collections::HashMap<crate::types::NodeId, crate::solc_ast::DeclNode>,
1015 name: &str,
1016) -> Option<&'a crate::solc_ast::VariableDeclaration> {
1017 use crate::solc_ast::DeclNode;
1018
1019 decl_index.values().find_map(|decl| match decl {
1020 DeclNode::VariableDeclaration(v)
1021 if v.name == name
1022 && matches!(
1023 v.type_name.as_ref(),
1024 Some(crate::solc_ast::TypeName::Mapping(_))
1025 ) =>
1026 {
1027 Some(v)
1028 }
1029 _ => None,
1030 })
1031}
1032
1033fn mapping_signature_help_typed(
1038 decl_index: &std::collections::HashMap<crate::types::NodeId, crate::solc_ast::DeclNode>,
1039 name: &str,
1040) -> Option<SignatureHelp> {
1041 use crate::solc_ast::TypeName;
1042
1043 let decl = find_mapping_decl_typed(decl_index, name)?;
1044 let mapping = match decl.type_name.as_ref()? {
1045 TypeName::Mapping(m) => m,
1046 _ => return None,
1047 };
1048
1049 let key_type = crate::solc_ast::type_name_to_str(&mapping.key_type);
1051
1052 let key_name = mapping.key_name.as_deref().filter(|s| !s.is_empty());
1054
1055 let param_str = if let Some(kn) = key_name {
1056 format!("{} {}", key_type, kn)
1057 } else {
1058 key_type.to_string()
1059 };
1060
1061 let sig_label = format!("{}[{}]", name, param_str);
1062
1063 let param_start = name.len() + 1; let param_end = param_start + param_str.len();
1065
1066 let key_param_name = key_name.unwrap_or("");
1067 let var_name = &decl.name;
1068
1069 let param_info = ParameterInformation {
1070 label: ParameterLabel::LabelOffsets([param_start as u32, param_end as u32]),
1071 documentation: if !key_param_name.is_empty() {
1072 Some(Documentation::MarkupContent(MarkupContent {
1073 kind: MarkupKind::Markdown,
1074 value: format!("`{}` — key for `{}`", key_param_name, var_name),
1075 }))
1076 } else {
1077 None
1078 },
1079 };
1080
1081 let value_type = crate::solc_ast::type_name_to_str(&mapping.value_type);
1083 let sig_doc = Some(format!("@returns `{}`", value_type));
1084
1085 Some(SignatureHelp {
1086 signatures: vec![SignatureInformation {
1087 label: sig_label,
1088 documentation: sig_doc.map(|doc| {
1089 Documentation::MarkupContent(MarkupContent {
1090 kind: MarkupKind::Markdown,
1091 value: doc,
1092 })
1093 }),
1094 parameters: Some(vec![param_info]),
1095 active_parameter: Some(0),
1096 }],
1097 active_signature: Some(0),
1098 active_parameter: Some(0),
1099 })
1100}
1101
1102pub fn signature_help(
1110 cached_build: &crate::goto::CachedBuild,
1111 source_bytes: &[u8],
1112 position: Position,
1113) -> Option<SignatureHelp> {
1114 let hint_index = &cached_build.hint_index;
1115 let doc_index = &cached_build.doc_index;
1116 let di = &cached_build.decl_index;
1117 let id_to_path = &cached_build.node_id_to_source_path;
1118
1119 let source_str = String::from_utf8_lossy(source_bytes);
1120 let tree = crate::inlay_hints::ts_parse(&source_str)?;
1121 let byte_pos = pos_to_bytes(source_bytes, position);
1122
1123 let ctx =
1125 crate::inlay_hints::ts_find_call_for_signature(tree.root_node(), &source_str, byte_pos)?;
1126
1127 if ctx.is_index_access {
1129 return mapping_signature_help_typed(di, ctx.name);
1130 }
1131
1132 let (decl_id, skip) = hint_index.values().find_map(|lookup| {
1134 lookup.resolve_callsite_with_skip(ctx.call_start_byte, ctx.name, ctx.arg_count)
1135 })?;
1136
1137 let typed_decl = di.get(&decl_id)?;
1139
1140 let sig_label = typed_decl.build_signature()?;
1142
1143 let param_strings = typed_decl.param_strings();
1145
1146 let doc_entry = lookup_doc_entry_typed(doc_index, typed_decl, di, id_to_path);
1148
1149 let params_start = sig_label.find('(')? + 1;
1153 let mut param_infos = Vec::new();
1154 let mut offset = params_start;
1155
1156 for (i, param_str) in param_strings.iter().enumerate() {
1157 let start = offset;
1158 let end = start + param_str.len();
1159
1160 let param_name = typed_decl
1162 .parameters()
1163 .and_then(|pl| pl.parameters.get(i))
1164 .map(|p| p.name.as_str())
1165 .unwrap_or("");
1166
1167 let param_doc = doc_entry.as_ref().and_then(|entry| {
1168 entry
1169 .params
1170 .iter()
1171 .find(|(name, _)| name == param_name)
1172 .map(|(_, desc)| desc.clone())
1173 });
1174
1175 param_infos.push(ParameterInformation {
1176 label: ParameterLabel::LabelOffsets([start as u32, end as u32]),
1177 documentation: param_doc.map(|doc| {
1178 Documentation::MarkupContent(MarkupContent {
1179 kind: MarkupKind::Markdown,
1180 value: doc,
1181 })
1182 }),
1183 });
1184
1185 offset = end + 2;
1187 }
1188
1189 let sig_doc = doc_entry.as_ref().and_then(|entry| {
1191 let mut parts = Vec::new();
1192 if let Some(notice) = &entry.notice {
1193 parts.push(notice.clone());
1194 }
1195 if let Some(details) = &entry.details {
1196 parts.push(format!("*{}*", details));
1197 }
1198 if parts.is_empty() {
1199 None
1200 } else {
1201 Some(parts.join("\n\n"))
1202 }
1203 });
1204
1205 let active_param = (ctx.arg_index + skip) as u32;
1207
1208 Some(SignatureHelp {
1209 signatures: vec![SignatureInformation {
1210 label: sig_label,
1211 documentation: sig_doc.map(|doc| {
1212 Documentation::MarkupContent(MarkupContent {
1213 kind: MarkupKind::Markdown,
1214 value: doc,
1215 })
1216 }),
1217 parameters: Some(param_infos),
1218 active_parameter: Some(active_param),
1219 }],
1220 active_signature: Some(0),
1221 active_parameter: Some(active_param),
1222 })
1223}
1224
1225fn identifier_at(text: &str, pos: usize) -> Option<&str> {
1228 let bytes = text.as_bytes();
1229 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'$';
1230 let pos = pos.min(bytes.len());
1231 let anchor = if pos < bytes.len() && is_ident(bytes[pos]) {
1232 pos
1233 } else if pos > 0 && is_ident(bytes[pos - 1]) {
1234 pos - 1
1235 } else {
1236 return None;
1237 };
1238 let start = bytes[..anchor]
1239 .iter()
1240 .rposition(|&b| !is_ident(b))
1241 .map_or(0, |i| i + 1);
1242 let end = bytes[anchor..]
1243 .iter()
1244 .position(|&b| !is_ident(b))
1245 .map_or(bytes.len(), |i| anchor + i);
1246 Some(&text[start..end])
1247}
1248
1249fn cursor_in_compiled(live: &str, compiled: &str, live_pos: usize) -> Option<usize> {
1253 let mapped = crate::completion::map_live_offset_to_compiled(live, compiled, live_pos);
1254 (identifier_at(live, live_pos) == identifier_at(compiled, mapped)).then_some(mapped)
1255}
1256
1257pub fn hover_info(
1264 cached_build: &crate::goto::CachedBuild,
1265 file_uri: &Url,
1266 position: Position,
1267 source_bytes: &[u8],
1268 compiled_text: Option<&str>,
1269) -> Option<Hover> {
1270 let nodes = &cached_build.nodes;
1271 let path_to_abs = &cached_build.path_to_abs;
1272 let external_refs = &cached_build.external_refs;
1273 let id_to_path = &cached_build.id_to_path_map;
1274 let doc_index = &cached_build.doc_index;
1275 let hint_index = &cached_build.hint_index;
1276
1277 let file_path = file_uri.to_file_path().ok()?;
1279 let file_path_str = file_path.to_str()?;
1280
1281 let abs_path = path_to_abs
1283 .iter()
1284 .find(|(k, _)| file_path_str.ends_with(k.as_str()))
1285 .map(|(_, v)| v.clone())?;
1286
1287 let live_text = String::from_utf8_lossy(source_bytes);
1288 let live_pos = pos_to_bytes(source_bytes, position);
1289 let live_word = identifier_at(&live_text, live_pos);
1290 let byte_pos = match compiled_text {
1291 Some(ct) => cursor_in_compiled(&live_text, ct, live_pos)?,
1292 None => live_pos,
1293 };
1294
1295 let node_id = byte_to_decl_via_external_refs(external_refs, id_to_path, &abs_path, byte_pos)
1297 .or_else(|| byte_to_id(nodes, &abs_path, byte_pos))?;
1298
1299 let node_info = nodes
1301 .values()
1302 .find_map(|file_nodes| file_nodes.get(&node_id))?;
1303
1304 let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
1306
1307 let typed_decl = cached_build.decl_index.get(&decl_id);
1309
1310 if compiled_text.is_none()
1313 && let (Some(word), Some(decl)) = (live_word, typed_decl)
1314 && !decl.name().is_empty()
1315 && word != decl.name()
1316 && node_info.member_name.as_deref() != Some(word)
1317 {
1318 return None;
1319 }
1320
1321 let mut parts: Vec<String> = Vec::new();
1323
1324 if let Some(sig) = typed_decl.and_then(|d| d.build_signature()) {
1326 parts.push(format!("```solidity\n{sig}\n```"));
1327 } else if let Some(d) = typed_decl {
1328 if let Some(ts) = d.type_string() {
1330 parts.push(format!("```solidity\n{ts} {}\n```", d.name()));
1331 }
1332 }
1333
1334 if let Some(selector) = typed_decl.and_then(|d| d.extract_typed_selector()) {
1336 parts.push(format!("Selector: `{}`", selector.to_prefixed()));
1337 }
1338
1339 if let Some(slot_info) = cached_build.storage_layout.get(&decl_id) {
1341 if slot_info.offset > 0 {
1342 parts.push(format!(
1343 "Storage: slot `{}`, offset `{}`",
1344 slot_info.slot, slot_info.offset
1345 ));
1346 } else {
1347 parts.push(format!("Storage: slot `{}`", slot_info.slot));
1348 }
1349 }
1350
1351 let di = &cached_build.decl_index;
1352 let id_to_path = &cached_build.node_id_to_source_path;
1353
1354 if let Some(doc_entry) =
1356 typed_decl.and_then(|d| lookup_doc_entry_typed(doc_index, d, di, id_to_path))
1357 {
1358 let formatted = format_doc_entry(&doc_entry);
1359 if !formatted.is_empty() {
1360 parts.push(format!("---\n{formatted}"));
1361 }
1362 } else if let Some(doc_text) = typed_decl.and_then(|d| d.extract_doc_text()) {
1363 let inherited_doc = typed_decl.and_then(|d| resolve_inheritdoc_typed(d, &doc_text, di));
1364 let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
1365 if !formatted.is_empty() {
1366 parts.push(format!("---\n{formatted}"));
1367 }
1368 } else if let Some(param_doc) =
1369 typed_decl.and_then(|d| lookup_param_doc_typed(doc_index, d, di, id_to_path))
1370 {
1371 if !param_doc.is_empty() {
1373 parts.push(format!("---\n{param_doc}"));
1374 }
1375 }
1376
1377 if let Some(hint_lookup) = hint_index.get(&abs_path) {
1382 let source_str = String::from_utf8_lossy(source_bytes);
1383 if let Some(tree) = crate::inlay_hints::ts_parse(&source_str)
1384 && let Some(ctx) =
1385 crate::inlay_hints::ts_find_call_at_byte(tree.root_node(), &source_str, byte_pos)
1386 && let Some(resolved) = hint_lookup.resolve_callsite_param(
1387 ctx.call_start_byte,
1388 ctx.name,
1389 ctx.arg_count,
1390 ctx.arg_index,
1391 )
1392 {
1393 let typed_fn = di.get(&resolved.decl_id);
1395 let param_doc = typed_fn.and_then(|fn_decl| {
1396 if let Some(doc_entry) = lookup_doc_entry_typed(doc_index, fn_decl, di, id_to_path)
1398 {
1399 for (pname, pdesc) in &doc_entry.params {
1400 if pname == &resolved.param_name {
1401 return Some(pdesc.clone());
1402 }
1403 }
1404 }
1405 if let Some(doc_text) = fn_decl.extract_doc_text() {
1407 let resolved_doc = if doc_text.contains("@inheritdoc") {
1408 resolve_inheritdoc_typed(fn_decl, &doc_text, di)
1409 } else {
1410 None
1411 };
1412 let text = resolved_doc.as_deref().unwrap_or(&doc_text);
1413 for line in text.lines() {
1414 let trimmed = line.trim().trim_start_matches('*').trim();
1415 if let Some(rest) = trimmed.strip_prefix("@param ")
1416 && let Some((name, desc)) = rest.split_once(' ')
1417 && name == resolved.param_name
1418 {
1419 return Some(desc.to_string());
1420 }
1421 }
1422 }
1423 None
1424 });
1425 if let Some(desc) = param_doc
1426 && !desc.is_empty()
1427 {
1428 parts.push(format!("**@param `{}`** — {desc}", resolved.param_name));
1429 }
1430 }
1431 }
1432
1433 if parts.is_empty() {
1434 return None;
1435 }
1436
1437 Some(Hover {
1438 contents: HoverContents::Markup(MarkupContent {
1439 kind: MarkupKind::Markdown,
1440 value: parts.join("\n\n"),
1441 }),
1442 range: None,
1443 })
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 use super::*;
1449
1450 fn load_test_ast() -> Value {
1451 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
1452 let raw: Value = serde_json::from_str(&data).expect("valid json");
1453 crate::solc::normalize_solc_output(raw, None)
1454 }
1455
1456 #[test]
1457 fn test_find_node_by_id_pool_manager() {
1458 let ast = load_test_ast();
1459 let sources = ast.get("sources").unwrap();
1460 let node = find_node_by_id(sources, NodeId(1216)).unwrap();
1461 assert_eq!(
1462 node.get("name").and_then(|v| v.as_str()),
1463 Some("PoolManager")
1464 );
1465 assert_eq!(
1466 node.get("nodeType").and_then(|v| v.as_str()),
1467 Some("ContractDefinition")
1468 );
1469 }
1470
1471 #[test]
1472 fn test_find_node_by_id_initialize() {
1473 let ast = load_test_ast();
1474 let sources = ast.get("sources").unwrap();
1475 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1477 assert_eq!(
1478 node.get("name").and_then(|v| v.as_str()),
1479 Some("initialize")
1480 );
1481 }
1482
1483 #[test]
1484 fn test_extract_documentation_object() {
1485 let ast = load_test_ast();
1486 let sources = ast.get("sources").unwrap();
1487 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1489 let doc = extract_documentation(node).unwrap();
1490 assert!(doc.contains("@notice"));
1491 assert!(doc.contains("@param key"));
1492 }
1493
1494 #[test]
1495 fn test_extract_documentation_none() {
1496 let ast = load_test_ast();
1497 let sources = ast.get("sources").unwrap();
1498 let node = find_node_by_id(sources, NodeId(6871)).unwrap();
1500 let _ = extract_documentation(node);
1502 }
1503
1504 #[test]
1505 fn test_format_natspec_notice_and_params() {
1506 let text = "@notice Initialize the state for a given pool ID\n @param key The pool key\n @param sqrtPriceX96 The initial square root price\n @return tick The initial tick";
1507 let formatted = format_natspec(text, None);
1508 assert!(formatted.contains("Initialize the state"));
1509 assert!(formatted.contains("**Parameters:**"));
1510 assert!(formatted.contains("`key`"));
1511 assert!(formatted.contains("**Returns:**"));
1512 assert!(formatted.contains("`tick`"));
1513 }
1514
1515 #[test]
1516 fn test_format_natspec_inheritdoc() {
1517 let text = "@inheritdoc IPoolManager";
1518 let formatted = format_natspec(text, None);
1519 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
1520 }
1521
1522 #[test]
1523 fn test_format_natspec_dev() {
1524 let text = "@notice Do something\n @dev This is an implementation detail";
1525 let formatted = format_natspec(text, None);
1526 assert!(formatted.contains("Do something"));
1527 assert!(formatted.contains("**@dev**"));
1528 assert!(formatted.contains("*This is an implementation detail*"));
1529 }
1530
1531 #[test]
1532 fn test_format_natspec_custom_tag() {
1533 let text = "@notice Do something\n @custom:security Non-reentrant";
1534 let formatted = format_natspec(text, None);
1535 assert!(formatted.contains("Do something"));
1536 assert!(formatted.contains("**@custom:security**"));
1537 assert!(formatted.contains("*Non-reentrant*"));
1538 }
1539
1540 #[test]
1541 fn test_build_function_signature_initialize() {
1542 let ast = load_test_ast();
1543 let sources = ast.get("sources").unwrap();
1544 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1545 let sig = build_function_signature(node).unwrap();
1546 assert!(sig.starts_with("function initialize("));
1547 assert!(sig.contains("returns"));
1548 }
1549
1550 #[test]
1551 fn test_build_signature_contract() {
1552 let ast = load_test_ast();
1553 let sources = ast.get("sources").unwrap();
1554 let node = find_node_by_id(sources, NodeId(1216)).unwrap();
1555 let sig = build_function_signature(node).unwrap();
1556 assert!(sig.contains("contract PoolManager"));
1557 assert!(sig.contains(" is "));
1558 }
1559
1560 #[test]
1561 fn test_build_signature_struct() {
1562 let ast = load_test_ast();
1563 let sources = ast.get("sources").unwrap();
1564 let node = find_node_by_id(sources, NodeId(6871)).unwrap();
1565 let sig = build_function_signature(node).unwrap();
1566 assert!(sig.starts_with("struct PoolKey"));
1567 assert!(sig.contains('{'));
1568 }
1569
1570 #[test]
1571 fn test_build_signature_error() {
1572 let ast = load_test_ast();
1573 let sources = ast.get("sources").unwrap();
1574 let node = find_node_by_id(sources, NodeId(1372)).unwrap();
1576 assert_eq!(
1577 node.get("nodeType").and_then(|v| v.as_str()),
1578 Some("ErrorDefinition")
1579 );
1580 let sig = build_function_signature(node).unwrap();
1581 assert!(sig.starts_with("error "));
1582 }
1583
1584 #[test]
1585 fn test_build_signature_event() {
1586 let ast = load_test_ast();
1587 let sources = ast.get("sources").unwrap();
1588 let node = find_node_by_id(sources, NodeId(7404)).unwrap();
1590 assert_eq!(
1591 node.get("nodeType").and_then(|v| v.as_str()),
1592 Some("EventDefinition")
1593 );
1594 let sig = build_function_signature(node).unwrap();
1595 assert!(sig.starts_with("event "));
1596 }
1597
1598 #[test]
1599 fn test_build_signature_variable() {
1600 let ast = load_test_ast();
1601 let sources = ast.get("sources").unwrap();
1602 let pm = find_node_by_id(sources, NodeId(1216)).unwrap();
1605 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
1606 for node in nodes {
1607 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
1608 let sig = build_function_signature(node);
1609 assert!(sig.is_some());
1610 break;
1611 }
1612 }
1613 }
1614 }
1615
1616 #[test]
1617 fn test_pool_manager_has_documentation() {
1618 let ast = load_test_ast();
1619 let sources = ast.get("sources").unwrap();
1620 let node = find_node_by_id(sources, NodeId(7455)).unwrap();
1622 let doc = extract_documentation(node).unwrap();
1623 assert!(doc.contains("@notice"));
1624 }
1625
1626 #[test]
1627 fn test_format_parameters_empty() {
1628 let result = format_parameters(None);
1629 assert_eq!(result, "");
1630 }
1631
1632 #[test]
1633 fn test_format_parameters_with_data() {
1634 let params: Value = serde_json::json!({
1635 "parameters": [
1636 {
1637 "name": "key",
1638 "typeDescriptions": { "typeString": "struct PoolKey" },
1639 "storageLocation": "memory"
1640 },
1641 {
1642 "name": "sqrtPriceX96",
1643 "typeDescriptions": { "typeString": "uint160" },
1644 "storageLocation": "default"
1645 }
1646 ]
1647 });
1648 let result = format_parameters(Some(¶ms));
1649 assert!(result.contains("struct PoolKey memory key"));
1650 assert!(result.contains("uint160 sqrtPriceX96"));
1651 }
1652
1653 #[test]
1656 fn test_extract_selector_function() {
1657 let ast = load_test_ast();
1658 let sources = ast.get("sources").unwrap();
1659 let node = find_node_by_id(sources, NodeId(616)).unwrap();
1661 let selector = extract_selector(node).unwrap();
1662 assert_eq!(selector, Selector::Func(FuncSelector::new("f3cd914c")));
1663 assert_eq!(selector.as_hex(), "f3cd914c");
1664 }
1665
1666 #[test]
1667 fn test_extract_selector_error() {
1668 let ast = load_test_ast();
1669 let sources = ast.get("sources").unwrap();
1670 let node = find_node_by_id(sources, NodeId(1372)).unwrap();
1672 let selector = extract_selector(node).unwrap();
1673 assert_eq!(selector, Selector::Func(FuncSelector::new("0d89438e")));
1674 assert_eq!(selector.as_hex(), "0d89438e");
1675 }
1676
1677 #[test]
1678 fn test_extract_selector_event() {
1679 let ast = load_test_ast();
1680 let sources = ast.get("sources").unwrap();
1681 let node = find_node_by_id(sources, NodeId(7404)).unwrap();
1683 let selector = extract_selector(node).unwrap();
1684 assert!(matches!(selector, Selector::Event(_)));
1685 assert_eq!(selector.as_hex().len(), 64); }
1687
1688 #[test]
1689 fn test_extract_selector_public_variable() {
1690 let ast = load_test_ast();
1691 let sources = ast.get("sources").unwrap();
1692 let node = find_node_by_id(sources, NodeId(7406)).unwrap();
1694 let selector = extract_selector(node).unwrap();
1695 assert_eq!(selector, Selector::Func(FuncSelector::new("8da5cb5b")));
1696 }
1697
1698 #[test]
1699 fn test_extract_selector_internal_function_none() {
1700 let ast = load_test_ast();
1701 let sources = ast.get("sources").unwrap();
1702 let node = find_node_by_id(sources, NodeId(5021)).unwrap();
1704 assert!(extract_selector(node).is_none());
1705 }
1706
1707 #[test]
1710 fn test_resolve_inheritdoc_swap() {
1711 let ast = load_test_ast();
1712 let build = crate::goto::CachedBuild::new(ast, 0, None);
1713 let decl = build.decl_index.get(&NodeId(616)).unwrap();
1715 let doc_text = decl.extract_doc_text().unwrap();
1716 assert!(doc_text.contains("@inheritdoc"));
1717
1718 let resolved = resolve_inheritdoc_typed(decl, &doc_text, &build.decl_index).unwrap();
1719 assert!(resolved.contains("@notice"));
1720 assert!(resolved.contains("Swap against the given pool"));
1721 }
1722
1723 #[test]
1724 fn test_resolve_inheritdoc_initialize() {
1725 let ast = load_test_ast();
1726 let build = crate::goto::CachedBuild::new(ast, 0, None);
1727 let decl = build.decl_index.get(&NodeId(330)).unwrap();
1729 let doc_text = decl.extract_doc_text().unwrap();
1730
1731 let resolved = resolve_inheritdoc_typed(decl, &doc_text, &build.decl_index).unwrap();
1732 assert!(resolved.contains("Initialize the state"));
1733 assert!(resolved.contains("@param key"));
1734 }
1735
1736 #[test]
1737 fn test_resolve_inheritdoc_extsload_overload() {
1738 let ast = load_test_ast();
1739 let build = crate::goto::CachedBuild::new(ast, 0, None);
1740
1741 let decl = build.decl_index.get(&NodeId(1306)).unwrap();
1743 let doc_text = decl.extract_doc_text().unwrap();
1744 let resolved = resolve_inheritdoc_typed(decl, &doc_text, &build.decl_index).unwrap();
1745 assert!(resolved.contains("granular pool state"));
1746 assert!(resolved.contains("@param slot"));
1747
1748 let decl2 = build.decl_index.get(&NodeId(1319)).unwrap();
1750 let doc_text2 = decl2.extract_doc_text().unwrap();
1751 let resolved2 = resolve_inheritdoc_typed(decl2, &doc_text2, &build.decl_index).unwrap();
1752 assert!(resolved2.contains("@param startSlot"));
1753
1754 let decl3 = build.decl_index.get(&NodeId(1331)).unwrap();
1756 let doc_text3 = decl3.extract_doc_text().unwrap();
1757 let resolved3 = resolve_inheritdoc_typed(decl3, &doc_text3, &build.decl_index).unwrap();
1758 assert!(resolved3.contains("sparse pool state"));
1759 }
1760
1761 #[test]
1762 fn test_resolve_inheritdoc_formats_in_hover() {
1763 let ast = load_test_ast();
1764 let build = crate::goto::CachedBuild::new(ast, 0, None);
1765 let decl = build.decl_index.get(&NodeId(616)).unwrap();
1767 let doc_text = decl.extract_doc_text().unwrap();
1768 let inherited = resolve_inheritdoc_typed(decl, &doc_text, &build.decl_index);
1769 let formatted = format_natspec(&doc_text, inherited.as_deref());
1770 assert!(!formatted.contains("@inheritdoc"));
1771 assert!(formatted.contains("Swap against the given pool"));
1772 assert!(formatted.contains("**Parameters:**"));
1773 }
1774
1775 #[test]
1778 fn test_param_doc_error_parameter() {
1779 let ast = load_test_ast();
1780 let build = crate::goto::CachedBuild::new(ast, 0, None);
1781
1782 let decl = build.decl_index.get(&NodeId(3821)).unwrap();
1784 assert_eq!(decl.name(), "sqrtPriceCurrentX96");
1785
1786 let doc = lookup_param_doc_typed(
1787 &build.doc_index,
1788 decl,
1789 &build.decl_index,
1790 &build.node_id_to_source_path,
1791 )
1792 .unwrap();
1793 assert!(
1794 doc.contains("invalid"),
1795 "should describe the invalid price: {doc}"
1796 );
1797 }
1798
1799 #[test]
1800 fn test_param_doc_error_second_parameter() {
1801 let ast = load_test_ast();
1802 let build = crate::goto::CachedBuild::new(ast, 0, None);
1803
1804 let decl = build.decl_index.get(&NodeId(3823)).unwrap();
1806 let doc = lookup_param_doc_typed(
1807 &build.doc_index,
1808 decl,
1809 &build.decl_index,
1810 &build.node_id_to_source_path,
1811 )
1812 .unwrap();
1813 assert!(
1814 doc.contains("surpassed price limit"),
1815 "should describe the surpassed limit: {doc}"
1816 );
1817 }
1818
1819 #[test]
1820 fn test_param_doc_function_return_value() {
1821 let ast = load_test_ast();
1822 let build = crate::goto::CachedBuild::new(ast, 0, None);
1823
1824 let decl = build.decl_index.get(&NodeId(4055)).unwrap();
1826 assert_eq!(decl.name(), "delta");
1827
1828 let doc = lookup_param_doc_typed(
1829 &build.doc_index,
1830 decl,
1831 &build.decl_index,
1832 &build.node_id_to_source_path,
1833 )
1834 .unwrap();
1835 assert!(
1836 doc.contains("deltas of the token balances"),
1837 "should have return doc: {doc}"
1838 );
1839 }
1840
1841 #[test]
1842 fn test_param_doc_function_input_parameter() {
1843 let ast = load_test_ast();
1844 let build = crate::goto::CachedBuild::new(ast, 0, None);
1845
1846 let params_decl = build
1849 .decl_index
1850 .values()
1851 .find(|d| d.name() == "params" && d.scope() == Some(4371))
1852 .unwrap();
1853
1854 let doc = lookup_param_doc_typed(
1855 &build.doc_index,
1856 params_decl,
1857 &build.decl_index,
1858 &build.node_id_to_source_path,
1859 )
1860 .unwrap();
1861 assert!(
1862 doc.contains("position details"),
1863 "should have param doc: {doc}"
1864 );
1865 }
1866
1867 #[test]
1868 fn test_param_doc_inherited_function_via_docindex() {
1869 let ast = load_test_ast();
1870 let build = crate::goto::CachedBuild::new(ast, 0, None);
1871
1872 let decl = build.decl_index.get(&NodeId(478)).unwrap();
1874 assert_eq!(decl.name(), "key");
1875
1876 let doc = lookup_param_doc_typed(
1877 &build.doc_index,
1878 decl,
1879 &build.decl_index,
1880 &build.node_id_to_source_path,
1881 )
1882 .unwrap();
1883 assert!(
1884 doc.contains("pool to swap"),
1885 "should have inherited param doc: {doc}"
1886 );
1887 }
1888
1889 #[test]
1890 fn test_param_doc_non_parameter_returns_none() {
1891 let ast = load_test_ast();
1892 let build = crate::goto::CachedBuild::new(ast, 0, None);
1893
1894 let decl = build.decl_index.get(&NodeId(1216)).unwrap();
1896 assert!(
1897 lookup_param_doc_typed(
1898 &build.doc_index,
1899 decl,
1900 &build.decl_index,
1901 &build.node_id_to_source_path,
1902 )
1903 .is_none()
1904 );
1905 }
1906
1907 fn load_solc_fixture() -> Value {
1910 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
1911 let raw: Value = serde_json::from_str(&data).expect("valid json");
1912 crate::solc::normalize_solc_output(raw, None)
1913 }
1914
1915 #[test]
1916 fn test_doc_index_is_not_empty() {
1917 let ast = load_solc_fixture();
1918 let index = build_doc_index(&ast);
1919 assert!(!index.is_empty(), "DocIndex should contain entries");
1920 }
1921
1922 #[test]
1923 fn test_doc_index_has_contract_entries() {
1924 let ast = load_solc_fixture();
1925 let index = build_doc_index(&ast);
1926
1927 let pm_keys: Vec<_> = index
1929 .keys()
1930 .filter(|k| matches!(k, DocKey::Contract(s) if s.contains("PoolManager")))
1931 .collect();
1932 assert!(
1933 !pm_keys.is_empty(),
1934 "should have a Contract entry for PoolManager"
1935 );
1936
1937 let pm_key = DocKey::Contract(
1938 "/Users/meek/developer/asyncswap/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
1939 );
1940 let entry = index.get(&pm_key).expect("PoolManager contract entry");
1941 assert_eq!(entry.title.as_deref(), Some("PoolManager"));
1942 assert_eq!(
1943 entry.notice.as_deref(),
1944 Some("Holds the state for all pools")
1945 );
1946 }
1947
1948 #[test]
1949 fn test_doc_index_has_function_by_selector() {
1950 let ast = load_solc_fixture();
1951 let index = build_doc_index(&ast);
1952
1953 let init_key = DocKey::Func(FuncSelector::new("6276cbbe"));
1955 let entry = index
1956 .get(&init_key)
1957 .expect("should have initialize by selector");
1958 assert_eq!(
1959 entry.notice.as_deref(),
1960 Some("Initialize the state for a given pool ID")
1961 );
1962 assert!(
1963 entry
1964 .details
1965 .as_deref()
1966 .unwrap_or("")
1967 .contains("MAX_SWAP_FEE"),
1968 "devdoc details should mention MAX_SWAP_FEE"
1969 );
1970 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
1972 assert!(param_names.contains(&"key"), "should have param 'key'");
1973 assert!(
1974 param_names.contains(&"sqrtPriceX96"),
1975 "should have param 'sqrtPriceX96'"
1976 );
1977 let return_names: Vec<&str> = entry.returns.iter().map(|(n, _)| n.as_str()).collect();
1979 assert!(return_names.contains(&"tick"), "should have return 'tick'");
1980 }
1981
1982 #[test]
1983 fn test_doc_index_swap_by_selector() {
1984 let ast = load_solc_fixture();
1985 let index = build_doc_index(&ast);
1986
1987 let swap_key = DocKey::Func(FuncSelector::new("f3cd914c"));
1989 let entry = index.get(&swap_key).expect("should have swap by selector");
1990 assert!(
1991 entry
1992 .notice
1993 .as_deref()
1994 .unwrap_or("")
1995 .contains("Swap against the given pool"),
1996 "swap notice should describe swapping"
1997 );
1998 assert!(
2000 !entry.params.is_empty(),
2001 "swap should have param documentation"
2002 );
2003 }
2004
2005 #[test]
2006 fn test_doc_index_settle_by_selector() {
2007 let ast = load_solc_fixture();
2008 let index = build_doc_index(&ast);
2009
2010 let key = DocKey::Func(FuncSelector::new("11da60b4"));
2012 let entry = index.get(&key).expect("should have settle by selector");
2013 assert!(
2014 entry.notice.is_some(),
2015 "settle should have a notice from userdoc"
2016 );
2017 }
2018
2019 #[test]
2020 fn test_doc_index_has_error_entries() {
2021 let ast = load_solc_fixture();
2022 let index = build_doc_index(&ast);
2023
2024 let selector = compute_selector("AlreadyUnlocked()");
2026 let key = DocKey::Func(FuncSelector::new(&selector));
2027 let entry = index.get(&key).expect("should have AlreadyUnlocked error");
2028 assert!(
2029 entry
2030 .notice
2031 .as_deref()
2032 .unwrap_or("")
2033 .contains("already unlocked"),
2034 "AlreadyUnlocked notice: {:?}",
2035 entry.notice
2036 );
2037 }
2038
2039 #[test]
2040 fn test_doc_index_error_with_params() {
2041 let ast = load_solc_fixture();
2042 let index = build_doc_index(&ast);
2043
2044 let selector = compute_selector("CurrenciesOutOfOrderOrEqual(address,address)");
2046 let key = DocKey::Func(FuncSelector::new(&selector));
2047 let entry = index
2048 .get(&key)
2049 .expect("should have CurrenciesOutOfOrderOrEqual error");
2050 assert!(entry.notice.is_some(), "error should have notice");
2051 }
2052
2053 #[test]
2054 fn test_doc_index_has_event_entries() {
2055 let ast = load_solc_fixture();
2056 let index = build_doc_index(&ast);
2057
2058 let event_count = index
2060 .keys()
2061 .filter(|k| matches!(k, DocKey::Event(_)))
2062 .count();
2063 assert!(event_count > 0, "should have event entries in the DocIndex");
2064 }
2065
2066 #[test]
2067 fn test_doc_index_swap_event() {
2068 let ast = load_solc_fixture();
2069 let index = build_doc_index(&ast);
2070
2071 let topic =
2073 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2074 let key = DocKey::Event(EventSelector::new(&topic));
2075 let entry = index.get(&key).expect("should have Swap event");
2076
2077 assert!(
2079 entry
2080 .notice
2081 .as_deref()
2082 .unwrap_or("")
2083 .contains("swaps between currency0 and currency1"),
2084 "Swap event notice: {:?}",
2085 entry.notice
2086 );
2087
2088 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
2090 assert!(
2091 param_names.contains(&"amount0"),
2092 "should have param 'amount0'"
2093 );
2094 assert!(
2095 param_names.contains(&"sender"),
2096 "should have param 'sender'"
2097 );
2098 assert!(param_names.contains(&"id"), "should have param 'id'");
2099 }
2100
2101 #[test]
2102 fn test_doc_index_initialize_event() {
2103 let ast = load_solc_fixture();
2104 let index = build_doc_index(&ast);
2105
2106 let topic = compute_event_topic(
2107 "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)",
2108 );
2109 let key = DocKey::Event(EventSelector::new(&topic));
2110 let entry = index.get(&key).expect("should have Initialize event");
2111 assert!(
2112 !entry.params.is_empty(),
2113 "Initialize event should have param docs"
2114 );
2115 }
2116
2117 #[test]
2118 fn test_doc_index_no_state_variables_for_pool_manager() {
2119 let ast = load_solc_fixture();
2120 let index = build_doc_index(&ast);
2121
2122 let sv_count = index
2124 .keys()
2125 .filter(|k| matches!(k, DocKey::StateVar(s) if s.contains("PoolManager")))
2126 .count();
2127 assert_eq!(
2128 sv_count, 0,
2129 "PoolManager should have no state variable doc entries"
2130 );
2131 }
2132
2133 #[test]
2134 fn test_doc_index_multiple_contracts() {
2135 let ast = load_solc_fixture();
2136 let index = build_doc_index(&ast);
2137
2138 let contract_count = index
2140 .keys()
2141 .filter(|k| matches!(k, DocKey::Contract(_)))
2142 .count();
2143 assert!(
2144 contract_count >= 5,
2145 "should have at least 5 contract-level entries, got {contract_count}"
2146 );
2147 }
2148
2149 #[test]
2150 fn test_doc_index_func_key_count() {
2151 let ast = load_solc_fixture();
2152 let index = build_doc_index(&ast);
2153
2154 let func_count = index
2155 .keys()
2156 .filter(|k| matches!(k, DocKey::Func(_)))
2157 .count();
2158 assert!(
2160 func_count >= 30,
2161 "should have at least 30 Func entries (methods + errors), got {func_count}"
2162 );
2163 }
2164
2165 #[test]
2166 fn test_doc_index_format_initialize_entry() {
2167 let ast = load_solc_fixture();
2168 let index = build_doc_index(&ast);
2169
2170 let key = DocKey::Func(FuncSelector::new("6276cbbe"));
2171 let entry = index.get(&key).expect("initialize entry");
2172 let formatted = format_doc_entry(entry);
2173
2174 assert!(
2175 formatted.contains("Initialize the state for a given pool ID"),
2176 "formatted should include notice"
2177 );
2178 assert!(
2179 formatted.contains("**@dev**"),
2180 "formatted should include dev section"
2181 );
2182 assert!(
2183 formatted.contains("**Parameters:**"),
2184 "formatted should include parameters"
2185 );
2186 assert!(
2187 formatted.contains("`key`"),
2188 "formatted should include key param"
2189 );
2190 assert!(
2191 formatted.contains("**Returns:**"),
2192 "formatted should include returns"
2193 );
2194 assert!(
2195 formatted.contains("`tick`"),
2196 "formatted should include tick return"
2197 );
2198 }
2199
2200 #[test]
2201 fn test_doc_index_format_contract_entry() {
2202 let ast = load_solc_fixture();
2203 let index = build_doc_index(&ast);
2204
2205 let key = DocKey::Contract(
2206 "/Users/meek/developer/asyncswap/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2207 );
2208 let entry = index.get(&key).expect("PoolManager contract entry");
2209 let formatted = format_doc_entry(entry);
2210
2211 assert!(
2212 formatted.contains("**PoolManager**"),
2213 "should include bold title"
2214 );
2215 assert!(
2216 formatted.contains("Holds the state for all pools"),
2217 "should include notice"
2218 );
2219 }
2220
2221 #[test]
2222 fn test_doc_index_inherited_docs_resolved() {
2223 let ast = load_solc_fixture();
2224 let index = build_doc_index(&ast);
2225
2226 let key = DocKey::Func(FuncSelector::new("f3cd914c"));
2230 let entry = index.get(&key).expect("swap entry");
2231 let notice = entry.notice.as_deref().unwrap_or("");
2233 assert!(
2234 !notice.contains("@inheritdoc"),
2235 "userdoc/devdoc should have resolved inherited docs, not raw @inheritdoc"
2236 );
2237 }
2238
2239 #[test]
2240 fn test_compute_selector_known_values() {
2241 let sel = compute_selector("AlreadyUnlocked()");
2243 assert_eq!(sel.len(), 8, "selector should be 8 hex chars");
2244
2245 let init_sel =
2247 compute_selector("initialize((address,address,uint24,int24,address),uint160)");
2248 assert_eq!(
2249 init_sel, "6276cbbe",
2250 "computed initialize selector should match evm.methodIdentifiers"
2251 );
2252 }
2253
2254 #[test]
2255 fn test_compute_event_topic_length() {
2256 let topic =
2257 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2258 assert_eq!(
2259 topic.len(),
2260 64,
2261 "event topic should be 64 hex chars (32 bytes)"
2262 );
2263 }
2264
2265 #[test]
2266 fn test_doc_index_error_count_poolmanager() {
2267 let ast = load_solc_fixture();
2268 let index = build_doc_index(&ast);
2269
2270 let error_sigs = [
2273 "AlreadyUnlocked()",
2274 "CurrenciesOutOfOrderOrEqual(address,address)",
2275 "CurrencyNotSettled()",
2276 "InvalidCaller()",
2277 "ManagerLocked()",
2278 "MustClearExactPositiveDelta()",
2279 "NonzeroNativeValue()",
2280 "PoolNotInitialized()",
2281 "ProtocolFeeCurrencySynced()",
2282 "ProtocolFeeTooLarge(uint24)",
2283 "SwapAmountCannotBeZero()",
2284 "TickSpacingTooLarge(int24)",
2285 "TickSpacingTooSmall(int24)",
2286 "UnauthorizedDynamicLPFeeUpdate()",
2287 ];
2288 let mut found = 0;
2289 for sig in &error_sigs {
2290 let selector = compute_selector(sig);
2291 let key = DocKey::Func(FuncSelector::new(&selector));
2292 if index.contains_key(&key) {
2293 found += 1;
2294 }
2295 }
2296 assert_eq!(
2297 found,
2298 error_sigs.len(),
2299 "all 14 PoolManager errors should be in the DocIndex"
2300 );
2301 }
2302
2303 #[test]
2304 fn test_doc_index_extsload_overloads_have_different_selectors() {
2305 let ast = load_solc_fixture();
2306 let index = build_doc_index(&ast);
2307
2308 let sel1 = DocKey::Func(FuncSelector::new("1e2eaeaf"));
2313 let sel2 = DocKey::Func(FuncSelector::new("35fd631a"));
2314 let sel3 = DocKey::Func(FuncSelector::new("dbd035ff"));
2315
2316 assert!(index.contains_key(&sel1), "extsload(bytes32) should exist");
2317 assert!(
2318 index.contains_key(&sel2),
2319 "extsload(bytes32,uint256) should exist"
2320 );
2321 assert!(
2322 index.contains_key(&sel3),
2323 "extsload(bytes32[]) should exist"
2324 );
2325 }
2326
2327 #[test]
2328 fn test_signature_help_parameter_offsets() {
2329 let label = "function addTax(uint256 amount, uint16 tax, uint16 base)";
2331 let param_strings = vec![
2332 "uint256 amount".to_string(),
2333 "uint16 tax".to_string(),
2334 "uint16 base".to_string(),
2335 ];
2336
2337 let params_start = label.find('(').unwrap() + 1;
2338 let mut offsets = Vec::new();
2339 let mut offset = params_start;
2340 for param_str in ¶m_strings {
2341 let start = offset;
2342 let end = start + param_str.len();
2343 offsets.push((start, end));
2344 offset = end + 2; }
2346
2347 assert_eq!(&label[offsets[0].0..offsets[0].1], "uint256 amount");
2349 assert_eq!(&label[offsets[1].0..offsets[1].1], "uint16 tax");
2350 assert_eq!(&label[offsets[2].0..offsets[2].1], "uint16 base");
2351 }
2352
2353 #[test]
2356 fn find_mapping_decl_typed_pools() {
2357 let ast = load_test_ast();
2358 let build = crate::goto::CachedBuild::new(ast, 0, None);
2359 let decl = find_mapping_decl_typed(&build.decl_index, "_pools").unwrap();
2360 assert_eq!(decl.name, "_pools");
2361 assert!(matches!(
2362 decl.type_name.as_ref(),
2363 Some(crate::solc_ast::TypeName::Mapping(_))
2364 ));
2365 }
2366
2367 #[test]
2368 fn find_mapping_decl_typed_not_found() {
2369 let ast = load_test_ast();
2370 let build = crate::goto::CachedBuild::new(ast, 0, None);
2371 assert!(find_mapping_decl_typed(&build.decl_index, "nonexistent").is_none());
2372 }
2373
2374 #[test]
2375 fn mapping_signature_help_typed_pools() {
2376 let ast = load_test_ast();
2377 let build = crate::goto::CachedBuild::new(ast, 0, None);
2378 let help = mapping_signature_help_typed(&build.decl_index, "_pools").unwrap();
2379 assert!(help.signatures[0].label.contains("_pools"));
2380 let params = help.signatures[0].parameters.as_ref().unwrap();
2381 assert!(!params.is_empty());
2382 }
2383
2384 #[test]
2385 fn mapping_signature_help_typed_protocol_fees() {
2386 let ast = load_test_ast();
2387 let build = crate::goto::CachedBuild::new(ast, 0, None);
2388 let help = mapping_signature_help_typed(&build.decl_index, "protocolFeesAccrued").unwrap();
2389 assert!(help.signatures[0].label.contains("protocolFeesAccrued"));
2390 }
2391
2392 #[test]
2393 fn mapping_signature_help_typed_non_mapping() {
2394 let ast = load_test_ast();
2395 let build = crate::goto::CachedBuild::new(ast, 0, None);
2396 assert!(mapping_signature_help_typed(&build.decl_index, "owner").is_none());
2397 }
2398
2399 #[test]
2400 fn identifier_at_finds_word_around_cursor() {
2401 let text = "token.paymentToken();";
2402 assert_eq!(super::identifier_at(text, 0), Some("token"));
2403 assert_eq!(super::identifier_at(text, 5), Some("token")); assert_eq!(super::identifier_at(text, 10), Some("paymentToken"));
2405 assert_eq!(super::identifier_at("a ( b", 2), None);
2406 }
2407
2408 #[test]
2409 fn cursor_in_compiled_maps_through_edits_above() {
2410 let compiled = "uint a;\nIERC20 paymentToken;\n";
2411 let live = "uint a;\nuint extra;\nIERC20 paymentToken;\n";
2412 let live_pos = live.find("paymentToken").unwrap() + 3;
2413 let mapped = super::cursor_in_compiled(live, compiled, live_pos).unwrap();
2414 assert_eq!(super::identifier_at(compiled, mapped), Some("paymentToken"));
2415 }
2416
2417 #[test]
2418 fn cursor_in_compiled_rejects_stale_identifier() {
2419 let compiled = "IERC20 oldToken;\n";
2422 let live = "IERC20 paymentToken;\n";
2423 let live_pos = live.find("paymentToken").unwrap() + 3;
2424 assert_eq!(super::cursor_in_compiled(live, compiled, live_pos), None);
2425 }
2426}