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};
9use crate::goto::{CHILD_KEYS, cache_ids, pos_to_bytes};
10use crate::inlay_hints::HintIndex;
11use crate::references::{byte_to_decl_via_external_refs, byte_to_id};
12use crate::types::{EventSelector, FuncSelector, MethodId, NodeId, Selector};
13
14#[derive(Debug, Clone, Default)]
18pub struct DocEntry {
19 pub notice: Option<String>,
21 pub details: Option<String>,
23 pub params: Vec<(String, String)>,
25 pub returns: Vec<(String, String)>,
27 pub title: Option<String>,
29 pub author: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum DocKey {
36 Func(FuncSelector),
38 Event(EventSelector),
40 Contract(String),
42 StateVar(String),
44 Method(String),
46}
47
48pub type DocIndex = HashMap<DocKey, DocEntry>;
52
53pub fn build_doc_index(ast_data: &Value) -> DocIndex {
58 let mut index = DocIndex::new();
59
60 let contracts = match ast_data.get("contracts").and_then(|c| c.as_object()) {
61 Some(c) => c,
62 None => return index,
63 };
64
65 for (path, names) in contracts {
66 let names_obj = match names.as_object() {
67 Some(n) => n,
68 None => continue,
69 };
70
71 for (name, contract) in names_obj {
72 let userdoc = contract.get("userdoc");
73 let devdoc = contract.get("devdoc");
74 let method_ids = contract
75 .get("evm")
76 .and_then(|e| e.get("methodIdentifiers"))
77 .and_then(|m| m.as_object());
78
79 let sig_to_selector: HashMap<&str, &str> = method_ids
81 .map(|mi| {
82 mi.iter()
83 .filter_map(|(sig, sel)| sel.as_str().map(|s| (sig.as_str(), s)))
84 .collect()
85 })
86 .unwrap_or_default();
87
88 let mut contract_entry = DocEntry::default();
90 if let Some(ud) = userdoc {
91 contract_entry.notice = ud
92 .get("notice")
93 .and_then(|v| v.as_str())
94 .map(|s| s.to_string());
95 }
96 if let Some(dd) = devdoc {
97 contract_entry.title = dd
98 .get("title")
99 .and_then(|v| v.as_str())
100 .map(|s| s.to_string());
101 contract_entry.details = dd
102 .get("details")
103 .and_then(|v| v.as_str())
104 .map(|s| s.to_string());
105 contract_entry.author = dd
106 .get("author")
107 .and_then(|v| v.as_str())
108 .map(|s| s.to_string());
109 }
110 if contract_entry.notice.is_some()
111 || contract_entry.title.is_some()
112 || contract_entry.details.is_some()
113 {
114 let key = DocKey::Contract(format!("{path}:{name}"));
115 index.insert(key, contract_entry);
116 }
117
118 let ud_methods = userdoc
120 .and_then(|u| u.get("methods"))
121 .and_then(|m| m.as_object());
122 let dd_methods = devdoc
123 .and_then(|d| d.get("methods"))
124 .and_then(|m| m.as_object());
125
126 let mut all_sigs: Vec<&str> = Vec::new();
128 if let Some(um) = ud_methods {
129 all_sigs.extend(um.keys().map(|k| k.as_str()));
130 }
131 if let Some(dm) = dd_methods {
132 for k in dm.keys() {
133 if !all_sigs.contains(&k.as_str()) {
134 all_sigs.push(k.as_str());
135 }
136 }
137 }
138
139 for sig in &all_sigs {
140 let mut entry = DocEntry::default();
141
142 if let Some(um) = ud_methods
144 && let Some(method) = um.get(*sig)
145 {
146 entry.notice = method
147 .get("notice")
148 .and_then(|v| v.as_str())
149 .map(|s| s.to_string());
150 }
151
152 if let Some(dm) = dd_methods
154 && let Some(method) = dm.get(*sig)
155 {
156 entry.details = method
157 .get("details")
158 .and_then(|v| v.as_str())
159 .map(|s| s.to_string());
160
161 if let Some(params) = method.get("params").and_then(|p| p.as_object()) {
162 for (pname, pdesc) in params {
163 if let Some(desc) = pdesc.as_str() {
164 entry.params.push((pname.clone(), desc.to_string()));
165 }
166 }
167 }
168
169 if let Some(returns) = method.get("returns").and_then(|r| r.as_object()) {
170 for (rname, rdesc) in returns {
171 if let Some(desc) = rdesc.as_str() {
172 entry.returns.push((rname.clone(), desc.to_string()));
173 }
174 }
175 }
176 }
177
178 if entry.notice.is_none()
179 && entry.details.is_none()
180 && entry.params.is_empty()
181 && entry.returns.is_empty()
182 {
183 continue;
184 }
185
186 if let Some(selector) = sig_to_selector.get(sig) {
188 let key = DocKey::Func(FuncSelector::new(*selector));
189 index.insert(key, entry);
190 } else {
191 let fn_name = sig.split('(').next().unwrap_or(sig);
194 let key = DocKey::Method(format!("{path}:{name}:{fn_name}"));
195 index.insert(key, entry);
196 }
197 }
198
199 let ud_errors = userdoc
201 .and_then(|u| u.get("errors"))
202 .and_then(|e| e.as_object());
203 let dd_errors = devdoc
204 .and_then(|d| d.get("errors"))
205 .and_then(|e| e.as_object());
206
207 let mut all_error_sigs: Vec<&str> = Vec::new();
208 if let Some(ue) = ud_errors {
209 all_error_sigs.extend(ue.keys().map(|k| k.as_str()));
210 }
211 if let Some(de) = dd_errors {
212 for k in de.keys() {
213 if !all_error_sigs.contains(&k.as_str()) {
214 all_error_sigs.push(k.as_str());
215 }
216 }
217 }
218
219 for sig in &all_error_sigs {
220 let mut entry = DocEntry::default();
221
222 if let Some(ue) = ud_errors
224 && let Some(arr) = ue.get(*sig).and_then(|v| v.as_array())
225 && let Some(first) = arr.first()
226 {
227 entry.notice = first
228 .get("notice")
229 .and_then(|v| v.as_str())
230 .map(|s| s.to_string());
231 }
232
233 if let Some(de) = dd_errors
235 && let Some(arr) = de.get(*sig).and_then(|v| v.as_array())
236 && let Some(first) = arr.first()
237 {
238 entry.details = first
239 .get("details")
240 .and_then(|v| v.as_str())
241 .map(|s| s.to_string());
242 if let Some(params) = first.get("params").and_then(|p| p.as_object()) {
243 for (pname, pdesc) in params {
244 if let Some(desc) = pdesc.as_str() {
245 entry.params.push((pname.clone(), desc.to_string()));
246 }
247 }
248 }
249 }
250
251 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
252 continue;
253 }
254
255 let selector = FuncSelector::new(compute_selector(sig));
258 index.insert(DocKey::Func(selector), entry);
259 }
260
261 let ud_events = userdoc
263 .and_then(|u| u.get("events"))
264 .and_then(|e| e.as_object());
265 let dd_events = devdoc
266 .and_then(|d| d.get("events"))
267 .and_then(|e| e.as_object());
268
269 let mut all_event_sigs: Vec<&str> = Vec::new();
270 if let Some(ue) = ud_events {
271 all_event_sigs.extend(ue.keys().map(|k| k.as_str()));
272 }
273 if let Some(de) = dd_events {
274 for k in de.keys() {
275 if !all_event_sigs.contains(&k.as_str()) {
276 all_event_sigs.push(k.as_str());
277 }
278 }
279 }
280
281 for sig in &all_event_sigs {
282 let mut entry = DocEntry::default();
283
284 if let Some(ue) = ud_events
285 && let Some(ev) = ue.get(*sig)
286 {
287 entry.notice = ev
288 .get("notice")
289 .and_then(|v| v.as_str())
290 .map(|s| s.to_string());
291 }
292
293 if let Some(de) = dd_events
294 && let Some(ev) = de.get(*sig)
295 {
296 entry.details = ev
297 .get("details")
298 .and_then(|v| v.as_str())
299 .map(|s| s.to_string());
300 if let Some(params) = ev.get("params").and_then(|p| p.as_object()) {
301 for (pname, pdesc) in params {
302 if let Some(desc) = pdesc.as_str() {
303 entry.params.push((pname.clone(), desc.to_string()));
304 }
305 }
306 }
307 }
308
309 if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
310 continue;
311 }
312
313 let topic = EventSelector::new(compute_event_topic(sig));
315 index.insert(DocKey::Event(topic), entry);
316 }
317
318 if let Some(dd) = devdoc
320 && let Some(state_vars) = dd.get("stateVariables").and_then(|s| s.as_object())
321 {
322 for (var_name, var_doc) in state_vars {
323 let mut entry = DocEntry::default();
324 entry.details = var_doc
325 .get("details")
326 .and_then(|v| v.as_str())
327 .map(|s| s.to_string());
328
329 if let Some(returns) = var_doc.get("return").and_then(|v| v.as_str()) {
330 entry.returns.push(("_0".to_string(), returns.to_string()));
331 }
332 if let Some(returns) = var_doc.get("returns").and_then(|r| r.as_object()) {
333 for (rname, rdesc) in returns {
334 if let Some(desc) = rdesc.as_str() {
335 entry.returns.push((rname.clone(), desc.to_string()));
336 }
337 }
338 }
339
340 if entry.details.is_some() || !entry.returns.is_empty() {
341 let key = DocKey::StateVar(format!("{path}:{name}:{var_name}"));
342 index.insert(key, entry);
343 }
344 }
345 }
346 }
347 }
348
349 index
350}
351
352fn compute_selector(sig: &str) -> String {
356 use tiny_keccak::{Hasher, Keccak};
357 let mut hasher = Keccak::v256();
358 hasher.update(sig.as_bytes());
359 let mut output = [0u8; 32];
360 hasher.finalize(&mut output);
361 hex::encode(&output[..4])
362}
363
364fn compute_event_topic(sig: &str) -> String {
368 use tiny_keccak::{Hasher, Keccak};
369 let mut hasher = Keccak::v256();
370 hasher.update(sig.as_bytes());
371 let mut output = [0u8; 32];
372 hasher.finalize(&mut output);
373 hex::encode(output)
374}
375
376pub fn lookup_doc_entry(
380 doc_index: &DocIndex,
381 decl_node: &Value,
382 sources: &Value,
383) -> Option<DocEntry> {
384 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
385
386 match node_type {
387 "FunctionDefinition" | "VariableDeclaration" => {
388 if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str()) {
390 let key = DocKey::Func(FuncSelector::new(selector));
391 if let Some(entry) = doc_index.get(&key) {
392 return Some(entry.clone());
393 }
394 }
395
396 if node_type == "VariableDeclaration" {
398 let var_name = decl_node.get("name").and_then(|v| v.as_str())?;
399 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
401 let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
402 let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
403
404 let path = find_source_path_for_node(sources, scope_id)?;
406 let key = DocKey::StateVar(format!("{path}:{contract_name}:{var_name}"));
407 if let Some(entry) = doc_index.get(&key) {
408 return Some(entry.clone());
409 }
410 }
411
412 let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
414 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
415 let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
416 let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
417 let path = find_source_path_for_node(sources, scope_id)?;
418 let key = DocKey::Method(format!("{path}:{contract_name}:{fn_name}"));
419 doc_index.get(&key).cloned()
420 }
421 "ErrorDefinition" => {
422 if let Some(selector) = decl_node.get("errorSelector").and_then(|v| v.as_str()) {
423 let key = DocKey::Func(FuncSelector::new(selector));
424 return doc_index.get(&key).cloned();
425 }
426 None
427 }
428 "EventDefinition" => {
429 if let Some(selector) = decl_node.get("eventSelector").and_then(|v| v.as_str()) {
430 let key = DocKey::Event(EventSelector::new(selector));
431 return doc_index.get(&key).cloned();
432 }
433 None
434 }
435 "ContractDefinition" => {
436 let contract_name = decl_node.get("name").and_then(|v| v.as_str())?;
437 let node_id = decl_node.get("id").and_then(|v| v.as_u64())?;
439 let path = find_source_path_for_node(sources, node_id)?;
440 let key = DocKey::Contract(format!("{path}:{contract_name}"));
441 doc_index.get(&key).cloned()
442 }
443 _ => None,
444 }
445}
446
447pub fn lookup_param_doc(
456 doc_index: &DocIndex,
457 decl_node: &Value,
458 sources: &Value,
459) -> Option<String> {
460 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
461 if node_type != "VariableDeclaration" {
462 return None;
463 }
464
465 let param_name = decl_node.get("name").and_then(|v| v.as_str())?;
466 if param_name.is_empty() {
467 return None;
468 }
469
470 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
472 let parent_node = find_node_by_id(sources, NodeId(scope_id))?;
473 let parent_type = parent_node.get("nodeType").and_then(|v| v.as_str())?;
474
475 if !matches!(
477 parent_type,
478 "FunctionDefinition" | "ErrorDefinition" | "EventDefinition" | "ModifierDefinition"
479 ) {
480 return None;
481 }
482
483 let is_return = if parent_type == "FunctionDefinition" {
485 parent_node
486 .get("returnParameters")
487 .and_then(|rp| rp.get("parameters"))
488 .and_then(|p| p.as_array())
489 .map(|arr| {
490 let decl_id = decl_node.get("id").and_then(|v| v.as_u64());
491 arr.iter()
492 .any(|p| p.get("id").and_then(|v| v.as_u64()) == decl_id)
493 })
494 .unwrap_or(false)
495 } else {
496 false
497 };
498
499 if let Some(parent_doc) = lookup_doc_entry(doc_index, parent_node, sources) {
501 if is_return {
502 for (rname, rdesc) in &parent_doc.returns {
504 if rname == param_name {
505 return Some(rdesc.clone());
506 }
507 }
508 } else {
509 for (pname, pdesc) in &parent_doc.params {
511 if pname == param_name {
512 return Some(pdesc.clone());
513 }
514 }
515 }
516 }
517
518 if let Some(doc_text) = extract_documentation(parent_node) {
520 let resolved = if doc_text.contains("@inheritdoc") {
522 resolve_inheritdoc(sources, parent_node, &doc_text)
523 } else {
524 None
525 };
526 let text = resolved.as_deref().unwrap_or(&doc_text);
527
528 let tag = if is_return { "@return " } else { "@param " };
529 for line in text.lines() {
530 let trimmed = line.trim().trim_start_matches('*').trim();
531 if let Some(rest) = trimmed.strip_prefix(tag) {
532 if let Some((name, desc)) = rest.split_once(' ') {
533 if name == param_name {
534 return Some(desc.to_string());
535 }
536 } else if rest == param_name {
537 return Some(String::new());
538 }
539 }
540 }
541 }
542
543 None
544}
545
546fn find_source_path_for_node(sources: &Value, target_id: u64) -> Option<String> {
548 let sources_obj = sources.as_object()?;
549 for (path, source_data) in sources_obj {
550 let ast = source_data.get("ast")?;
551 let source_id = ast.get("id").and_then(|v| v.as_u64())?;
553 if source_id == target_id {
554 return Some(path.clone());
555 }
556
557 if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
559 for node in nodes {
560 if let Some(id) = node.get("id").and_then(|v| v.as_u64())
561 && id == target_id
562 {
563 return Some(path.clone());
564 }
565 if let Some(sub_nodes) = node.get("nodes").and_then(|n| n.as_array()) {
567 for sub in sub_nodes {
568 if let Some(id) = sub.get("id").and_then(|v| v.as_u64())
569 && id == target_id
570 {
571 return Some(path.clone());
572 }
573 }
574 }
575 }
576 }
577 }
578 None
579}
580
581pub fn format_doc_entry(entry: &DocEntry) -> String {
583 let mut lines: Vec<String> = Vec::new();
584
585 if let Some(title) = &entry.title {
587 lines.push(format!("**{title}**"));
588 lines.push(String::new());
589 }
590
591 if let Some(notice) = &entry.notice {
593 lines.push(notice.clone());
594 }
595
596 if let Some(author) = &entry.author {
598 lines.push(format!("*@author {author}*"));
599 }
600
601 if let Some(details) = &entry.details {
603 lines.push(String::new());
604 lines.push("**@dev**".to_string());
605 lines.push(format!("*{details}*"));
606 }
607
608 if !entry.params.is_empty() {
610 lines.push(String::new());
611 lines.push("**Parameters:**".to_string());
612 for (name, desc) in &entry.params {
613 lines.push(format!("- `{name}` — {desc}"));
614 }
615 }
616
617 if !entry.returns.is_empty() {
619 lines.push(String::new());
620 lines.push("**Returns:**".to_string());
621 for (name, desc) in &entry.returns {
622 if name.starts_with('_') && name.len() <= 3 {
623 lines.push(format!("- {desc}"));
625 } else {
626 lines.push(format!("- `{name}` — {desc}"));
627 }
628 }
629 }
630
631 lines.join("\n")
632}
633
634pub fn find_node_by_id(sources: &Value, target_id: NodeId) -> Option<&Value> {
636 let sources_obj = sources.as_object()?;
637 for (_path, source_data) in sources_obj {
638 let ast = source_data.get("ast")?;
639
640 if ast.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
642 return Some(ast);
643 }
644
645 let mut stack = vec![ast];
646 while let Some(node) = stack.pop() {
647 if node.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
648 return Some(node);
649 }
650 for key in CHILD_KEYS {
651 if let Some(value) = node.get(key) {
652 match value {
653 Value::Array(arr) => stack.extend(arr.iter()),
654 Value::Object(_) => stack.push(value),
655 _ => {}
656 }
657 }
658 }
659 }
660 }
661 None
662}
663
664pub fn extract_documentation(node: &Value) -> Option<String> {
667 let doc = node.get("documentation")?;
668 match doc {
669 Value::Object(_) => doc
670 .get("text")
671 .and_then(|v| v.as_str())
672 .map(|s| s.to_string()),
673 Value::String(s) => Some(s.clone()),
674 _ => None,
675 }
676}
677
678pub fn extract_selector(node: &Value) -> Option<Selector> {
683 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
684 match node_type {
685 "FunctionDefinition" | "VariableDeclaration" => node
686 .get("functionSelector")
687 .and_then(|v| v.as_str())
688 .map(|s| Selector::Func(FuncSelector::new(s))),
689 "ErrorDefinition" => node
690 .get("errorSelector")
691 .and_then(|v| v.as_str())
692 .map(|s| Selector::Func(FuncSelector::new(s))),
693 "EventDefinition" => node
694 .get("eventSelector")
695 .and_then(|v| v.as_str())
696 .map(|s| Selector::Event(EventSelector::new(s))),
697 _ => None,
698 }
699}
700
701pub fn resolve_inheritdoc<'a>(
709 sources: &'a Value,
710 decl_node: &'a Value,
711 doc_text: &str,
712) -> Option<String> {
713 let parent_name = doc_text
715 .lines()
716 .find_map(|line| {
717 let trimmed = line.trim().trim_start_matches('*').trim();
718 trimmed.strip_prefix("@inheritdoc ")
719 })?
720 .trim();
721
722 let impl_selector = extract_selector(decl_node)?;
724
725 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
727
728 let scope_contract = find_node_by_id(sources, NodeId(scope_id))?;
730
731 let base_contracts = scope_contract
733 .get("baseContracts")
734 .and_then(|v| v.as_array())?;
735 let parent_id = base_contracts.iter().find_map(|base| {
736 let name = base
737 .get("baseName")
738 .and_then(|bn| bn.get("name"))
739 .and_then(|n| n.as_str())?;
740 if name == parent_name {
741 base.get("baseName")
742 .and_then(|bn| bn.get("referencedDeclaration"))
743 .and_then(|v| v.as_u64())
744 } else {
745 None
746 }
747 })?;
748
749 let parent_contract = find_node_by_id(sources, NodeId(parent_id))?;
751
752 let parent_nodes = parent_contract.get("nodes").and_then(|v| v.as_array())?;
754 for child in parent_nodes {
755 if let Some(child_selector) = extract_selector(child)
756 && child_selector == impl_selector
757 {
758 return extract_documentation(child);
759 }
760 }
761
762 None
763}
764
765pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
769 let mut lines: Vec<String> = Vec::new();
770 let mut in_params = false;
771 let mut in_returns = false;
772
773 for raw_line in text.lines() {
774 let line = raw_line.trim().trim_start_matches('*').trim();
775 if line.is_empty() {
776 continue;
777 }
778
779 if let Some(rest) = line.strip_prefix("@title ") {
780 in_params = false;
781 in_returns = false;
782 lines.push(format!("**{rest}**"));
783 lines.push(String::new());
784 } else if let Some(rest) = line.strip_prefix("@notice ") {
785 in_params = false;
786 in_returns = false;
787 lines.push(rest.to_string());
788 } else if let Some(rest) = line.strip_prefix("@dev ") {
789 in_params = false;
790 in_returns = false;
791 lines.push(String::new());
792 lines.push("**@dev**".to_string());
793 lines.push(format!("*{rest}*"));
794 } else if let Some(rest) = line.strip_prefix("@param ") {
795 if !in_params {
796 in_params = true;
797 in_returns = false;
798 lines.push(String::new());
799 lines.push("**Parameters:**".to_string());
800 }
801 if let Some((name, desc)) = rest.split_once(' ') {
802 lines.push(format!("- `{name}` — {desc}"));
803 } else {
804 lines.push(format!("- `{rest}`"));
805 }
806 } else if let Some(rest) = line.strip_prefix("@return ") {
807 if !in_returns {
808 in_returns = true;
809 in_params = false;
810 lines.push(String::new());
811 lines.push("**Returns:**".to_string());
812 }
813 if let Some((name, desc)) = rest.split_once(' ') {
814 lines.push(format!("- `{name}` — {desc}"));
815 } else {
816 lines.push(format!("- `{rest}`"));
817 }
818 } else if let Some(rest) = line.strip_prefix("@author ") {
819 in_params = false;
820 in_returns = false;
821 lines.push(format!("*@author {rest}*"));
822 } else if line.starts_with("@inheritdoc ") {
823 if let Some(inherited) = inherited_doc {
825 let formatted = format_natspec(inherited, None);
827 if !formatted.is_empty() {
828 lines.push(formatted);
829 }
830 } else {
831 let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
832 lines.push(format!("*Inherits documentation from `{parent}`*"));
833 }
834 } else if line.starts_with('@') {
835 in_params = false;
837 in_returns = false;
838 if let Some((tag, rest)) = line.split_once(' ') {
839 lines.push(String::new());
840 lines.push(format!("**{tag}**"));
841 lines.push(format!("*{rest}*"));
842 } else {
843 lines.push(String::new());
844 lines.push(format!("**{line}**"));
845 }
846 } else {
847 lines.push(line.to_string());
849 }
850 }
851
852 lines.join("\n")
853}
854
855fn build_function_signature(node: &Value) -> Option<String> {
857 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
858 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
859
860 match node_type {
861 "FunctionDefinition" => {
862 let kind = node
863 .get("kind")
864 .and_then(|v| v.as_str())
865 .unwrap_or("function");
866 let visibility = node
867 .get("visibility")
868 .and_then(|v| v.as_str())
869 .unwrap_or("");
870 let state_mutability = node
871 .get("stateMutability")
872 .and_then(|v| v.as_str())
873 .unwrap_or("");
874
875 let params = format_parameters(node.get("parameters"));
876 let returns = format_parameters(node.get("returnParameters"));
877
878 let mut sig = match kind {
879 "constructor" => format!("constructor({params})"),
880 "receive" => "receive() external payable".to_string(),
881 "fallback" => format!("fallback({params})"),
882 _ => format!("function {name}({params})"),
883 };
884
885 if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
886 sig.push_str(&format!(" {visibility}"));
887 }
888 if !state_mutability.is_empty() && state_mutability != "nonpayable" {
889 sig.push_str(&format!(" {state_mutability}"));
890 }
891 if !returns.is_empty() {
892 sig.push_str(&format!(" returns ({returns})"));
893 }
894 Some(sig)
895 }
896 "ModifierDefinition" => {
897 let params = format_parameters(node.get("parameters"));
898 Some(format!("modifier {name}({params})"))
899 }
900 "EventDefinition" => {
901 let params = format_parameters(node.get("parameters"));
902 Some(format!("event {name}({params})"))
903 }
904 "ErrorDefinition" => {
905 let params = format_parameters(node.get("parameters"));
906 Some(format!("error {name}({params})"))
907 }
908 "VariableDeclaration" => {
909 let type_str = node
910 .get("typeDescriptions")
911 .and_then(|v| v.get("typeString"))
912 .and_then(|v| v.as_str())
913 .unwrap_or("unknown");
914 let visibility = node
915 .get("visibility")
916 .and_then(|v| v.as_str())
917 .unwrap_or("");
918 let mutability = node
919 .get("mutability")
920 .and_then(|v| v.as_str())
921 .unwrap_or("");
922
923 let mut sig = type_str.to_string();
924 if !visibility.is_empty() {
925 sig.push_str(&format!(" {visibility}"));
926 }
927 if mutability == "constant" || mutability == "immutable" {
928 sig.push_str(&format!(" {mutability}"));
929 }
930 sig.push_str(&format!(" {name}"));
931 Some(sig)
932 }
933 "ContractDefinition" => {
934 let contract_kind = node
935 .get("contractKind")
936 .and_then(|v| v.as_str())
937 .unwrap_or("contract");
938
939 let mut sig = format!("{contract_kind} {name}");
940
941 if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array())
943 && !bases.is_empty()
944 {
945 let base_names: Vec<&str> = bases
946 .iter()
947 .filter_map(|b| {
948 b.get("baseName")
949 .and_then(|bn| bn.get("name"))
950 .and_then(|n| n.as_str())
951 })
952 .collect();
953 if !base_names.is_empty() {
954 sig.push_str(&format!(" is {}", base_names.join(", ")));
955 }
956 }
957 Some(sig)
958 }
959 "StructDefinition" => {
960 let mut sig = format!("struct {name} {{\n");
961 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
962 for member in members {
963 let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
964 let mtype = member
965 .get("typeDescriptions")
966 .and_then(|v| v.get("typeString"))
967 .and_then(|v| v.as_str())
968 .unwrap_or("?");
969 sig.push_str(&format!(" {mtype} {mname};\n"));
970 }
971 }
972 sig.push('}');
973 Some(sig)
974 }
975 "EnumDefinition" => {
976 let mut sig = format!("enum {name} {{\n");
977 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
978 let names: Vec<&str> = members
979 .iter()
980 .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
981 .collect();
982 for n in &names {
983 sig.push_str(&format!(" {n},\n"));
984 }
985 }
986 sig.push('}');
987 Some(sig)
988 }
989 "UserDefinedValueTypeDefinition" => {
990 let underlying = node
991 .get("underlyingType")
992 .and_then(|v| v.get("typeDescriptions"))
993 .and_then(|v| v.get("typeString"))
994 .and_then(|v| v.as_str())
995 .unwrap_or("unknown");
996 Some(format!("type {name} is {underlying}"))
997 }
998 _ => None,
999 }
1000}
1001
1002fn format_parameters(params_node: Option<&Value>) -> String {
1004 let params_node = match params_node {
1005 Some(v) => v,
1006 None => return String::new(),
1007 };
1008 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
1009 Some(arr) => arr,
1010 None => return String::new(),
1011 };
1012
1013 let parts: Vec<String> = params
1014 .iter()
1015 .map(|p| {
1016 let type_str = p
1017 .get("typeDescriptions")
1018 .and_then(|v| v.get("typeString"))
1019 .and_then(|v| v.as_str())
1020 .unwrap_or("?");
1021 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
1022 let storage = p
1023 .get("storageLocation")
1024 .and_then(|v| v.as_str())
1025 .unwrap_or("default");
1026
1027 if name.is_empty() {
1028 type_str.to_string()
1029 } else if storage != "default" {
1030 format!("{type_str} {storage} {name}")
1031 } else {
1032 format!("{type_str} {name}")
1033 }
1034 })
1035 .collect();
1036
1037 parts.join(", ")
1038}
1039
1040fn build_parameter_strings(params_node: Option<&Value>) -> Vec<String> {
1046 let params_node = match params_node {
1047 Some(v) => v,
1048 None => return vec![],
1049 };
1050 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
1051 Some(arr) => arr,
1052 None => return vec![],
1053 };
1054
1055 params
1056 .iter()
1057 .map(|p| {
1058 let type_str = p
1059 .get("typeDescriptions")
1060 .and_then(|v| v.get("typeString"))
1061 .and_then(|v| v.as_str())
1062 .unwrap_or("?");
1063 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
1064 let storage = p
1065 .get("storageLocation")
1066 .and_then(|v| v.as_str())
1067 .unwrap_or("default");
1068
1069 if name.is_empty() {
1070 type_str.to_string()
1071 } else if storage != "default" {
1072 format!("{type_str} {storage} {name}")
1073 } else {
1074 format!("{type_str} {name}")
1075 }
1076 })
1077 .collect()
1078}
1079
1080fn find_mapping_decl_by_name<'a>(sources: &'a Value, name: &str) -> Option<&'a Value> {
1085 let sources_obj = sources.as_object()?;
1086 for (_path, source_data) in sources_obj {
1087 let ast = source_data.get("ast")?;
1088 let mut stack = vec![ast];
1089 while let Some(node) = stack.pop() {
1090 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration")
1091 && node.get("name").and_then(|v| v.as_str()) == Some(name)
1092 && node
1093 .get("typeName")
1094 .and_then(|t| t.get("nodeType"))
1095 .and_then(|v| v.as_str())
1096 == Some("Mapping")
1097 {
1098 return Some(node);
1099 }
1100 for key in CHILD_KEYS {
1101 if let Some(value) = node.get(key) {
1102 match value {
1103 Value::Array(arr) => stack.extend(arr.iter()),
1104 Value::Object(_) => stack.push(value),
1105 _ => {}
1106 }
1107 }
1108 }
1109 }
1110 }
1111 None
1112}
1113
1114fn mapping_signature_help(sources: &Value, name: &str) -> Option<SignatureHelp> {
1119 let decl = find_mapping_decl_by_name(sources, name)?;
1120 let type_name = decl.get("typeName")?;
1121
1122 let key_type = type_name
1123 .get("keyType")
1124 .and_then(|k| k.get("typeDescriptions"))
1125 .and_then(|t| t.get("typeString"))
1126 .and_then(|v| v.as_str())
1127 .unwrap_or("unknown");
1128
1129 let key_name = type_name
1131 .get("keyName")
1132 .and_then(|v| v.as_str())
1133 .filter(|s| !s.is_empty());
1134
1135 let param_str = if let Some(kn) = key_name {
1136 format!("{} {}", key_type, kn)
1137 } else {
1138 key_type.to_string()
1139 };
1140
1141 let sig_label = format!("{}[{}]", name, param_str);
1142
1143 let param_start = name.len() + 1; let param_end = param_start + param_str.len();
1146
1147 let key_param_name = key_name.unwrap_or("");
1149 let var_name = decl.get("name").and_then(|v| v.as_str()).unwrap_or("");
1150
1151 let _param_doc: Option<String> = None;
1153
1154 let param_info = ParameterInformation {
1155 label: ParameterLabel::LabelOffsets([param_start as u32, param_end as u32]),
1156 documentation: if !key_param_name.is_empty() {
1157 Some(Documentation::MarkupContent(MarkupContent {
1158 kind: MarkupKind::Markdown,
1159 value: format!("`{}` — key for `{}`", key_param_name, var_name),
1160 }))
1161 } else {
1162 None
1163 },
1164 };
1165
1166 let value_type = type_name
1168 .get("valueType")
1169 .and_then(|v| v.get("typeDescriptions"))
1170 .and_then(|t| t.get("typeString"))
1171 .and_then(|v| v.as_str());
1172
1173 let sig_doc = value_type.map(|vt| format!("@returns `{}`", vt));
1174
1175 Some(SignatureHelp {
1176 signatures: vec![SignatureInformation {
1177 label: sig_label,
1178 documentation: sig_doc.map(|doc| {
1179 Documentation::MarkupContent(MarkupContent {
1180 kind: MarkupKind::Markdown,
1181 value: doc,
1182 })
1183 }),
1184 parameters: Some(vec![param_info]),
1185 active_parameter: Some(0),
1186 }],
1187 active_signature: Some(0),
1188 active_parameter: Some(0),
1189 })
1190}
1191
1192pub fn signature_help(
1200 ast_data: &Value,
1201 source_bytes: &[u8],
1202 position: Position,
1203 hint_index: &HintIndex,
1204 doc_index: &DocIndex,
1205) -> Option<SignatureHelp> {
1206 let sources = ast_data.get("sources")?;
1207
1208 let source_str = String::from_utf8_lossy(source_bytes);
1209 let tree = crate::inlay_hints::ts_parse(&source_str)?;
1210 let byte_pos = pos_to_bytes(source_bytes, position);
1211
1212 let ctx =
1214 crate::inlay_hints::ts_find_call_for_signature(tree.root_node(), &source_str, byte_pos)?;
1215
1216 if ctx.is_index_access {
1218 return mapping_signature_help(sources, ctx.name);
1219 }
1220
1221 let (decl_id, skip) = hint_index.values().find_map(|lookup| {
1223 lookup.resolve_callsite_with_skip(ctx.call_start_byte, ctx.name, ctx.arg_count)
1224 })?;
1225
1226 let decl_node = find_node_by_id(sources, NodeId(decl_id))?;
1228
1229 let sig_label = build_function_signature(&decl_node)?;
1231
1232 let param_strings = build_parameter_strings(decl_node.get("parameters"));
1234
1235 let doc_entry = lookup_doc_entry(doc_index, &decl_node, sources);
1237
1238 let params_start = sig_label.find('(')? + 1;
1242 let mut param_infos = Vec::new();
1243 let mut offset = params_start;
1244
1245 for (i, param_str) in param_strings.iter().enumerate() {
1246 let start = offset;
1247 let end = start + param_str.len();
1248
1249 let param_name = decl_node
1251 .get("parameters")
1252 .and_then(|p| p.get("parameters"))
1253 .and_then(|arr| arr.as_array())
1254 .and_then(|arr| arr.get(i))
1255 .and_then(|p| p.get("name"))
1256 .and_then(|v| v.as_str())
1257 .unwrap_or("");
1258
1259 let param_doc = doc_entry.as_ref().and_then(|entry| {
1260 entry
1261 .params
1262 .iter()
1263 .find(|(name, _)| name == param_name)
1264 .map(|(_, desc)| desc.clone())
1265 });
1266
1267 param_infos.push(ParameterInformation {
1268 label: ParameterLabel::LabelOffsets([start as u32, end as u32]),
1269 documentation: param_doc.map(|doc| {
1270 Documentation::MarkupContent(MarkupContent {
1271 kind: MarkupKind::Markdown,
1272 value: doc,
1273 })
1274 }),
1275 });
1276
1277 offset = end + 2;
1279 }
1280
1281 let sig_doc = doc_entry.as_ref().and_then(|entry| {
1283 let mut parts = Vec::new();
1284 if let Some(notice) = &entry.notice {
1285 parts.push(notice.clone());
1286 }
1287 if let Some(details) = &entry.details {
1288 parts.push(format!("*{}*", details));
1289 }
1290 if parts.is_empty() {
1291 None
1292 } else {
1293 Some(parts.join("\n\n"))
1294 }
1295 });
1296
1297 let active_param = (ctx.arg_index + skip) as u32;
1299
1300 Some(SignatureHelp {
1301 signatures: vec![SignatureInformation {
1302 label: sig_label,
1303 documentation: sig_doc.map(|doc| {
1304 Documentation::MarkupContent(MarkupContent {
1305 kind: MarkupKind::Markdown,
1306 value: doc,
1307 })
1308 }),
1309 parameters: Some(param_infos),
1310 active_parameter: Some(active_param),
1311 }],
1312 active_signature: Some(0),
1313 active_parameter: Some(active_param),
1314 })
1315}
1316
1317fn source_has_gas_sentinel(source: &str, decl_node: &Value) -> bool {
1322 let src_str = decl_node.get("src").and_then(|v| v.as_str()).unwrap_or("");
1323 let offset = src_str
1324 .split(':')
1325 .next()
1326 .and_then(|s| s.parse::<usize>().ok())
1327 .unwrap_or(0);
1328
1329 let preceding = &source[..offset.min(source.len())];
1331 for line in preceding.lines().rev().take(10) {
1333 let trimmed = line.trim();
1334 if trimmed.contains(gas::GAS_SENTINEL) {
1335 return true;
1336 }
1337 if !trimmed.is_empty()
1339 && !trimmed.starts_with("///")
1340 && !trimmed.starts_with("//")
1341 && !trimmed.starts_with('*')
1342 && !trimmed.starts_with("/*")
1343 {
1344 break;
1345 }
1346 }
1347 false
1348}
1349
1350fn gas_hover_for_function(
1352 decl_node: &Value,
1353 sources: &Value,
1354 gas_index: &GasIndex,
1355) -> Option<String> {
1356 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1357 if node_type != "FunctionDefinition" {
1358 return None;
1359 }
1360
1361 if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str())
1363 && let Some((_contract, cost)) =
1364 gas::gas_by_selector(gas_index, &FuncSelector::new(selector))
1365 {
1366 return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1367 }
1368
1369 let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
1371 let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1372 let contract_gas = gas_index.get(&contract_key)?;
1373
1374 let prefix = format!("{fn_name}(");
1376 for (sig, cost) in &contract_gas.internal {
1377 if sig.starts_with(&prefix) {
1378 return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1379 }
1380 }
1381
1382 None
1383}
1384
1385fn gas_hover_for_contract(
1387 decl_node: &Value,
1388 sources: &Value,
1389 gas_index: &GasIndex,
1390) -> Option<String> {
1391 let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1392 if node_type != "ContractDefinition" {
1393 return None;
1394 }
1395
1396 let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1397 let contract_gas = gas_index.get(&contract_key)?;
1398
1399 let mut lines = Vec::new();
1400
1401 if !contract_gas.creation.is_empty() {
1403 lines.push("**Deploy Cost**".to_string());
1404 if let Some(cost) = contract_gas.creation.get("totalCost") {
1405 lines.push(format!("- Total: `{}`", gas::format_gas(cost)));
1406 }
1407 if let Some(cost) = contract_gas.creation.get("codeDepositCost") {
1408 lines.push(format!("- Code deposit: `{}`", gas::format_gas(cost)));
1409 }
1410 if let Some(cost) = contract_gas.creation.get("executionCost") {
1411 lines.push(format!("- Execution: `{}`", gas::format_gas(cost)));
1412 }
1413 }
1414
1415 if !contract_gas.external_by_sig.is_empty() {
1417 lines.push(String::new());
1418 lines.push("**Function Gas**".to_string());
1419
1420 let mut fns: Vec<(&MethodId, &String)> = contract_gas.external_by_sig.iter().collect();
1421 fns.sort_by_key(|(k, _)| k.as_str().to_string());
1422
1423 for (sig, cost) in fns {
1424 lines.push(format!("- `{}`: `{}`", sig.name(), gas::format_gas(cost)));
1425 }
1426 }
1427
1428 if lines.is_empty() {
1429 return None;
1430 }
1431
1432 Some(lines.join("\n"))
1433}
1434
1435pub fn hover_info(
1437 ast_data: &Value,
1438 file_uri: &Url,
1439 position: Position,
1440 source_bytes: &[u8],
1441 gas_index: &GasIndex,
1442 doc_index: &DocIndex,
1443 hint_index: &HintIndex,
1444) -> Option<Hover> {
1445 let sources = ast_data.get("sources")?;
1446 let source_id_to_path = ast_data
1447 .get("source_id_to_path")
1448 .and_then(|v| v.as_object())?;
1449
1450 let id_to_path: HashMap<String, String> = source_id_to_path
1451 .iter()
1452 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
1453 .collect();
1454
1455 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
1456
1457 let file_path = file_uri.to_file_path().ok()?;
1459 let file_path_str = file_path.to_str()?;
1460
1461 let abs_path = path_to_abs
1463 .iter()
1464 .find(|(k, _)| file_path_str.ends_with(k.as_str()))
1465 .map(|(_, v)| v.clone())?;
1466
1467 let byte_pos = pos_to_bytes(source_bytes, position);
1468
1469 let node_id = byte_to_decl_via_external_refs(&external_refs, &id_to_path, &abs_path, byte_pos)
1471 .or_else(|| byte_to_id(&nodes, &abs_path, byte_pos))?;
1472
1473 let node_info = nodes
1475 .values()
1476 .find_map(|file_nodes| file_nodes.get(&node_id))?;
1477
1478 let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
1480
1481 let decl_node = find_node_by_id(sources, decl_id)?;
1483
1484 let mut parts: Vec<String> = Vec::new();
1486
1487 if let Some(sig) = build_function_signature(decl_node) {
1489 parts.push(format!("```solidity\n{sig}\n```"));
1490 } else {
1491 if let Some(type_str) = decl_node
1493 .get("typeDescriptions")
1494 .and_then(|v| v.get("typeString"))
1495 .and_then(|v| v.as_str())
1496 {
1497 let name = decl_node.get("name").and_then(|v| v.as_str()).unwrap_or("");
1498 parts.push(format!("```solidity\n{type_str} {name}\n```"));
1499 }
1500 }
1501
1502 if let Some(selector) = extract_selector(decl_node) {
1504 parts.push(format!("Selector: `{}`", selector.to_prefixed()));
1505 }
1506
1507 if !gas_index.is_empty() {
1509 let source_str = String::from_utf8_lossy(source_bytes);
1510 if source_has_gas_sentinel(&source_str, decl_node) {
1511 if let Some(gas_text) = gas_hover_for_function(decl_node, sources, gas_index) {
1512 parts.push(gas_text);
1513 } else if let Some(gas_text) = gas_hover_for_contract(decl_node, sources, gas_index) {
1514 parts.push(gas_text);
1515 }
1516 }
1517 }
1518
1519 if let Some(doc_entry) = lookup_doc_entry(doc_index, decl_node, sources) {
1521 let formatted = format_doc_entry(&doc_entry);
1522 if !formatted.is_empty() {
1523 parts.push(format!("---\n{formatted}"));
1524 }
1525 } else if let Some(doc_text) = extract_documentation(decl_node) {
1526 let inherited_doc = resolve_inheritdoc(sources, decl_node, &doc_text);
1527 let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
1528 if !formatted.is_empty() {
1529 parts.push(format!("---\n{formatted}"));
1530 }
1531 } else if let Some(param_doc) = lookup_param_doc(doc_index, decl_node, sources) {
1532 if !param_doc.is_empty() {
1534 parts.push(format!("---\n{param_doc}"));
1535 }
1536 }
1537
1538 if let Some(hint_lookup) = hint_index.get(&abs_path) {
1543 let source_str = String::from_utf8_lossy(source_bytes);
1544 if let Some(tree) = crate::inlay_hints::ts_parse(&source_str) {
1545 if let Some(ctx) =
1546 crate::inlay_hints::ts_find_call_at_byte(tree.root_node(), &source_str, byte_pos)
1547 {
1548 if let Some(resolved) = hint_lookup.resolve_callsite_param(
1549 ctx.call_start_byte,
1550 ctx.name,
1551 ctx.arg_count,
1552 ctx.arg_index,
1553 ) {
1554 let fn_decl = find_node_by_id(sources, NodeId(resolved.decl_id));
1556 let param_doc = fn_decl.and_then(|decl| {
1557 if let Some(doc_entry) = lookup_doc_entry(doc_index, decl, sources) {
1559 for (pname, pdesc) in &doc_entry.params {
1560 if pname == &resolved.param_name {
1561 return Some(pdesc.clone());
1562 }
1563 }
1564 }
1565 if let Some(doc_text) = extract_documentation(decl) {
1567 let resolved_doc = if doc_text.contains("@inheritdoc") {
1568 resolve_inheritdoc(sources, decl, &doc_text)
1569 } else {
1570 None
1571 };
1572 let text = resolved_doc.as_deref().unwrap_or(&doc_text);
1573 for line in text.lines() {
1574 let trimmed = line.trim().trim_start_matches('*').trim();
1575 if let Some(rest) = trimmed.strip_prefix("@param ") {
1576 if let Some((name, desc)) = rest.split_once(' ') {
1577 if name == resolved.param_name {
1578 return Some(desc.to_string());
1579 }
1580 }
1581 }
1582 }
1583 }
1584 None
1585 });
1586 if let Some(desc) = param_doc {
1587 if !desc.is_empty() {
1588 parts.push(format!("**@param `{}`** — {desc}", resolved.param_name));
1589 }
1590 }
1591 }
1592 }
1593 }
1594 }
1595
1596 if parts.is_empty() {
1597 return None;
1598 }
1599
1600 Some(Hover {
1601 contents: HoverContents::Markup(MarkupContent {
1602 kind: MarkupKind::Markdown,
1603 value: parts.join("\n\n"),
1604 }),
1605 range: None,
1606 })
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611 use super::*;
1612
1613 fn load_test_ast() -> Value {
1614 let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
1615 let raw: Value = serde_json::from_str(&data).expect("valid json");
1616 crate::solc::normalize_forge_output(raw)
1617 }
1618
1619 #[test]
1620 fn test_find_node_by_id_pool_manager() {
1621 let ast = load_test_ast();
1622 let sources = ast.get("sources").unwrap();
1623 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1624 assert_eq!(
1625 node.get("name").and_then(|v| v.as_str()),
1626 Some("PoolManager")
1627 );
1628 assert_eq!(
1629 node.get("nodeType").and_then(|v| v.as_str()),
1630 Some("ContractDefinition")
1631 );
1632 }
1633
1634 #[test]
1635 fn test_find_node_by_id_initialize() {
1636 let ast = load_test_ast();
1637 let sources = ast.get("sources").unwrap();
1638 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1640 assert_eq!(
1641 node.get("name").and_then(|v| v.as_str()),
1642 Some("initialize")
1643 );
1644 }
1645
1646 #[test]
1647 fn test_extract_documentation_object() {
1648 let ast = load_test_ast();
1649 let sources = ast.get("sources").unwrap();
1650 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1652 let doc = extract_documentation(node).unwrap();
1653 assert!(doc.contains("@notice"));
1654 assert!(doc.contains("@param key"));
1655 }
1656
1657 #[test]
1658 fn test_extract_documentation_none() {
1659 let ast = load_test_ast();
1660 let sources = ast.get("sources").unwrap();
1661 let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1663 let _ = extract_documentation(node);
1665 }
1666
1667 #[test]
1668 fn test_format_natspec_notice_and_params() {
1669 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";
1670 let formatted = format_natspec(text, None);
1671 assert!(formatted.contains("Initialize the state"));
1672 assert!(formatted.contains("**Parameters:**"));
1673 assert!(formatted.contains("`key`"));
1674 assert!(formatted.contains("**Returns:**"));
1675 assert!(formatted.contains("`tick`"));
1676 }
1677
1678 #[test]
1679 fn test_format_natspec_inheritdoc() {
1680 let text = "@inheritdoc IPoolManager";
1681 let formatted = format_natspec(text, None);
1682 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
1683 }
1684
1685 #[test]
1686 fn test_format_natspec_dev() {
1687 let text = "@notice Do something\n @dev This is an implementation detail";
1688 let formatted = format_natspec(text, None);
1689 assert!(formatted.contains("Do something"));
1690 assert!(formatted.contains("**@dev**"));
1691 assert!(formatted.contains("*This is an implementation detail*"));
1692 }
1693
1694 #[test]
1695 fn test_format_natspec_custom_tag() {
1696 let text = "@notice Do something\n @custom:security Non-reentrant";
1697 let formatted = format_natspec(text, None);
1698 assert!(formatted.contains("Do something"));
1699 assert!(formatted.contains("**@custom:security**"));
1700 assert!(formatted.contains("*Non-reentrant*"));
1701 }
1702
1703 #[test]
1704 fn test_build_function_signature_initialize() {
1705 let ast = load_test_ast();
1706 let sources = ast.get("sources").unwrap();
1707 let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1708 let sig = build_function_signature(node).unwrap();
1709 assert!(sig.starts_with("function initialize("));
1710 assert!(sig.contains("returns"));
1711 }
1712
1713 #[test]
1714 fn test_build_signature_contract() {
1715 let ast = load_test_ast();
1716 let sources = ast.get("sources").unwrap();
1717 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1718 let sig = build_function_signature(node).unwrap();
1719 assert!(sig.contains("contract PoolManager"));
1720 assert!(sig.contains(" is "));
1721 }
1722
1723 #[test]
1724 fn test_build_signature_struct() {
1725 let ast = load_test_ast();
1726 let sources = ast.get("sources").unwrap();
1727 let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1728 let sig = build_function_signature(node).unwrap();
1729 assert!(sig.starts_with("struct PoolKey"));
1730 assert!(sig.contains('{'));
1731 }
1732
1733 #[test]
1734 fn test_build_signature_error() {
1735 let ast = load_test_ast();
1736 let sources = ast.get("sources").unwrap();
1737 let node = find_node_by_id(sources, NodeId(508)).unwrap();
1739 assert_eq!(
1740 node.get("nodeType").and_then(|v| v.as_str()),
1741 Some("ErrorDefinition")
1742 );
1743 let sig = build_function_signature(node).unwrap();
1744 assert!(sig.starts_with("error "));
1745 }
1746
1747 #[test]
1748 fn test_build_signature_event() {
1749 let ast = load_test_ast();
1750 let sources = ast.get("sources").unwrap();
1751 let node = find_node_by_id(sources, NodeId(8)).unwrap();
1753 assert_eq!(
1754 node.get("nodeType").and_then(|v| v.as_str()),
1755 Some("EventDefinition")
1756 );
1757 let sig = build_function_signature(node).unwrap();
1758 assert!(sig.starts_with("event "));
1759 }
1760
1761 #[test]
1762 fn test_build_signature_variable() {
1763 let ast = load_test_ast();
1764 let sources = ast.get("sources").unwrap();
1765 let pm = find_node_by_id(sources, NodeId(1767)).unwrap();
1768 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
1769 for node in nodes {
1770 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
1771 let sig = build_function_signature(node);
1772 assert!(sig.is_some());
1773 break;
1774 }
1775 }
1776 }
1777 }
1778
1779 #[test]
1780 fn test_pool_manager_has_documentation() {
1781 let ast = load_test_ast();
1782 let sources = ast.get("sources").unwrap();
1783 let node = find_node_by_id(sources, NodeId(59)).unwrap();
1785 let doc = extract_documentation(node).unwrap();
1786 assert!(doc.contains("@notice"));
1787 }
1788
1789 #[test]
1790 fn test_format_parameters_empty() {
1791 let result = format_parameters(None);
1792 assert_eq!(result, "");
1793 }
1794
1795 #[test]
1796 fn test_format_parameters_with_data() {
1797 let params: Value = serde_json::json!({
1798 "parameters": [
1799 {
1800 "name": "key",
1801 "typeDescriptions": { "typeString": "struct PoolKey" },
1802 "storageLocation": "memory"
1803 },
1804 {
1805 "name": "sqrtPriceX96",
1806 "typeDescriptions": { "typeString": "uint160" },
1807 "storageLocation": "default"
1808 }
1809 ]
1810 });
1811 let result = format_parameters(Some(¶ms));
1812 assert!(result.contains("struct PoolKey memory key"));
1813 assert!(result.contains("uint160 sqrtPriceX96"));
1814 }
1815
1816 #[test]
1819 fn test_extract_selector_function() {
1820 let ast = load_test_ast();
1821 let sources = ast.get("sources").unwrap();
1822 let node = find_node_by_id(sources, NodeId(1167)).unwrap();
1824 let selector = extract_selector(node).unwrap();
1825 assert_eq!(selector, Selector::Func(FuncSelector::new("f3cd914c")));
1826 assert_eq!(selector.as_hex(), "f3cd914c");
1827 }
1828
1829 #[test]
1830 fn test_extract_selector_error() {
1831 let ast = load_test_ast();
1832 let sources = ast.get("sources").unwrap();
1833 let node = find_node_by_id(sources, NodeId(508)).unwrap();
1835 let selector = extract_selector(node).unwrap();
1836 assert_eq!(selector, Selector::Func(FuncSelector::new("0d89438e")));
1837 assert_eq!(selector.as_hex(), "0d89438e");
1838 }
1839
1840 #[test]
1841 fn test_extract_selector_event() {
1842 let ast = load_test_ast();
1843 let sources = ast.get("sources").unwrap();
1844 let node = find_node_by_id(sources, NodeId(8)).unwrap();
1846 let selector = extract_selector(node).unwrap();
1847 assert!(matches!(selector, Selector::Event(_)));
1848 assert_eq!(selector.as_hex().len(), 64); }
1850
1851 #[test]
1852 fn test_extract_selector_public_variable() {
1853 let ast = load_test_ast();
1854 let sources = ast.get("sources").unwrap();
1855 let node = find_node_by_id(sources, NodeId(10)).unwrap();
1857 let selector = extract_selector(node).unwrap();
1858 assert_eq!(selector, Selector::Func(FuncSelector::new("8da5cb5b")));
1859 }
1860
1861 #[test]
1862 fn test_extract_selector_internal_function_none() {
1863 let ast = load_test_ast();
1864 let sources = ast.get("sources").unwrap();
1865 let node = find_node_by_id(sources, NodeId(5960)).unwrap();
1867 assert!(extract_selector(node).is_none());
1868 }
1869
1870 #[test]
1873 fn test_resolve_inheritdoc_swap() {
1874 let ast = load_test_ast();
1875 let sources = ast.get("sources").unwrap();
1876 let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1878 let doc_text = extract_documentation(decl).unwrap();
1879 assert!(doc_text.contains("@inheritdoc"));
1880
1881 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1882 assert!(resolved.contains("@notice"));
1883 assert!(resolved.contains("Swap against the given pool"));
1884 }
1885
1886 #[test]
1887 fn test_resolve_inheritdoc_initialize() {
1888 let ast = load_test_ast();
1889 let sources = ast.get("sources").unwrap();
1890 let decl = find_node_by_id(sources, NodeId(881)).unwrap();
1892 let doc_text = extract_documentation(decl).unwrap();
1893
1894 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1895 assert!(resolved.contains("Initialize the state"));
1896 assert!(resolved.contains("@param key"));
1897 }
1898
1899 #[test]
1900 fn test_resolve_inheritdoc_extsload_overload() {
1901 let ast = load_test_ast();
1902 let sources = ast.get("sources").unwrap();
1903
1904 let decl = find_node_by_id(sources, NodeId(442)).unwrap();
1906 let doc_text = extract_documentation(decl).unwrap();
1907 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1908 assert!(resolved.contains("granular pool state"));
1909 assert!(resolved.contains("@param slot"));
1911
1912 let decl2 = find_node_by_id(sources, NodeId(455)).unwrap();
1914 let doc_text2 = extract_documentation(decl2).unwrap();
1915 let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
1916 assert!(resolved2.contains("@param startSlot"));
1917
1918 let decl3 = find_node_by_id(sources, NodeId(467)).unwrap();
1920 let doc_text3 = extract_documentation(decl3).unwrap();
1921 let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
1922 assert!(resolved3.contains("sparse pool state"));
1923 }
1924
1925 #[test]
1926 fn test_resolve_inheritdoc_formats_in_hover() {
1927 let ast = load_test_ast();
1928 let sources = ast.get("sources").unwrap();
1929 let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1931 let doc_text = extract_documentation(decl).unwrap();
1932 let inherited = resolve_inheritdoc(sources, decl, &doc_text);
1933 let formatted = format_natspec(&doc_text, inherited.as_deref());
1934 assert!(!formatted.contains("@inheritdoc"));
1936 assert!(formatted.contains("Swap against the given pool"));
1937 assert!(formatted.contains("**Parameters:**"));
1938 }
1939
1940 #[test]
1943 fn test_param_doc_error_parameter() {
1944 let ast = load_test_ast();
1945 let sources = ast.get("sources").unwrap();
1946 let doc_index = build_doc_index(&ast);
1947
1948 let param_node = find_node_by_id(sources, NodeId(4760)).unwrap();
1950 assert_eq!(
1951 param_node.get("name").and_then(|v| v.as_str()),
1952 Some("sqrtPriceCurrentX96")
1953 );
1954
1955 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1956 assert!(
1957 doc.contains("invalid"),
1958 "should describe the invalid price: {doc}"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_param_doc_error_second_parameter() {
1964 let ast = load_test_ast();
1965 let sources = ast.get("sources").unwrap();
1966 let doc_index = build_doc_index(&ast);
1967
1968 let param_node = find_node_by_id(sources, NodeId(4762)).unwrap();
1970 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1971 assert!(
1972 doc.contains("surpassed price limit"),
1973 "should describe the surpassed limit: {doc}"
1974 );
1975 }
1976
1977 #[test]
1978 fn test_param_doc_function_return_value() {
1979 let ast = load_test_ast();
1980 let sources = ast.get("sources").unwrap();
1981 let doc_index = build_doc_index(&ast);
1982
1983 let param_node = find_node_by_id(sources, NodeId(4994)).unwrap();
1985 assert_eq!(
1986 param_node.get("name").and_then(|v| v.as_str()),
1987 Some("delta")
1988 );
1989
1990 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1991 assert!(
1992 doc.contains("deltas of the token balances"),
1993 "should have return doc: {doc}"
1994 );
1995 }
1996
1997 #[test]
1998 fn test_param_doc_function_input_parameter() {
1999 let ast = load_test_ast();
2000 let sources = ast.get("sources").unwrap();
2001 let doc_index = build_doc_index(&ast);
2002
2003 let fn_node = find_node_by_id(sources, NodeId(5310)).unwrap();
2006 let params_arr = fn_node
2007 .get("parameters")
2008 .and_then(|p| p.get("parameters"))
2009 .and_then(|p| p.as_array())
2010 .unwrap();
2011 let params_param = params_arr
2012 .iter()
2013 .find(|p| p.get("name").and_then(|v| v.as_str()) == Some("params"))
2014 .unwrap();
2015
2016 let doc = lookup_param_doc(&doc_index, params_param, sources).unwrap();
2017 assert!(
2018 doc.contains("position details"),
2019 "should have param doc: {doc}"
2020 );
2021 }
2022
2023 #[test]
2024 fn test_param_doc_inherited_function_via_docindex() {
2025 let ast = load_test_ast();
2026 let sources = ast.get("sources").unwrap();
2027 let doc_index = build_doc_index(&ast);
2028
2029 let param_node = find_node_by_id(sources, NodeId(1029)).unwrap();
2032 assert_eq!(param_node.get("name").and_then(|v| v.as_str()), Some("key"));
2033
2034 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
2035 assert!(
2036 doc.contains("pool to swap"),
2037 "should have inherited param doc: {doc}"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_param_doc_non_parameter_returns_none() {
2043 let ast = load_test_ast();
2044 let sources = ast.get("sources").unwrap();
2045 let doc_index = build_doc_index(&ast);
2046
2047 let node = find_node_by_id(sources, NodeId(1767)).unwrap();
2049 assert!(lookup_param_doc(&doc_index, node, sources).is_none());
2050 }
2051
2052 fn load_solc_fixture() -> Value {
2055 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
2056 let raw: Value = serde_json::from_str(&data).expect("valid json");
2057 crate::solc::normalize_solc_output(raw, None)
2058 }
2059
2060 #[test]
2061 fn test_doc_index_is_not_empty() {
2062 let ast = load_solc_fixture();
2063 let index = build_doc_index(&ast);
2064 assert!(!index.is_empty(), "DocIndex should contain entries");
2065 }
2066
2067 #[test]
2068 fn test_doc_index_has_contract_entries() {
2069 let ast = load_solc_fixture();
2070 let index = build_doc_index(&ast);
2071
2072 let pm_keys: Vec<_> = index
2074 .keys()
2075 .filter(|k| matches!(k, DocKey::Contract(s) if s.contains("PoolManager")))
2076 .collect();
2077 assert!(
2078 !pm_keys.is_empty(),
2079 "should have a Contract entry for PoolManager"
2080 );
2081
2082 let pm_key = DocKey::Contract(
2083 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2084 );
2085 let entry = index.get(&pm_key).expect("PoolManager contract entry");
2086 assert_eq!(entry.title.as_deref(), Some("PoolManager"));
2087 assert_eq!(
2088 entry.notice.as_deref(),
2089 Some("Holds the state for all pools")
2090 );
2091 }
2092
2093 #[test]
2094 fn test_doc_index_has_function_by_selector() {
2095 let ast = load_solc_fixture();
2096 let index = build_doc_index(&ast);
2097
2098 let init_key = DocKey::Func(FuncSelector::new("6276cbbe"));
2100 let entry = index
2101 .get(&init_key)
2102 .expect("should have initialize by selector");
2103 assert_eq!(
2104 entry.notice.as_deref(),
2105 Some("Initialize the state for a given pool ID")
2106 );
2107 assert!(
2108 entry
2109 .details
2110 .as_deref()
2111 .unwrap_or("")
2112 .contains("MAX_SWAP_FEE"),
2113 "devdoc details should mention MAX_SWAP_FEE"
2114 );
2115 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
2117 assert!(param_names.contains(&"key"), "should have param 'key'");
2118 assert!(
2119 param_names.contains(&"sqrtPriceX96"),
2120 "should have param 'sqrtPriceX96'"
2121 );
2122 let return_names: Vec<&str> = entry.returns.iter().map(|(n, _)| n.as_str()).collect();
2124 assert!(return_names.contains(&"tick"), "should have return 'tick'");
2125 }
2126
2127 #[test]
2128 fn test_doc_index_swap_by_selector() {
2129 let ast = load_solc_fixture();
2130 let index = build_doc_index(&ast);
2131
2132 let swap_key = DocKey::Func(FuncSelector::new("f3cd914c"));
2134 let entry = index.get(&swap_key).expect("should have swap by selector");
2135 assert!(
2136 entry
2137 .notice
2138 .as_deref()
2139 .unwrap_or("")
2140 .contains("Swap against the given pool"),
2141 "swap notice should describe swapping"
2142 );
2143 assert!(
2145 !entry.params.is_empty(),
2146 "swap should have param documentation"
2147 );
2148 }
2149
2150 #[test]
2151 fn test_doc_index_settle_by_selector() {
2152 let ast = load_solc_fixture();
2153 let index = build_doc_index(&ast);
2154
2155 let key = DocKey::Func(FuncSelector::new("11da60b4"));
2157 let entry = index.get(&key).expect("should have settle by selector");
2158 assert!(
2159 entry.notice.is_some(),
2160 "settle should have a notice from userdoc"
2161 );
2162 }
2163
2164 #[test]
2165 fn test_doc_index_has_error_entries() {
2166 let ast = load_solc_fixture();
2167 let index = build_doc_index(&ast);
2168
2169 let selector = compute_selector("AlreadyUnlocked()");
2171 let key = DocKey::Func(FuncSelector::new(&selector));
2172 let entry = index.get(&key).expect("should have AlreadyUnlocked error");
2173 assert!(
2174 entry
2175 .notice
2176 .as_deref()
2177 .unwrap_or("")
2178 .contains("already unlocked"),
2179 "AlreadyUnlocked notice: {:?}",
2180 entry.notice
2181 );
2182 }
2183
2184 #[test]
2185 fn test_doc_index_error_with_params() {
2186 let ast = load_solc_fixture();
2187 let index = build_doc_index(&ast);
2188
2189 let selector = compute_selector("CurrenciesOutOfOrderOrEqual(address,address)");
2191 let key = DocKey::Func(FuncSelector::new(&selector));
2192 let entry = index
2193 .get(&key)
2194 .expect("should have CurrenciesOutOfOrderOrEqual error");
2195 assert!(entry.notice.is_some(), "error should have notice");
2196 }
2197
2198 #[test]
2199 fn test_doc_index_has_event_entries() {
2200 let ast = load_solc_fixture();
2201 let index = build_doc_index(&ast);
2202
2203 let event_count = index
2205 .keys()
2206 .filter(|k| matches!(k, DocKey::Event(_)))
2207 .count();
2208 assert!(event_count > 0, "should have event entries in the DocIndex");
2209 }
2210
2211 #[test]
2212 fn test_doc_index_swap_event() {
2213 let ast = load_solc_fixture();
2214 let index = build_doc_index(&ast);
2215
2216 let topic =
2218 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2219 let key = DocKey::Event(EventSelector::new(&topic));
2220 let entry = index.get(&key).expect("should have Swap event");
2221
2222 assert!(
2224 entry
2225 .notice
2226 .as_deref()
2227 .unwrap_or("")
2228 .contains("swaps between currency0 and currency1"),
2229 "Swap event notice: {:?}",
2230 entry.notice
2231 );
2232
2233 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
2235 assert!(
2236 param_names.contains(&"amount0"),
2237 "should have param 'amount0'"
2238 );
2239 assert!(
2240 param_names.contains(&"sender"),
2241 "should have param 'sender'"
2242 );
2243 assert!(param_names.contains(&"id"), "should have param 'id'");
2244 }
2245
2246 #[test]
2247 fn test_doc_index_initialize_event() {
2248 let ast = load_solc_fixture();
2249 let index = build_doc_index(&ast);
2250
2251 let topic = compute_event_topic(
2252 "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)",
2253 );
2254 let key = DocKey::Event(EventSelector::new(&topic));
2255 let entry = index.get(&key).expect("should have Initialize event");
2256 assert!(
2257 !entry.params.is_empty(),
2258 "Initialize event should have param docs"
2259 );
2260 }
2261
2262 #[test]
2263 fn test_doc_index_no_state_variables_for_pool_manager() {
2264 let ast = load_solc_fixture();
2265 let index = build_doc_index(&ast);
2266
2267 let sv_count = index
2269 .keys()
2270 .filter(|k| matches!(k, DocKey::StateVar(s) if s.contains("PoolManager")))
2271 .count();
2272 assert_eq!(
2273 sv_count, 0,
2274 "PoolManager should have no state variable doc entries"
2275 );
2276 }
2277
2278 #[test]
2279 fn test_doc_index_multiple_contracts() {
2280 let ast = load_solc_fixture();
2281 let index = build_doc_index(&ast);
2282
2283 let contract_count = index
2285 .keys()
2286 .filter(|k| matches!(k, DocKey::Contract(_)))
2287 .count();
2288 assert!(
2289 contract_count >= 5,
2290 "should have at least 5 contract-level entries, got {contract_count}"
2291 );
2292 }
2293
2294 #[test]
2295 fn test_doc_index_func_key_count() {
2296 let ast = load_solc_fixture();
2297 let index = build_doc_index(&ast);
2298
2299 let func_count = index
2300 .keys()
2301 .filter(|k| matches!(k, DocKey::Func(_)))
2302 .count();
2303 assert!(
2305 func_count >= 30,
2306 "should have at least 30 Func entries (methods + errors), got {func_count}"
2307 );
2308 }
2309
2310 #[test]
2311 fn test_doc_index_format_initialize_entry() {
2312 let ast = load_solc_fixture();
2313 let index = build_doc_index(&ast);
2314
2315 let key = DocKey::Func(FuncSelector::new("6276cbbe"));
2316 let entry = index.get(&key).expect("initialize entry");
2317 let formatted = format_doc_entry(entry);
2318
2319 assert!(
2320 formatted.contains("Initialize the state for a given pool ID"),
2321 "formatted should include notice"
2322 );
2323 assert!(
2324 formatted.contains("**@dev**"),
2325 "formatted should include dev section"
2326 );
2327 assert!(
2328 formatted.contains("**Parameters:**"),
2329 "formatted should include parameters"
2330 );
2331 assert!(
2332 formatted.contains("`key`"),
2333 "formatted should include key param"
2334 );
2335 assert!(
2336 formatted.contains("**Returns:**"),
2337 "formatted should include returns"
2338 );
2339 assert!(
2340 formatted.contains("`tick`"),
2341 "formatted should include tick return"
2342 );
2343 }
2344
2345 #[test]
2346 fn test_doc_index_format_contract_entry() {
2347 let ast = load_solc_fixture();
2348 let index = build_doc_index(&ast);
2349
2350 let key = DocKey::Contract(
2351 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2352 );
2353 let entry = index.get(&key).expect("PoolManager contract entry");
2354 let formatted = format_doc_entry(entry);
2355
2356 assert!(
2357 formatted.contains("**PoolManager**"),
2358 "should include bold title"
2359 );
2360 assert!(
2361 formatted.contains("Holds the state for all pools"),
2362 "should include notice"
2363 );
2364 }
2365
2366 #[test]
2367 fn test_doc_index_inherited_docs_resolved() {
2368 let ast = load_solc_fixture();
2369 let index = build_doc_index(&ast);
2370
2371 let key = DocKey::Func(FuncSelector::new("f3cd914c"));
2375 let entry = index.get(&key).expect("swap entry");
2376 let notice = entry.notice.as_deref().unwrap_or("");
2378 assert!(
2379 !notice.contains("@inheritdoc"),
2380 "userdoc/devdoc should have resolved inherited docs, not raw @inheritdoc"
2381 );
2382 }
2383
2384 #[test]
2385 fn test_compute_selector_known_values() {
2386 let sel = compute_selector("AlreadyUnlocked()");
2388 assert_eq!(sel.len(), 8, "selector should be 8 hex chars");
2389
2390 let init_sel =
2392 compute_selector("initialize((address,address,uint24,int24,address),uint160)");
2393 assert_eq!(
2394 init_sel, "6276cbbe",
2395 "computed initialize selector should match evm.methodIdentifiers"
2396 );
2397 }
2398
2399 #[test]
2400 fn test_compute_event_topic_length() {
2401 let topic =
2402 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2403 assert_eq!(
2404 topic.len(),
2405 64,
2406 "event topic should be 64 hex chars (32 bytes)"
2407 );
2408 }
2409
2410 #[test]
2411 fn test_doc_index_error_count_poolmanager() {
2412 let ast = load_solc_fixture();
2413 let index = build_doc_index(&ast);
2414
2415 let error_sigs = [
2418 "AlreadyUnlocked()",
2419 "CurrenciesOutOfOrderOrEqual(address,address)",
2420 "CurrencyNotSettled()",
2421 "InvalidCaller()",
2422 "ManagerLocked()",
2423 "MustClearExactPositiveDelta()",
2424 "NonzeroNativeValue()",
2425 "PoolNotInitialized()",
2426 "ProtocolFeeCurrencySynced()",
2427 "ProtocolFeeTooLarge(uint24)",
2428 "SwapAmountCannotBeZero()",
2429 "TickSpacingTooLarge(int24)",
2430 "TickSpacingTooSmall(int24)",
2431 "UnauthorizedDynamicLPFeeUpdate()",
2432 ];
2433 let mut found = 0;
2434 for sig in &error_sigs {
2435 let selector = compute_selector(sig);
2436 let key = DocKey::Func(FuncSelector::new(&selector));
2437 if index.contains_key(&key) {
2438 found += 1;
2439 }
2440 }
2441 assert_eq!(
2442 found,
2443 error_sigs.len(),
2444 "all 14 PoolManager errors should be in the DocIndex"
2445 );
2446 }
2447
2448 #[test]
2449 fn test_doc_index_extsload_overloads_have_different_selectors() {
2450 let ast = load_solc_fixture();
2451 let index = build_doc_index(&ast);
2452
2453 let sel1 = DocKey::Func(FuncSelector::new("1e2eaeaf"));
2458 let sel2 = DocKey::Func(FuncSelector::new("35fd631a"));
2459 let sel3 = DocKey::Func(FuncSelector::new("dbd035ff"));
2460
2461 assert!(index.contains_key(&sel1), "extsload(bytes32) should exist");
2462 assert!(
2463 index.contains_key(&sel2),
2464 "extsload(bytes32,uint256) should exist"
2465 );
2466 assert!(
2467 index.contains_key(&sel3),
2468 "extsload(bytes32[]) should exist"
2469 );
2470 }
2471
2472 #[test]
2473 fn test_build_parameter_strings_basic() {
2474 let node: Value = serde_json::json!({
2475 "parameters": {
2476 "parameters": [
2477 {
2478 "name": "amount",
2479 "typeDescriptions": { "typeString": "uint256" },
2480 "storageLocation": "default"
2481 },
2482 {
2483 "name": "tax",
2484 "typeDescriptions": { "typeString": "uint16" },
2485 "storageLocation": "default"
2486 }
2487 ]
2488 }
2489 });
2490 let params = build_parameter_strings(Some(&node.get("parameters").unwrap()));
2491 assert_eq!(params, vec!["uint256 amount", "uint16 tax"]);
2492 }
2493
2494 #[test]
2495 fn test_build_parameter_strings_with_storage() {
2496 let node: Value = serde_json::json!({
2497 "parameters": {
2498 "parameters": [
2499 {
2500 "name": "key",
2501 "typeDescriptions": { "typeString": "struct PoolKey" },
2502 "storageLocation": "calldata"
2503 }
2504 ]
2505 }
2506 });
2507 let params = build_parameter_strings(Some(&node.get("parameters").unwrap()));
2508 assert_eq!(params, vec!["struct PoolKey calldata key"]);
2509 }
2510
2511 #[test]
2512 fn test_build_parameter_strings_empty() {
2513 let node: Value = serde_json::json!({
2514 "parameters": { "parameters": [] }
2515 });
2516 let params = build_parameter_strings(Some(&node.get("parameters").unwrap()));
2517 assert!(params.is_empty());
2518 }
2519
2520 #[test]
2521 fn test_build_parameter_strings_unnamed() {
2522 let node: Value = serde_json::json!({
2523 "parameters": {
2524 "parameters": [
2525 {
2526 "name": "",
2527 "typeDescriptions": { "typeString": "uint256" },
2528 "storageLocation": "default"
2529 }
2530 ]
2531 }
2532 });
2533 let params = build_parameter_strings(Some(&node.get("parameters").unwrap()));
2534 assert_eq!(params, vec!["uint256"]);
2535 }
2536
2537 #[test]
2538 fn test_signature_help_parameter_offsets() {
2539 let label = "function addTax(uint256 amount, uint16 tax, uint16 base)";
2541 let param_strings = vec![
2542 "uint256 amount".to_string(),
2543 "uint16 tax".to_string(),
2544 "uint16 base".to_string(),
2545 ];
2546
2547 let params_start = label.find('(').unwrap() + 1;
2548 let mut offsets = Vec::new();
2549 let mut offset = params_start;
2550 for param_str in ¶m_strings {
2551 let start = offset;
2552 let end = start + param_str.len();
2553 offsets.push((start, end));
2554 offset = end + 2; }
2556
2557 assert_eq!(&label[offsets[0].0..offsets[0].1], "uint256 amount");
2559 assert_eq!(&label[offsets[1].0..offsets[1].1], "uint16 tax");
2560 assert_eq!(&label[offsets[2].0..offsets[2].1], "uint16 base");
2561 }
2562
2563 #[test]
2564 fn test_find_mapping_decl_by_name_pools() {
2565 let ast = load_test_ast();
2566 let sources = ast.get("sources").unwrap();
2567 let decl = find_mapping_decl_by_name(sources, "_pools").unwrap();
2568 assert_eq!(decl.get("name").and_then(|v| v.as_str()), Some("_pools"));
2569 assert_eq!(
2570 decl.get("typeName")
2571 .and_then(|t| t.get("nodeType"))
2572 .and_then(|v| v.as_str()),
2573 Some("Mapping")
2574 );
2575 }
2576
2577 #[test]
2578 fn test_find_mapping_decl_by_name_not_found() {
2579 let ast = load_test_ast();
2580 let sources = ast.get("sources").unwrap();
2581 assert!(find_mapping_decl_by_name(sources, "nonexistent").is_none());
2582 }
2583
2584 #[test]
2585 fn test_mapping_signature_help_pools() {
2586 let ast = load_test_ast();
2587 let sources = ast.get("sources").unwrap();
2588 let help = mapping_signature_help(sources, "_pools").unwrap();
2589
2590 assert_eq!(help.signatures.len(), 1);
2591 let sig = &help.signatures[0];
2592 assert_eq!(sig.label, "_pools[PoolId id]");
2594 assert_eq!(sig.active_parameter, Some(0));
2595
2596 let params = sig.parameters.as_ref().unwrap();
2598 assert_eq!(params.len(), 1);
2599 if let ParameterLabel::LabelOffsets([start, end]) = params[0].label {
2600 assert_eq!(&sig.label[start as usize..end as usize], "PoolId id");
2601 } else {
2602 panic!("expected LabelOffsets");
2603 }
2604
2605 let doc = sig.documentation.as_ref().unwrap();
2607 if let Documentation::MarkupContent(mc) = doc {
2608 assert!(mc.value.contains("struct Pool.State"));
2609 }
2610 }
2611
2612 #[test]
2613 fn test_mapping_signature_help_protocol_fees() {
2614 let ast = load_test_ast();
2615 let sources = ast.get("sources").unwrap();
2616 let help = mapping_signature_help(sources, "protocolFeesAccrued").unwrap();
2617 let sig = &help.signatures[0];
2618 assert_eq!(sig.label, "protocolFeesAccrued[Currency currency]");
2619 }
2620
2621 #[test]
2622 fn test_mapping_signature_help_non_mapping() {
2623 let ast = load_test_ast();
2624 let sources = ast.get("sources").unwrap();
2625 assert!(mapping_signature_help(sources, "owner").is_none());
2627 }
2628}