1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Url};
4
5use crate::gas::{self, GasIndex};
6use crate::goto::{CHILD_KEYS, cache_ids, pos_to_bytes};
7use crate::inlay_hints::HintIndex;
8use crate::references::{byte_to_decl_via_external_refs, byte_to_id};
9use crate::types::{EventSelector, FuncSelector, MethodId, NodeId, Selector};
10
11#[derive(Debug, Clone, Default)]
15pub struct DocEntry {
16 pub notice: Option<String>,
18 pub details: Option<String>,
20 pub params: Vec<(String, String)>,
22 pub returns: Vec<(String, String)>,
24 pub title: Option<String>,
26 pub author: Option<String>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum DocKey {
33 Func(FuncSelector),
35 Event(EventSelector),
37 Contract(String),
39 StateVar(String),
41 Method(String),
43}
44
45pub type DocIndex = HashMap<DocKey, DocEntry>;
49
50pub fn build_doc_index(ast_data: &Value) -> DocIndex {
55 let mut index = DocIndex::new();
56
57 let contracts = match ast_data.get("contracts").and_then(|c| c.as_object()) {
58 Some(c) => c,
59 None => return index,
60 };
61
62 for (path, names) in contracts {
63 let names_obj = match names.as_object() {
64 Some(n) => n,
65 None => continue,
66 };
67
68 for (name, contract) in names_obj {
69 let userdoc = contract.get("userdoc");
70 let devdoc = contract.get("devdoc");
71 let method_ids = contract
72 .get("evm")
73 .and_then(|e| e.get("methodIdentifiers"))
74 .and_then(|m| m.as_object());
75
76 let sig_to_selector: HashMap<&str, &str> = method_ids
78 .map(|mi| {
79 mi.iter()
80 .filter_map(|(sig, sel)| sel.as_str().map(|s| (sig.as_str(), s)))
81 .collect()
82 })
83 .unwrap_or_default();
84
85 let mut contract_entry = DocEntry::default();
87 if let Some(ud) = userdoc {
88 contract_entry.notice = ud
89 .get("notice")
90 .and_then(|v| v.as_str())
91 .map(|s| s.to_string());
92 }
93 if let Some(dd) = devdoc {
94 contract_entry.title = dd
95 .get("title")
96 .and_then(|v| v.as_str())
97 .map(|s| s.to_string());
98 contract_entry.details = dd
99 .get("details")
100 .and_then(|v| v.as_str())
101 .map(|s| s.to_string());
102 contract_entry.author = dd
103 .get("author")
104 .and_then(|v| v.as_str())
105 .map(|s| s.to_string());
106 }
107 if contract_entry.notice.is_some()
108 || contract_entry.title.is_some()
109 || contract_entry.details.is_some()
110 {
111 let key = DocKey::Contract(format!("{path}:{name}"));
112 index.insert(key, contract_entry);
113 }
114
115 let ud_methods = userdoc
117 .and_then(|u| u.get("methods"))
118 .and_then(|m| m.as_object());
119 let dd_methods = devdoc
120 .and_then(|d| d.get("methods"))
121 .and_then(|m| m.as_object());
122
123 let mut all_sigs: Vec<&str> = Vec::new();
125 if let Some(um) = ud_methods {
126 all_sigs.extend(um.keys().map(|k| k.as_str()));
127 }
128 if let Some(dm) = dd_methods {
129 for k in dm.keys() {
130 if !all_sigs.contains(&k.as_str()) {
131 all_sigs.push(k.as_str());
132 }
133 }
134 }
135
136 for sig in &all_sigs {
137 let mut entry = DocEntry::default();
138
139 if let Some(um) = ud_methods
141 && let Some(method) = um.get(*sig)
142 {
143 entry.notice = method
144 .get("notice")
145 .and_then(|v| v.as_str())
146 .map(|s| s.to_string());
147 }
148
149 if let Some(dm) = dd_methods
151 && let Some(method) = dm.get(*sig)
152 {
153 entry.details = method
154 .get("details")
155 .and_then(|v| v.as_str())
156 .map(|s| s.to_string());
157
158 if let Some(params) = method.get("params").and_then(|p| p.as_object()) {
159 for (pname, pdesc) in params {
160 if let Some(desc) = pdesc.as_str() {
161 entry.params.push((pname.clone(), desc.to_string()));
162 }
163 }
164 }
165
166 if let Some(returns) = method.get("returns").and_then(|r| r.as_object()) {
167 for (rname, rdesc) in returns {
168 if let Some(desc) = rdesc.as_str() {
169 entry.returns.push((rname.clone(), desc.to_string()));
170 }
171 }
172 }
173 }
174
175 if entry.notice.is_none()
176 && entry.details.is_none()
177 && entry.params.is_empty()
178 && entry.returns.is_empty()
179 {
180 continue;
181 }
182
183 if let Some(selector) = sig_to_selector.get(sig) {
185 let key = DocKey::Func(FuncSelector::new(*selector));
186 index.insert(key, entry);
187 } else {
188 let fn_name = sig.split('(').next().unwrap_or(sig);
191 let key = DocKey::Method(format!("{path}:{name}:{fn_name}"));
192 index.insert(key, entry);
193 }
194 }
195
196 let ud_errors = userdoc
198 .and_then(|u| u.get("errors"))
199 .and_then(|e| e.as_object());
200 let dd_errors = devdoc
201 .and_then(|d| d.get("errors"))
202 .and_then(|e| e.as_object());
203
204 let mut all_error_sigs: Vec<&str> = Vec::new();
205 if let Some(ue) = ud_errors {
206 all_error_sigs.extend(ue.keys().map(|k| k.as_str()));
207 }
208 if let Some(de) = dd_errors {
209 for k in de.keys() {
210 if !all_error_sigs.contains(&k.as_str()) {
211 all_error_sigs.push(k.as_str());
212 }
213 }
214 }
215
216 for sig in &all_error_sigs {
217 let mut entry = DocEntry::default();
218
219 if let Some(ue) = ud_errors
221 && let Some(arr) = ue.get(*sig).and_then(|v| v.as_array())
222 && let Some(first) = arr.first()
223 {
224 entry.notice = first
225 .get("notice")
226 .and_then(|v| v.as_str())
227 .map(|s| s.to_string());
228 }
229
230 if let Some(de) = dd_errors
232 && let Some(arr) = de.get(*sig).and_then(|v| v.as_array())
233 && let Some(first) = arr.first()
234 {
235 entry.details = first
236 .get("details")
237 .and_then(|v| v.as_str())
238 .map(|s| s.to_string());
239 if let Some(params) = first.get("params").and_then(|p| p.as_object()) {
240 for (pname, pdesc) in params {
241 if let Some(desc) = pdesc.as_str() {
242 entry.params.push((pname.clone(), desc.to_string()));
243 }
244 }
245 }
246 }
247
248 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
249 continue;
250 }
251
252 let selector = FuncSelector::new(compute_selector(sig));
255 index.insert(DocKey::Func(selector), entry);
256 }
257
258 let ud_events = userdoc
260 .and_then(|u| u.get("events"))
261 .and_then(|e| e.as_object());
262 let dd_events = devdoc
263 .and_then(|d| d.get("events"))
264 .and_then(|e| e.as_object());
265
266 let mut all_event_sigs: Vec<&str> = Vec::new();
267 if let Some(ue) = ud_events {
268 all_event_sigs.extend(ue.keys().map(|k| k.as_str()));
269 }
270 if let Some(de) = dd_events {
271 for k in de.keys() {
272 if !all_event_sigs.contains(&k.as_str()) {
273 all_event_sigs.push(k.as_str());
274 }
275 }
276 }
277
278 for sig in &all_event_sigs {
279 let mut entry = DocEntry::default();
280
281 if let Some(ue) = ud_events
282 && let Some(ev) = ue.get(*sig)
283 {
284 entry.notice = ev
285 .get("notice")
286 .and_then(|v| v.as_str())
287 .map(|s| s.to_string());
288 }
289
290 if let Some(de) = dd_events
291 && let Some(ev) = de.get(*sig)
292 {
293 entry.details = ev
294 .get("details")
295 .and_then(|v| v.as_str())
296 .map(|s| s.to_string());
297 if let Some(params) = ev.get("params").and_then(|p| p.as_object()) {
298 for (pname, pdesc) in params {
299 if let Some(desc) = pdesc.as_str() {
300 entry.params.push((pname.clone(), desc.to_string()));
301 }
302 }
303 }
304 }
305
306 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
307 continue;
308 }
309
310 let topic = EventSelector::new(compute_event_topic(sig));
312 index.insert(DocKey::Event(topic), entry);
313 }
314
315 if let Some(dd) = devdoc
317 && let Some(state_vars) = dd.get("stateVariables").and_then(|s| s.as_object())
318 {
319 for (var_name, var_doc) in state_vars {
320 let mut entry = DocEntry::default();
321 entry.details = var_doc
322 .get("details")
323 .and_then(|v| v.as_str())
324 .map(|s| s.to_string());
325
326 if let Some(returns) = var_doc.get("return").and_then(|v| v.as_str()) {
327 entry.returns.push(("_0".to_string(), returns.to_string()));
328 }
329 if let Some(returns) = var_doc.get("returns").and_then(|r| r.as_object()) {
330 for (rname, rdesc) in returns {
331 if let Some(desc) = rdesc.as_str() {
332 entry.returns.push((rname.clone(), desc.to_string()));
333 }
334 }
335 }
336
337 if entry.details.is_some() || !entry.returns.is_empty() {
338 let key = DocKey::StateVar(format!("{path}:{name}:{var_name}"));
339 index.insert(key, entry);
340 }
341 }
342 }
343 }
344 }
345
346 index
347}
348
349fn compute_selector(sig: &str) -> String {
353 use tiny_keccak::{Hasher, Keccak};
354 let mut hasher = Keccak::v256();
355 hasher.update(sig.as_bytes());
356 let mut output = [0u8; 32];
357 hasher.finalize(&mut output);
358 hex::encode(&output[..4])
359}
360
361fn compute_event_topic(sig: &str) -> String {
365 use tiny_keccak::{Hasher, Keccak};
366 let mut hasher = Keccak::v256();
367 hasher.update(sig.as_bytes());
368 let mut output = [0u8; 32];
369 hasher.finalize(&mut output);
370 hex::encode(output)
371}
372
373pub fn lookup_doc_entry(
377 doc_index: &DocIndex,
378 decl_node: &Value,
379 sources: &Value,
380) -> Option<DocEntry> {
381 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
382
383 match node_type {
384 "FunctionDefinition" | "VariableDeclaration" => {
385 if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str()) {
387 let key = DocKey::Func(FuncSelector::new(selector));
388 if let Some(entry) = doc_index.get(&key) {
389 return Some(entry.clone());
390 }
391 }
392
393 if node_type == "VariableDeclaration" {
395 let var_name = decl_node.get("name").and_then(|v| v.as_str())?;
396 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
398 let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
399 let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
400
401 let path = find_source_path_for_node(sources, scope_id)?;
403 let key = DocKey::StateVar(format!("{path}:{contract_name}:{var_name}"));
404 if let Some(entry) = doc_index.get(&key) {
405 return Some(entry.clone());
406 }
407 }
408
409 let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
411 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
412 let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
413 let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
414 let path = find_source_path_for_node(sources, scope_id)?;
415 let key = DocKey::Method(format!("{path}:{contract_name}:{fn_name}"));
416 doc_index.get(&key).cloned()
417 }
418 "ErrorDefinition" => {
419 if let Some(selector) = decl_node.get("errorSelector").and_then(|v| v.as_str()) {
420 let key = DocKey::Func(FuncSelector::new(selector));
421 return doc_index.get(&key).cloned();
422 }
423 None
424 }
425 "EventDefinition" => {
426 if let Some(selector) = decl_node.get("eventSelector").and_then(|v| v.as_str()) {
427 let key = DocKey::Event(EventSelector::new(selector));
428 return doc_index.get(&key).cloned();
429 }
430 None
431 }
432 "ContractDefinition" => {
433 let contract_name = decl_node.get("name").and_then(|v| v.as_str())?;
434 let node_id = decl_node.get("id").and_then(|v| v.as_u64())?;
436 let path = find_source_path_for_node(sources, node_id)?;
437 let key = DocKey::Contract(format!("{path}:{contract_name}"));
438 doc_index.get(&key).cloned()
439 }
440 _ => None,
441 }
442}
443
444pub fn lookup_param_doc(
453 doc_index: &DocIndex,
454 decl_node: &Value,
455 sources: &Value,
456) -> Option<String> {
457 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
458 if node_type != "VariableDeclaration" {
459 return None;
460 }
461
462 let param_name = decl_node.get("name").and_then(|v| v.as_str())?;
463 if param_name.is_empty() {
464 return None;
465 }
466
467 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
469 let parent_node = find_node_by_id(sources, NodeId(scope_id))?;
470 let parent_type = parent_node.get("nodeType").and_then(|v| v.as_str())?;
471
472 if !matches!(
474 parent_type,
475 "FunctionDefinition" | "ErrorDefinition" | "EventDefinition" | "ModifierDefinition"
476 ) {
477 return None;
478 }
479
480 let is_return = if parent_type == "FunctionDefinition" {
482 parent_node
483 .get("returnParameters")
484 .and_then(|rp| rp.get("parameters"))
485 .and_then(|p| p.as_array())
486 .map(|arr| {
487 let decl_id = decl_node.get("id").and_then(|v| v.as_u64());
488 arr.iter()
489 .any(|p| p.get("id").and_then(|v| v.as_u64()) == decl_id)
490 })
491 .unwrap_or(false)
492 } else {
493 false
494 };
495
496 if let Some(parent_doc) = lookup_doc_entry(doc_index, parent_node, sources) {
498 if is_return {
499 for (rname, rdesc) in &parent_doc.returns {
501 if rname == param_name {
502 return Some(rdesc.clone());
503 }
504 }
505 } else {
506 for (pname, pdesc) in &parent_doc.params {
508 if pname == param_name {
509 return Some(pdesc.clone());
510 }
511 }
512 }
513 }
514
515 if let Some(doc_text) = extract_documentation(parent_node) {
517 let resolved = if doc_text.contains("@inheritdoc") {
519 resolve_inheritdoc(sources, parent_node, &doc_text)
520 } else {
521 None
522 };
523 let text = resolved.as_deref().unwrap_or(&doc_text);
524
525 let tag = if is_return { "@return " } else { "@param " };
526 for line in text.lines() {
527 let trimmed = line.trim().trim_start_matches('*').trim();
528 if let Some(rest) = trimmed.strip_prefix(tag) {
529 if let Some((name, desc)) = rest.split_once(' ') {
530 if name == param_name {
531 return Some(desc.to_string());
532 }
533 } else if rest == param_name {
534 return Some(String::new());
535 }
536 }
537 }
538 }
539
540 None
541}
542
543fn find_source_path_for_node(sources: &Value, target_id: u64) -> Option<String> {
545 let sources_obj = sources.as_object()?;
546 for (path, source_data) in sources_obj {
547 let ast = source_data.get("ast")?;
548 let source_id = ast.get("id").and_then(|v| v.as_u64())?;
550 if source_id == target_id {
551 return Some(path.clone());
552 }
553
554 if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
556 for node in nodes {
557 if let Some(id) = node.get("id").and_then(|v| v.as_u64())
558 && id == target_id
559 {
560 return Some(path.clone());
561 }
562 if let Some(sub_nodes) = node.get("nodes").and_then(|n| n.as_array()) {
564 for sub in sub_nodes {
565 if let Some(id) = sub.get("id").and_then(|v| v.as_u64())
566 && id == target_id
567 {
568 return Some(path.clone());
569 }
570 }
571 }
572 }
573 }
574 }
575 None
576}
577
578pub fn format_doc_entry(entry: &DocEntry) -> String {
580 let mut lines: Vec<String> = Vec::new();
581
582 if let Some(title) = &entry.title {
584 lines.push(format!("**{title}**"));
585 lines.push(String::new());
586 }
587
588 if let Some(notice) = &entry.notice {
590 lines.push(notice.clone());
591 }
592
593 if let Some(author) = &entry.author {
595 lines.push(format!("*@author {author}*"));
596 }
597
598 if let Some(details) = &entry.details {
600 lines.push(String::new());
601 lines.push("**@dev**".to_string());
602 lines.push(format!("*{details}*"));
603 }
604
605 if !entry.params.is_empty() {
607 lines.push(String::new());
608 lines.push("**Parameters:**".to_string());
609 for (name, desc) in &entry.params {
610 lines.push(format!("- `{name}` — {desc}"));
611 }
612 }
613
614 if !entry.returns.is_empty() {
616 lines.push(String::new());
617 lines.push("**Returns:**".to_string());
618 for (name, desc) in &entry.returns {
619 if name.starts_with('_') && name.len() <= 3 {
620 lines.push(format!("- {desc}"));
622 } else {
623 lines.push(format!("- `{name}` — {desc}"));
624 }
625 }
626 }
627
628 lines.join("\n")
629}
630
631pub fn find_node_by_id(sources: &Value, target_id: NodeId) -> Option<&Value> {
633 let sources_obj = sources.as_object()?;
634 for (_path, source_data) in sources_obj {
635 let ast = source_data.get("ast")?;
636
637 if ast.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
639 return Some(ast);
640 }
641
642 let mut stack = vec![ast];
643 while let Some(node) = stack.pop() {
644 if node.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
645 return Some(node);
646 }
647 for key in CHILD_KEYS {
648 if let Some(value) = node.get(key) {
649 match value {
650 Value::Array(arr) => stack.extend(arr.iter()),
651 Value::Object(_) => stack.push(value),
652 _ => {}
653 }
654 }
655 }
656 }
657 }
658 None
659}
660
661pub fn extract_documentation(node: &Value) -> Option<String> {
664 let doc = node.get("documentation")?;
665 match doc {
666 Value::Object(_) => doc
667 .get("text")
668 .and_then(|v| v.as_str())
669 .map(|s| s.to_string()),
670 Value::String(s) => Some(s.clone()),
671 _ => None,
672 }
673}
674
675pub fn extract_selector(node: &Value) -> Option<Selector> {
680 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
681 match node_type {
682 "FunctionDefinition" | "VariableDeclaration" => node
683 .get("functionSelector")
684 .and_then(|v| v.as_str())
685 .map(|s| Selector::Func(FuncSelector::new(s))),
686 "ErrorDefinition" => node
687 .get("errorSelector")
688 .and_then(|v| v.as_str())
689 .map(|s| Selector::Func(FuncSelector::new(s))),
690 "EventDefinition" => node
691 .get("eventSelector")
692 .and_then(|v| v.as_str())
693 .map(|s| Selector::Event(EventSelector::new(s))),
694 _ => None,
695 }
696}
697
698pub fn resolve_inheritdoc<'a>(
706 sources: &'a Value,
707 decl_node: &'a Value,
708 doc_text: &str,
709) -> Option<String> {
710 let parent_name = doc_text
712 .lines()
713 .find_map(|line| {
714 let trimmed = line.trim().trim_start_matches('*').trim();
715 trimmed.strip_prefix("@inheritdoc ")
716 })?
717 .trim();
718
719 let impl_selector = extract_selector(decl_node)?;
721
722 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
724
725 let scope_contract = find_node_by_id(sources, NodeId(scope_id))?;
727
728 let base_contracts = scope_contract
730 .get("baseContracts")
731 .and_then(|v| v.as_array())?;
732 let parent_id = base_contracts.iter().find_map(|base| {
733 let name = base
734 .get("baseName")
735 .and_then(|bn| bn.get("name"))
736 .and_then(|n| n.as_str())?;
737 if name == parent_name {
738 base.get("baseName")
739 .and_then(|bn| bn.get("referencedDeclaration"))
740 .and_then(|v| v.as_u64())
741 } else {
742 None
743 }
744 })?;
745
746 let parent_contract = find_node_by_id(sources, NodeId(parent_id))?;
748
749 let parent_nodes = parent_contract.get("nodes").and_then(|v| v.as_array())?;
751 for child in parent_nodes {
752 if let Some(child_selector) = extract_selector(child)
753 && child_selector == impl_selector
754 {
755 return extract_documentation(child);
756 }
757 }
758
759 None
760}
761
762pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
766 let mut lines: Vec<String> = Vec::new();
767 let mut in_params = false;
768 let mut in_returns = false;
769
770 for raw_line in text.lines() {
771 let line = raw_line.trim().trim_start_matches('*').trim();
772 if line.is_empty() {
773 continue;
774 }
775
776 if let Some(rest) = line.strip_prefix("@title ") {
777 in_params = false;
778 in_returns = false;
779 lines.push(format!("**{rest}**"));
780 lines.push(String::new());
781 } else if let Some(rest) = line.strip_prefix("@notice ") {
782 in_params = false;
783 in_returns = false;
784 lines.push(rest.to_string());
785 } else if let Some(rest) = line.strip_prefix("@dev ") {
786 in_params = false;
787 in_returns = false;
788 lines.push(String::new());
789 lines.push("**@dev**".to_string());
790 lines.push(format!("*{rest}*"));
791 } else if let Some(rest) = line.strip_prefix("@param ") {
792 if !in_params {
793 in_params = true;
794 in_returns = false;
795 lines.push(String::new());
796 lines.push("**Parameters:**".to_string());
797 }
798 if let Some((name, desc)) = rest.split_once(' ') {
799 lines.push(format!("- `{name}` — {desc}"));
800 } else {
801 lines.push(format!("- `{rest}`"));
802 }
803 } else if let Some(rest) = line.strip_prefix("@return ") {
804 if !in_returns {
805 in_returns = true;
806 in_params = false;
807 lines.push(String::new());
808 lines.push("**Returns:**".to_string());
809 }
810 if let Some((name, desc)) = rest.split_once(' ') {
811 lines.push(format!("- `{name}` — {desc}"));
812 } else {
813 lines.push(format!("- `{rest}`"));
814 }
815 } else if let Some(rest) = line.strip_prefix("@author ") {
816 in_params = false;
817 in_returns = false;
818 lines.push(format!("*@author {rest}*"));
819 } else if line.starts_with("@inheritdoc ") {
820 if let Some(inherited) = inherited_doc {
822 let formatted = format_natspec(inherited, None);
824 if !formatted.is_empty() {
825 lines.push(formatted);
826 }
827 } else {
828 let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
829 lines.push(format!("*Inherits documentation from `{parent}`*"));
830 }
831 } else if line.starts_with('@') {
832 in_params = false;
834 in_returns = false;
835 if let Some((tag, rest)) = line.split_once(' ') {
836 lines.push(String::new());
837 lines.push(format!("**{tag}**"));
838 lines.push(format!("*{rest}*"));
839 } else {
840 lines.push(String::new());
841 lines.push(format!("**{line}**"));
842 }
843 } else {
844 lines.push(line.to_string());
846 }
847 }
848
849 lines.join("\n")
850}
851
852fn build_function_signature(node: &Value) -> Option<String> {
854 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
855 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
856
857 match node_type {
858 "FunctionDefinition" => {
859 let kind = node
860 .get("kind")
861 .and_then(|v| v.as_str())
862 .unwrap_or("function");
863 let visibility = node
864 .get("visibility")
865 .and_then(|v| v.as_str())
866 .unwrap_or("");
867 let state_mutability = node
868 .get("stateMutability")
869 .and_then(|v| v.as_str())
870 .unwrap_or("");
871
872 let params = format_parameters(node.get("parameters"));
873 let returns = format_parameters(node.get("returnParameters"));
874
875 let mut sig = match kind {
876 "constructor" => format!("constructor({params})"),
877 "receive" => "receive() external payable".to_string(),
878 "fallback" => format!("fallback({params})"),
879 _ => format!("function {name}({params})"),
880 };
881
882 if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
883 sig.push_str(&format!(" {visibility}"));
884 }
885 if !state_mutability.is_empty() && state_mutability != "nonpayable" {
886 sig.push_str(&format!(" {state_mutability}"));
887 }
888 if !returns.is_empty() {
889 sig.push_str(&format!(" returns ({returns})"));
890 }
891 Some(sig)
892 }
893 "ModifierDefinition" => {
894 let params = format_parameters(node.get("parameters"));
895 Some(format!("modifier {name}({params})"))
896 }
897 "EventDefinition" => {
898 let params = format_parameters(node.get("parameters"));
899 Some(format!("event {name}({params})"))
900 }
901 "ErrorDefinition" => {
902 let params = format_parameters(node.get("parameters"));
903 Some(format!("error {name}({params})"))
904 }
905 "VariableDeclaration" => {
906 let type_str = node
907 .get("typeDescriptions")
908 .and_then(|v| v.get("typeString"))
909 .and_then(|v| v.as_str())
910 .unwrap_or("unknown");
911 let visibility = node
912 .get("visibility")
913 .and_then(|v| v.as_str())
914 .unwrap_or("");
915 let mutability = node
916 .get("mutability")
917 .and_then(|v| v.as_str())
918 .unwrap_or("");
919
920 let mut sig = type_str.to_string();
921 if !visibility.is_empty() {
922 sig.push_str(&format!(" {visibility}"));
923 }
924 if mutability == "constant" || mutability == "immutable" {
925 sig.push_str(&format!(" {mutability}"));
926 }
927 sig.push_str(&format!(" {name}"));
928 Some(sig)
929 }
930 "ContractDefinition" => {
931 let contract_kind = node
932 .get("contractKind")
933 .and_then(|v| v.as_str())
934 .unwrap_or("contract");
935
936 let mut sig = format!("{contract_kind} {name}");
937
938 if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array())
940 && !bases.is_empty()
941 {
942 let base_names: Vec<&str> = bases
943 .iter()
944 .filter_map(|b| {
945 b.get("baseName")
946 .and_then(|bn| bn.get("name"))
947 .and_then(|n| n.as_str())
948 })
949 .collect();
950 if !base_names.is_empty() {
951 sig.push_str(&format!(" is {}", base_names.join(", ")));
952 }
953 }
954 Some(sig)
955 }
956 "StructDefinition" => {
957 let mut sig = format!("struct {name} {{\n");
958 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
959 for member in members {
960 let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
961 let mtype = member
962 .get("typeDescriptions")
963 .and_then(|v| v.get("typeString"))
964 .and_then(|v| v.as_str())
965 .unwrap_or("?");
966 sig.push_str(&format!(" {mtype} {mname};\n"));
967 }
968 }
969 sig.push('}');
970 Some(sig)
971 }
972 "EnumDefinition" => {
973 let mut sig = format!("enum {name} {{\n");
974 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
975 let names: Vec<&str> = members
976 .iter()
977 .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
978 .collect();
979 for n in &names {
980 sig.push_str(&format!(" {n},\n"));
981 }
982 }
983 sig.push('}');
984 Some(sig)
985 }
986 "UserDefinedValueTypeDefinition" => {
987 let underlying = node
988 .get("underlyingType")
989 .and_then(|v| v.get("typeDescriptions"))
990 .and_then(|v| v.get("typeString"))
991 .and_then(|v| v.as_str())
992 .unwrap_or("unknown");
993 Some(format!("type {name} is {underlying}"))
994 }
995 _ => None,
996 }
997}
998
999fn format_parameters(params_node: Option<&Value>) -> String {
1001 let params_node = match params_node {
1002 Some(v) => v,
1003 None => return String::new(),
1004 };
1005 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
1006 Some(arr) => arr,
1007 None => return String::new(),
1008 };
1009
1010 let parts: Vec<String> = params
1011 .iter()
1012 .map(|p| {
1013 let type_str = p
1014 .get("typeDescriptions")
1015 .and_then(|v| v.get("typeString"))
1016 .and_then(|v| v.as_str())
1017 .unwrap_or("?");
1018 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
1019 let storage = p
1020 .get("storageLocation")
1021 .and_then(|v| v.as_str())
1022 .unwrap_or("default");
1023
1024 if name.is_empty() {
1025 type_str.to_string()
1026 } else if storage != "default" {
1027 format!("{type_str} {storage} {name}")
1028 } else {
1029 format!("{type_str} {name}")
1030 }
1031 })
1032 .collect();
1033
1034 parts.join(", ")
1035}
1036
1037fn gas_hover_for_function(
1039 decl_node: &Value,
1040 sources: &Value,
1041 gas_index: &GasIndex,
1042) -> Option<String> {
1043 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1044 if node_type != "FunctionDefinition" {
1045 return None;
1046 }
1047
1048 if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str())
1050 && let Some((_contract, cost)) =
1051 gas::gas_by_selector(gas_index, &FuncSelector::new(selector))
1052 {
1053 return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1054 }
1055
1056 let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
1058 let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1059 let contract_gas = gas_index.get(&contract_key)?;
1060
1061 let prefix = format!("{fn_name}(");
1063 for (sig, cost) in &contract_gas.internal {
1064 if sig.starts_with(&prefix) {
1065 return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1066 }
1067 }
1068
1069 None
1070}
1071
1072fn gas_hover_for_contract(
1074 decl_node: &Value,
1075 sources: &Value,
1076 gas_index: &GasIndex,
1077) -> Option<String> {
1078 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1079 if node_type != "ContractDefinition" {
1080 return None;
1081 }
1082
1083 let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1084 let contract_gas = gas_index.get(&contract_key)?;
1085
1086 let mut lines = Vec::new();
1087
1088 if !contract_gas.creation.is_empty() {
1090 lines.push("**Deploy Cost**".to_string());
1091 if let Some(cost) = contract_gas.creation.get("totalCost") {
1092 lines.push(format!("- Total: `{}`", gas::format_gas(cost)));
1093 }
1094 if let Some(cost) = contract_gas.creation.get("codeDepositCost") {
1095 lines.push(format!("- Code deposit: `{}`", gas::format_gas(cost)));
1096 }
1097 if let Some(cost) = contract_gas.creation.get("executionCost") {
1098 lines.push(format!("- Execution: `{}`", gas::format_gas(cost)));
1099 }
1100 }
1101
1102 if !contract_gas.external_by_sig.is_empty() {
1104 lines.push(String::new());
1105 lines.push("**Function Gas**".to_string());
1106
1107 let mut fns: Vec<(&MethodId, &String)> = contract_gas.external_by_sig.iter().collect();
1108 fns.sort_by_key(|(k, _)| k.as_str().to_string());
1109
1110 for (sig, cost) in fns {
1111 lines.push(format!("- `{}`: `{}`", sig.name(), gas::format_gas(cost)));
1112 }
1113 }
1114
1115 if lines.is_empty() {
1116 return None;
1117 }
1118
1119 Some(lines.join("\n"))
1120}
1121
1122pub fn hover_info(
1124 ast_data: &Value,
1125 file_uri: &Url,
1126 position: Position,
1127 source_bytes: &[u8],
1128 gas_index: &GasIndex,
1129 doc_index: &DocIndex,
1130 hint_index: &HintIndex,
1131) -> Option<Hover> {
1132 let sources = ast_data.get("sources")?;
1133 let source_id_to_path = ast_data
1134 .get("source_id_to_path")
1135 .and_then(|v| v.as_object())?;
1136
1137 let id_to_path: HashMap<String, String> = source_id_to_path
1138 .iter()
1139 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
1140 .collect();
1141
1142 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
1143
1144 let file_path = file_uri.to_file_path().ok()?;
1146 let file_path_str = file_path.to_str()?;
1147
1148 let abs_path = path_to_abs
1150 .iter()
1151 .find(|(k, _)| file_path_str.ends_with(k.as_str()))
1152 .map(|(_, v)| v.clone())?;
1153
1154 let byte_pos = pos_to_bytes(source_bytes, position);
1155
1156 let node_id = byte_to_decl_via_external_refs(&external_refs, &id_to_path, &abs_path, byte_pos)
1158 .or_else(|| byte_to_id(&nodes, &abs_path, byte_pos))?;
1159
1160 let node_info = nodes
1162 .values()
1163 .find_map(|file_nodes| file_nodes.get(&node_id))?;
1164
1165 let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
1167
1168 let decl_node = find_node_by_id(sources, decl_id)?;
1170
1171 let mut parts: Vec<String> = Vec::new();
1173
1174 if let Some(sig) = build_function_signature(decl_node) {
1176 parts.push(format!("```solidity\n{sig}\n```"));
1177 } else {
1178 if let Some(type_str) = decl_node
1180 .get("typeDescriptions")
1181 .and_then(|v| v.get("typeString"))
1182 .and_then(|v| v.as_str())
1183 {
1184 let name = decl_node.get("name").and_then(|v| v.as_str()).unwrap_or("");
1185 parts.push(format!("```solidity\n{type_str} {name}\n```"));
1186 }
1187 }
1188
1189 if let Some(selector) = extract_selector(decl_node) {
1191 parts.push(format!("Selector: `{}`", selector.to_prefixed()));
1192 }
1193
1194 if !gas_index.is_empty() {
1196 if let Some(gas_text) = gas_hover_for_function(decl_node, sources, gas_index) {
1197 parts.push(gas_text);
1198 } else if let Some(gas_text) = gas_hover_for_contract(decl_node, sources, gas_index) {
1199 parts.push(gas_text);
1200 }
1201 }
1202
1203 if let Some(doc_entry) = lookup_doc_entry(doc_index, decl_node, sources) {
1205 let formatted = format_doc_entry(&doc_entry);
1206 if !formatted.is_empty() {
1207 parts.push(format!("---\n{formatted}"));
1208 }
1209 } else if let Some(doc_text) = extract_documentation(decl_node) {
1210 let inherited_doc = resolve_inheritdoc(sources, decl_node, &doc_text);
1211 let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
1212 if !formatted.is_empty() {
1213 parts.push(format!("---\n{formatted}"));
1214 }
1215 } else if let Some(param_doc) = lookup_param_doc(doc_index, decl_node, sources) {
1216 if !param_doc.is_empty() {
1218 parts.push(format!("---\n{param_doc}"));
1219 }
1220 }
1221
1222 if let Some(hint_lookup) = hint_index.get(&abs_path) {
1227 let source_str = String::from_utf8_lossy(source_bytes);
1228 if let Some(tree) = crate::inlay_hints::ts_parse(&source_str) {
1229 if let Some(ctx) =
1230 crate::inlay_hints::ts_find_call_at_byte(tree.root_node(), &source_str, byte_pos)
1231 {
1232 if let Some(resolved) = hint_lookup.resolve_callsite_param(
1233 ctx.call_start_byte,
1234 ctx.name,
1235 ctx.arg_count,
1236 ctx.arg_index,
1237 ) {
1238 let fn_decl = find_node_by_id(sources, NodeId(resolved.decl_id));
1240 let param_doc = fn_decl.and_then(|decl| {
1241 if let Some(doc_entry) = lookup_doc_entry(doc_index, decl, sources) {
1243 for (pname, pdesc) in &doc_entry.params {
1244 if pname == &resolved.param_name {
1245 return Some(pdesc.clone());
1246 }
1247 }
1248 }
1249 if let Some(doc_text) = extract_documentation(decl) {
1251 let resolved_doc = if doc_text.contains("@inheritdoc") {
1252 resolve_inheritdoc(sources, decl, &doc_text)
1253 } else {
1254 None
1255 };
1256 let text = resolved_doc.as_deref().unwrap_or(&doc_text);
1257 for line in text.lines() {
1258 let trimmed = line.trim().trim_start_matches('*').trim();
1259 if let Some(rest) = trimmed.strip_prefix("@param ") {
1260 if let Some((name, desc)) = rest.split_once(' ') {
1261 if name == resolved.param_name {
1262 return Some(desc.to_string());
1263 }
1264 }
1265 }
1266 }
1267 }
1268 None
1269 });
1270 if let Some(desc) = param_doc {
1271 if !desc.is_empty() {
1272 parts.push(format!("**@param `{}`** — {desc}", resolved.param_name));
1273 }
1274 }
1275 }
1276 }
1277 }
1278 }
1279
1280 if parts.is_empty() {
1281 return None;
1282 }
1283
1284 Some(Hover {
1285 contents: HoverContents::Markup(MarkupContent {
1286 kind: MarkupKind::Markdown,
1287 value: parts.join("\n\n"),
1288 }),
1289 range: None,
1290 })
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296
1297 fn load_test_ast() -> Value {
1298 let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
1299 let raw: Value = serde_json::from_str(&data).expect("valid json");
1300 crate::solc::normalize_forge_output(raw)
1301 }
1302
1303 #[test]
1304 fn test_find_node_by_id_pool_manager() {
1305 let ast = load_test_ast();
1306 let sources = ast.get("sources").unwrap();
1307 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1308 assert_eq!(
1309 node.get("name").and_then(|v| v.as_str()),
1310 Some("PoolManager")
1311 );
1312 assert_eq!(
1313 node.get("nodeType").and_then(|v| v.as_str()),
1314 Some("ContractDefinition")
1315 );
1316 }
1317
1318 #[test]
1319 fn test_find_node_by_id_initialize() {
1320 let ast = load_test_ast();
1321 let sources = ast.get("sources").unwrap();
1322 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1324 assert_eq!(
1325 node.get("name").and_then(|v| v.as_str()),
1326 Some("initialize")
1327 );
1328 }
1329
1330 #[test]
1331 fn test_extract_documentation_object() {
1332 let ast = load_test_ast();
1333 let sources = ast.get("sources").unwrap();
1334 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1336 let doc = extract_documentation(node).unwrap();
1337 assert!(doc.contains("@notice"));
1338 assert!(doc.contains("@param key"));
1339 }
1340
1341 #[test]
1342 fn test_extract_documentation_none() {
1343 let ast = load_test_ast();
1344 let sources = ast.get("sources").unwrap();
1345 let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1347 let _ = extract_documentation(node);
1349 }
1350
1351 #[test]
1352 fn test_format_natspec_notice_and_params() {
1353 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";
1354 let formatted = format_natspec(text, None);
1355 assert!(formatted.contains("Initialize the state"));
1356 assert!(formatted.contains("**Parameters:**"));
1357 assert!(formatted.contains("`key`"));
1358 assert!(formatted.contains("**Returns:**"));
1359 assert!(formatted.contains("`tick`"));
1360 }
1361
1362 #[test]
1363 fn test_format_natspec_inheritdoc() {
1364 let text = "@inheritdoc IPoolManager";
1365 let formatted = format_natspec(text, None);
1366 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
1367 }
1368
1369 #[test]
1370 fn test_format_natspec_dev() {
1371 let text = "@notice Do something\n @dev This is an implementation detail";
1372 let formatted = format_natspec(text, None);
1373 assert!(formatted.contains("Do something"));
1374 assert!(formatted.contains("**@dev**"));
1375 assert!(formatted.contains("*This is an implementation detail*"));
1376 }
1377
1378 #[test]
1379 fn test_format_natspec_custom_tag() {
1380 let text = "@notice Do something\n @custom:security Non-reentrant";
1381 let formatted = format_natspec(text, None);
1382 assert!(formatted.contains("Do something"));
1383 assert!(formatted.contains("**@custom:security**"));
1384 assert!(formatted.contains("*Non-reentrant*"));
1385 }
1386
1387 #[test]
1388 fn test_build_function_signature_initialize() {
1389 let ast = load_test_ast();
1390 let sources = ast.get("sources").unwrap();
1391 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1392 let sig = build_function_signature(node).unwrap();
1393 assert!(sig.starts_with("function initialize("));
1394 assert!(sig.contains("returns"));
1395 }
1396
1397 #[test]
1398 fn test_build_signature_contract() {
1399 let ast = load_test_ast();
1400 let sources = ast.get("sources").unwrap();
1401 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1402 let sig = build_function_signature(node).unwrap();
1403 assert!(sig.contains("contract PoolManager"));
1404 assert!(sig.contains(" is "));
1405 }
1406
1407 #[test]
1408 fn test_build_signature_struct() {
1409 let ast = load_test_ast();
1410 let sources = ast.get("sources").unwrap();
1411 let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1412 let sig = build_function_signature(node).unwrap();
1413 assert!(sig.starts_with("struct PoolKey"));
1414 assert!(sig.contains('{'));
1415 }
1416
1417 #[test]
1418 fn test_build_signature_error() {
1419 let ast = load_test_ast();
1420 let sources = ast.get("sources").unwrap();
1421 let node = find_node_by_id(sources, NodeId(508)).unwrap();
1423 assert_eq!(
1424 node.get("nodeType").and_then(|v| v.as_str()),
1425 Some("ErrorDefinition")
1426 );
1427 let sig = build_function_signature(node).unwrap();
1428 assert!(sig.starts_with("error "));
1429 }
1430
1431 #[test]
1432 fn test_build_signature_event() {
1433 let ast = load_test_ast();
1434 let sources = ast.get("sources").unwrap();
1435 let node = find_node_by_id(sources, NodeId(8)).unwrap();
1437 assert_eq!(
1438 node.get("nodeType").and_then(|v| v.as_str()),
1439 Some("EventDefinition")
1440 );
1441 let sig = build_function_signature(node).unwrap();
1442 assert!(sig.starts_with("event "));
1443 }
1444
1445 #[test]
1446 fn test_build_signature_variable() {
1447 let ast = load_test_ast();
1448 let sources = ast.get("sources").unwrap();
1449 let pm = find_node_by_id(sources, NodeId(1767)).unwrap();
1452 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
1453 for node in nodes {
1454 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
1455 let sig = build_function_signature(node);
1456 assert!(sig.is_some());
1457 break;
1458 }
1459 }
1460 }
1461 }
1462
1463 #[test]
1464 fn test_pool_manager_has_documentation() {
1465 let ast = load_test_ast();
1466 let sources = ast.get("sources").unwrap();
1467 let node = find_node_by_id(sources, NodeId(59)).unwrap();
1469 let doc = extract_documentation(node).unwrap();
1470 assert!(doc.contains("@notice"));
1471 }
1472
1473 #[test]
1474 fn test_format_parameters_empty() {
1475 let result = format_parameters(None);
1476 assert_eq!(result, "");
1477 }
1478
1479 #[test]
1480 fn test_format_parameters_with_data() {
1481 let params: Value = serde_json::json!({
1482 "parameters": [
1483 {
1484 "name": "key",
1485 "typeDescriptions": { "typeString": "struct PoolKey" },
1486 "storageLocation": "memory"
1487 },
1488 {
1489 "name": "sqrtPriceX96",
1490 "typeDescriptions": { "typeString": "uint160" },
1491 "storageLocation": "default"
1492 }
1493 ]
1494 });
1495 let result = format_parameters(Some(¶ms));
1496 assert!(result.contains("struct PoolKey memory key"));
1497 assert!(result.contains("uint160 sqrtPriceX96"));
1498 }
1499
1500 #[test]
1503 fn test_extract_selector_function() {
1504 let ast = load_test_ast();
1505 let sources = ast.get("sources").unwrap();
1506 let node = find_node_by_id(sources, NodeId(1167)).unwrap();
1508 let selector = extract_selector(node).unwrap();
1509 assert_eq!(selector, Selector::Func(FuncSelector::new("f3cd914c")));
1510 assert_eq!(selector.as_hex(), "f3cd914c");
1511 }
1512
1513 #[test]
1514 fn test_extract_selector_error() {
1515 let ast = load_test_ast();
1516 let sources = ast.get("sources").unwrap();
1517 let node = find_node_by_id(sources, NodeId(508)).unwrap();
1519 let selector = extract_selector(node).unwrap();
1520 assert_eq!(selector, Selector::Func(FuncSelector::new("0d89438e")));
1521 assert_eq!(selector.as_hex(), "0d89438e");
1522 }
1523
1524 #[test]
1525 fn test_extract_selector_event() {
1526 let ast = load_test_ast();
1527 let sources = ast.get("sources").unwrap();
1528 let node = find_node_by_id(sources, NodeId(8)).unwrap();
1530 let selector = extract_selector(node).unwrap();
1531 assert!(matches!(selector, Selector::Event(_)));
1532 assert_eq!(selector.as_hex().len(), 64); }
1534
1535 #[test]
1536 fn test_extract_selector_public_variable() {
1537 let ast = load_test_ast();
1538 let sources = ast.get("sources").unwrap();
1539 let node = find_node_by_id(sources, NodeId(10)).unwrap();
1541 let selector = extract_selector(node).unwrap();
1542 assert_eq!(selector, Selector::Func(FuncSelector::new("8da5cb5b")));
1543 }
1544
1545 #[test]
1546 fn test_extract_selector_internal_function_none() {
1547 let ast = load_test_ast();
1548 let sources = ast.get("sources").unwrap();
1549 let node = find_node_by_id(sources, NodeId(5960)).unwrap();
1551 assert!(extract_selector(node).is_none());
1552 }
1553
1554 #[test]
1557 fn test_resolve_inheritdoc_swap() {
1558 let ast = load_test_ast();
1559 let sources = ast.get("sources").unwrap();
1560 let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1562 let doc_text = extract_documentation(decl).unwrap();
1563 assert!(doc_text.contains("@inheritdoc"));
1564
1565 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1566 assert!(resolved.contains("@notice"));
1567 assert!(resolved.contains("Swap against the given pool"));
1568 }
1569
1570 #[test]
1571 fn test_resolve_inheritdoc_initialize() {
1572 let ast = load_test_ast();
1573 let sources = ast.get("sources").unwrap();
1574 let decl = find_node_by_id(sources, NodeId(881)).unwrap();
1576 let doc_text = extract_documentation(decl).unwrap();
1577
1578 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1579 assert!(resolved.contains("Initialize the state"));
1580 assert!(resolved.contains("@param key"));
1581 }
1582
1583 #[test]
1584 fn test_resolve_inheritdoc_extsload_overload() {
1585 let ast = load_test_ast();
1586 let sources = ast.get("sources").unwrap();
1587
1588 let decl = find_node_by_id(sources, NodeId(442)).unwrap();
1590 let doc_text = extract_documentation(decl).unwrap();
1591 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1592 assert!(resolved.contains("granular pool state"));
1593 assert!(resolved.contains("@param slot"));
1595
1596 let decl2 = find_node_by_id(sources, NodeId(455)).unwrap();
1598 let doc_text2 = extract_documentation(decl2).unwrap();
1599 let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
1600 assert!(resolved2.contains("@param startSlot"));
1601
1602 let decl3 = find_node_by_id(sources, NodeId(467)).unwrap();
1604 let doc_text3 = extract_documentation(decl3).unwrap();
1605 let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
1606 assert!(resolved3.contains("sparse pool state"));
1607 }
1608
1609 #[test]
1610 fn test_resolve_inheritdoc_formats_in_hover() {
1611 let ast = load_test_ast();
1612 let sources = ast.get("sources").unwrap();
1613 let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1615 let doc_text = extract_documentation(decl).unwrap();
1616 let inherited = resolve_inheritdoc(sources, decl, &doc_text);
1617 let formatted = format_natspec(&doc_text, inherited.as_deref());
1618 assert!(!formatted.contains("@inheritdoc"));
1620 assert!(formatted.contains("Swap against the given pool"));
1621 assert!(formatted.contains("**Parameters:**"));
1622 }
1623
1624 #[test]
1627 fn test_param_doc_error_parameter() {
1628 let ast = load_test_ast();
1629 let sources = ast.get("sources").unwrap();
1630 let doc_index = build_doc_index(&ast);
1631
1632 let param_node = find_node_by_id(sources, NodeId(4760)).unwrap();
1634 assert_eq!(
1635 param_node.get("name").and_then(|v| v.as_str()),
1636 Some("sqrtPriceCurrentX96")
1637 );
1638
1639 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1640 assert!(
1641 doc.contains("invalid"),
1642 "should describe the invalid price: {doc}"
1643 );
1644 }
1645
1646 #[test]
1647 fn test_param_doc_error_second_parameter() {
1648 let ast = load_test_ast();
1649 let sources = ast.get("sources").unwrap();
1650 let doc_index = build_doc_index(&ast);
1651
1652 let param_node = find_node_by_id(sources, NodeId(4762)).unwrap();
1654 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1655 assert!(
1656 doc.contains("surpassed price limit"),
1657 "should describe the surpassed limit: {doc}"
1658 );
1659 }
1660
1661 #[test]
1662 fn test_param_doc_function_return_value() {
1663 let ast = load_test_ast();
1664 let sources = ast.get("sources").unwrap();
1665 let doc_index = build_doc_index(&ast);
1666
1667 let param_node = find_node_by_id(sources, NodeId(4994)).unwrap();
1669 assert_eq!(
1670 param_node.get("name").and_then(|v| v.as_str()),
1671 Some("delta")
1672 );
1673
1674 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1675 assert!(
1676 doc.contains("deltas of the token balances"),
1677 "should have return doc: {doc}"
1678 );
1679 }
1680
1681 #[test]
1682 fn test_param_doc_function_input_parameter() {
1683 let ast = load_test_ast();
1684 let sources = ast.get("sources").unwrap();
1685 let doc_index = build_doc_index(&ast);
1686
1687 let fn_node = find_node_by_id(sources, NodeId(5310)).unwrap();
1690 let params_arr = fn_node
1691 .get("parameters")
1692 .and_then(|p| p.get("parameters"))
1693 .and_then(|p| p.as_array())
1694 .unwrap();
1695 let params_param = params_arr
1696 .iter()
1697 .find(|p| p.get("name").and_then(|v| v.as_str()) == Some("params"))
1698 .unwrap();
1699
1700 let doc = lookup_param_doc(&doc_index, params_param, sources).unwrap();
1701 assert!(
1702 doc.contains("position details"),
1703 "should have param doc: {doc}"
1704 );
1705 }
1706
1707 #[test]
1708 fn test_param_doc_inherited_function_via_docindex() {
1709 let ast = load_test_ast();
1710 let sources = ast.get("sources").unwrap();
1711 let doc_index = build_doc_index(&ast);
1712
1713 let param_node = find_node_by_id(sources, NodeId(1029)).unwrap();
1716 assert_eq!(param_node.get("name").and_then(|v| v.as_str()), Some("key"));
1717
1718 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1719 assert!(
1720 doc.contains("pool to swap"),
1721 "should have inherited param doc: {doc}"
1722 );
1723 }
1724
1725 #[test]
1726 fn test_param_doc_non_parameter_returns_none() {
1727 let ast = load_test_ast();
1728 let sources = ast.get("sources").unwrap();
1729 let doc_index = build_doc_index(&ast);
1730
1731 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1733 assert!(lookup_param_doc(&doc_index, node, sources).is_none());
1734 }
1735
1736 fn load_solc_fixture() -> Value {
1739 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
1740 let raw: Value = serde_json::from_str(&data).expect("valid json");
1741 crate::solc::normalize_solc_output(raw, None)
1742 }
1743
1744 #[test]
1745 fn test_doc_index_is_not_empty() {
1746 let ast = load_solc_fixture();
1747 let index = build_doc_index(&ast);
1748 assert!(!index.is_empty(), "DocIndex should contain entries");
1749 }
1750
1751 #[test]
1752 fn test_doc_index_has_contract_entries() {
1753 let ast = load_solc_fixture();
1754 let index = build_doc_index(&ast);
1755
1756 let pm_keys: Vec<_> = index
1758 .keys()
1759 .filter(|k| matches!(k, DocKey::Contract(s) if s.contains("PoolManager")))
1760 .collect();
1761 assert!(
1762 !pm_keys.is_empty(),
1763 "should have a Contract entry for PoolManager"
1764 );
1765
1766 let pm_key = DocKey::Contract(
1767 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
1768 );
1769 let entry = index.get(&pm_key).expect("PoolManager contract entry");
1770 assert_eq!(entry.title.as_deref(), Some("PoolManager"));
1771 assert_eq!(
1772 entry.notice.as_deref(),
1773 Some("Holds the state for all pools")
1774 );
1775 }
1776
1777 #[test]
1778 fn test_doc_index_has_function_by_selector() {
1779 let ast = load_solc_fixture();
1780 let index = build_doc_index(&ast);
1781
1782 let init_key = DocKey::Func(FuncSelector::new("6276cbbe"));
1784 let entry = index
1785 .get(&init_key)
1786 .expect("should have initialize by selector");
1787 assert_eq!(
1788 entry.notice.as_deref(),
1789 Some("Initialize the state for a given pool ID")
1790 );
1791 assert!(
1792 entry
1793 .details
1794 .as_deref()
1795 .unwrap_or("")
1796 .contains("MAX_SWAP_FEE"),
1797 "devdoc details should mention MAX_SWAP_FEE"
1798 );
1799 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
1801 assert!(param_names.contains(&"key"), "should have param 'key'");
1802 assert!(
1803 param_names.contains(&"sqrtPriceX96"),
1804 "should have param 'sqrtPriceX96'"
1805 );
1806 let return_names: Vec<&str> = entry.returns.iter().map(|(n, _)| n.as_str()).collect();
1808 assert!(return_names.contains(&"tick"), "should have return 'tick'");
1809 }
1810
1811 #[test]
1812 fn test_doc_index_swap_by_selector() {
1813 let ast = load_solc_fixture();
1814 let index = build_doc_index(&ast);
1815
1816 let swap_key = DocKey::Func(FuncSelector::new("f3cd914c"));
1818 let entry = index.get(&swap_key).expect("should have swap by selector");
1819 assert!(
1820 entry
1821 .notice
1822 .as_deref()
1823 .unwrap_or("")
1824 .contains("Swap against the given pool"),
1825 "swap notice should describe swapping"
1826 );
1827 assert!(
1829 !entry.params.is_empty(),
1830 "swap should have param documentation"
1831 );
1832 }
1833
1834 #[test]
1835 fn test_doc_index_settle_by_selector() {
1836 let ast = load_solc_fixture();
1837 let index = build_doc_index(&ast);
1838
1839 let key = DocKey::Func(FuncSelector::new("11da60b4"));
1841 let entry = index.get(&key).expect("should have settle by selector");
1842 assert!(
1843 entry.notice.is_some(),
1844 "settle should have a notice from userdoc"
1845 );
1846 }
1847
1848 #[test]
1849 fn test_doc_index_has_error_entries() {
1850 let ast = load_solc_fixture();
1851 let index = build_doc_index(&ast);
1852
1853 let selector = compute_selector("AlreadyUnlocked()");
1855 let key = DocKey::Func(FuncSelector::new(&selector));
1856 let entry = index.get(&key).expect("should have AlreadyUnlocked error");
1857 assert!(
1858 entry
1859 .notice
1860 .as_deref()
1861 .unwrap_or("")
1862 .contains("already unlocked"),
1863 "AlreadyUnlocked notice: {:?}",
1864 entry.notice
1865 );
1866 }
1867
1868 #[test]
1869 fn test_doc_index_error_with_params() {
1870 let ast = load_solc_fixture();
1871 let index = build_doc_index(&ast);
1872
1873 let selector = compute_selector("CurrenciesOutOfOrderOrEqual(address,address)");
1875 let key = DocKey::Func(FuncSelector::new(&selector));
1876 let entry = index
1877 .get(&key)
1878 .expect("should have CurrenciesOutOfOrderOrEqual error");
1879 assert!(entry.notice.is_some(), "error should have notice");
1880 }
1881
1882 #[test]
1883 fn test_doc_index_has_event_entries() {
1884 let ast = load_solc_fixture();
1885 let index = build_doc_index(&ast);
1886
1887 let event_count = index
1889 .keys()
1890 .filter(|k| matches!(k, DocKey::Event(_)))
1891 .count();
1892 assert!(event_count > 0, "should have event entries in the DocIndex");
1893 }
1894
1895 #[test]
1896 fn test_doc_index_swap_event() {
1897 let ast = load_solc_fixture();
1898 let index = build_doc_index(&ast);
1899
1900 let topic =
1902 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
1903 let key = DocKey::Event(EventSelector::new(&topic));
1904 let entry = index.get(&key).expect("should have Swap event");
1905
1906 assert!(
1908 entry
1909 .notice
1910 .as_deref()
1911 .unwrap_or("")
1912 .contains("swaps between currency0 and currency1"),
1913 "Swap event notice: {:?}",
1914 entry.notice
1915 );
1916
1917 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
1919 assert!(
1920 param_names.contains(&"amount0"),
1921 "should have param 'amount0'"
1922 );
1923 assert!(
1924 param_names.contains(&"sender"),
1925 "should have param 'sender'"
1926 );
1927 assert!(param_names.contains(&"id"), "should have param 'id'");
1928 }
1929
1930 #[test]
1931 fn test_doc_index_initialize_event() {
1932 let ast = load_solc_fixture();
1933 let index = build_doc_index(&ast);
1934
1935 let topic = compute_event_topic(
1936 "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)",
1937 );
1938 let key = DocKey::Event(EventSelector::new(&topic));
1939 let entry = index.get(&key).expect("should have Initialize event");
1940 assert!(
1941 !entry.params.is_empty(),
1942 "Initialize event should have param docs"
1943 );
1944 }
1945
1946 #[test]
1947 fn test_doc_index_no_state_variables_for_pool_manager() {
1948 let ast = load_solc_fixture();
1949 let index = build_doc_index(&ast);
1950
1951 let sv_count = index
1953 .keys()
1954 .filter(|k| matches!(k, DocKey::StateVar(s) if s.contains("PoolManager")))
1955 .count();
1956 assert_eq!(
1957 sv_count, 0,
1958 "PoolManager should have no state variable doc entries"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_doc_index_multiple_contracts() {
1964 let ast = load_solc_fixture();
1965 let index = build_doc_index(&ast);
1966
1967 let contract_count = index
1969 .keys()
1970 .filter(|k| matches!(k, DocKey::Contract(_)))
1971 .count();
1972 assert!(
1973 contract_count >= 5,
1974 "should have at least 5 contract-level entries, got {contract_count}"
1975 );
1976 }
1977
1978 #[test]
1979 fn test_doc_index_func_key_count() {
1980 let ast = load_solc_fixture();
1981 let index = build_doc_index(&ast);
1982
1983 let func_count = index
1984 .keys()
1985 .filter(|k| matches!(k, DocKey::Func(_)))
1986 .count();
1987 assert!(
1989 func_count >= 30,
1990 "should have at least 30 Func entries (methods + errors), got {func_count}"
1991 );
1992 }
1993
1994 #[test]
1995 fn test_doc_index_format_initialize_entry() {
1996 let ast = load_solc_fixture();
1997 let index = build_doc_index(&ast);
1998
1999 let key = DocKey::Func(FuncSelector::new("6276cbbe"));
2000 let entry = index.get(&key).expect("initialize entry");
2001 let formatted = format_doc_entry(entry);
2002
2003 assert!(
2004 formatted.contains("Initialize the state for a given pool ID"),
2005 "formatted should include notice"
2006 );
2007 assert!(
2008 formatted.contains("**@dev**"),
2009 "formatted should include dev section"
2010 );
2011 assert!(
2012 formatted.contains("**Parameters:**"),
2013 "formatted should include parameters"
2014 );
2015 assert!(
2016 formatted.contains("`key`"),
2017 "formatted should include key param"
2018 );
2019 assert!(
2020 formatted.contains("**Returns:**"),
2021 "formatted should include returns"
2022 );
2023 assert!(
2024 formatted.contains("`tick`"),
2025 "formatted should include tick return"
2026 );
2027 }
2028
2029 #[test]
2030 fn test_doc_index_format_contract_entry() {
2031 let ast = load_solc_fixture();
2032 let index = build_doc_index(&ast);
2033
2034 let key = DocKey::Contract(
2035 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2036 );
2037 let entry = index.get(&key).expect("PoolManager contract entry");
2038 let formatted = format_doc_entry(entry);
2039
2040 assert!(
2041 formatted.contains("**PoolManager**"),
2042 "should include bold title"
2043 );
2044 assert!(
2045 formatted.contains("Holds the state for all pools"),
2046 "should include notice"
2047 );
2048 }
2049
2050 #[test]
2051 fn test_doc_index_inherited_docs_resolved() {
2052 let ast = load_solc_fixture();
2053 let index = build_doc_index(&ast);
2054
2055 let key = DocKey::Func(FuncSelector::new("f3cd914c"));
2059 let entry = index.get(&key).expect("swap entry");
2060 let notice = entry.notice.as_deref().unwrap_or("");
2062 assert!(
2063 !notice.contains("@inheritdoc"),
2064 "userdoc/devdoc should have resolved inherited docs, not raw @inheritdoc"
2065 );
2066 }
2067
2068 #[test]
2069 fn test_compute_selector_known_values() {
2070 let sel = compute_selector("AlreadyUnlocked()");
2072 assert_eq!(sel.len(), 8, "selector should be 8 hex chars");
2073
2074 let init_sel =
2076 compute_selector("initialize((address,address,uint24,int24,address),uint160)");
2077 assert_eq!(
2078 init_sel, "6276cbbe",
2079 "computed initialize selector should match evm.methodIdentifiers"
2080 );
2081 }
2082
2083 #[test]
2084 fn test_compute_event_topic_length() {
2085 let topic =
2086 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2087 assert_eq!(
2088 topic.len(),
2089 64,
2090 "event topic should be 64 hex chars (32 bytes)"
2091 );
2092 }
2093
2094 #[test]
2095 fn test_doc_index_error_count_poolmanager() {
2096 let ast = load_solc_fixture();
2097 let index = build_doc_index(&ast);
2098
2099 let error_sigs = [
2102 "AlreadyUnlocked()",
2103 "CurrenciesOutOfOrderOrEqual(address,address)",
2104 "CurrencyNotSettled()",
2105 "InvalidCaller()",
2106 "ManagerLocked()",
2107 "MustClearExactPositiveDelta()",
2108 "NonzeroNativeValue()",
2109 "PoolNotInitialized()",
2110 "ProtocolFeeCurrencySynced()",
2111 "ProtocolFeeTooLarge(uint24)",
2112 "SwapAmountCannotBeZero()",
2113 "TickSpacingTooLarge(int24)",
2114 "TickSpacingTooSmall(int24)",
2115 "UnauthorizedDynamicLPFeeUpdate()",
2116 ];
2117 let mut found = 0;
2118 for sig in &error_sigs {
2119 let selector = compute_selector(sig);
2120 let key = DocKey::Func(FuncSelector::new(&selector));
2121 if index.contains_key(&key) {
2122 found += 1;
2123 }
2124 }
2125 assert_eq!(
2126 found,
2127 error_sigs.len(),
2128 "all 14 PoolManager errors should be in the DocIndex"
2129 );
2130 }
2131
2132 #[test]
2133 fn test_doc_index_extsload_overloads_have_different_selectors() {
2134 let ast = load_solc_fixture();
2135 let index = build_doc_index(&ast);
2136
2137 let sel1 = DocKey::Func(FuncSelector::new("1e2eaeaf"));
2142 let sel2 = DocKey::Func(FuncSelector::new("35fd631a"));
2143 let sel3 = DocKey::Func(FuncSelector::new("dbd035ff"));
2144
2145 assert!(index.contains_key(&sel1), "extsload(bytes32) should exist");
2146 assert!(
2147 index.contains_key(&sel2),
2148 "extsload(bytes32,uint256) should exist"
2149 );
2150 assert!(
2151 index.contains_key(&sel3),
2152 "extsload(bytes32[]) should exist"
2153 );
2154 }
2155}