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