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 && let Some(ctx) =
1546 crate::inlay_hints::ts_find_call_at_byte(tree.root_node(), &source_str, byte_pos)
1547 && let Some(resolved) = hint_lookup.resolve_callsite_param(
1548 ctx.call_start_byte,
1549 ctx.name,
1550 ctx.arg_count,
1551 ctx.arg_index,
1552 )
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 && let Some((name, desc)) = rest.split_once(' ')
1577 && name == resolved.param_name
1578 {
1579 return Some(desc.to_string());
1580 }
1581 }
1582 }
1583 None
1584 });
1585 if let Some(desc) = param_doc
1586 && !desc.is_empty()
1587 {
1588 parts.push(format!("**@param `{}`** — {desc}", resolved.param_name));
1589 }
1590 }
1591 }
1592
1593 if parts.is_empty() {
1594 return None;
1595 }
1596
1597 Some(Hover {
1598 contents: HoverContents::Markup(MarkupContent {
1599 kind: MarkupKind::Markdown,
1600 value: parts.join("\n\n"),
1601 }),
1602 range: None,
1603 })
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608 use super::*;
1609
1610 fn load_test_ast() -> Value {
1611 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
1612 let raw: Value = serde_json::from_str(&data).expect("valid json");
1613 crate::solc::normalize_solc_output(raw, None)
1614 }
1615
1616 #[test]
1617 fn test_find_node_by_id_pool_manager() {
1618 let ast = load_test_ast();
1619 let sources = ast.get("sources").unwrap();
1620 let node = find_node_by_id(sources, NodeId(1216)).unwrap();
1621 assert_eq!(
1622 node.get("name").and_then(|v| v.as_str()),
1623 Some("PoolManager")
1624 );
1625 assert_eq!(
1626 node.get("nodeType").and_then(|v| v.as_str()),
1627 Some("ContractDefinition")
1628 );
1629 }
1630
1631 #[test]
1632 fn test_find_node_by_id_initialize() {
1633 let ast = load_test_ast();
1634 let sources = ast.get("sources").unwrap();
1635 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1637 assert_eq!(
1638 node.get("name").and_then(|v| v.as_str()),
1639 Some("initialize")
1640 );
1641 }
1642
1643 #[test]
1644 fn test_extract_documentation_object() {
1645 let ast = load_test_ast();
1646 let sources = ast.get("sources").unwrap();
1647 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1649 let doc = extract_documentation(node).unwrap();
1650 assert!(doc.contains("@notice"));
1651 assert!(doc.contains("@param key"));
1652 }
1653
1654 #[test]
1655 fn test_extract_documentation_none() {
1656 let ast = load_test_ast();
1657 let sources = ast.get("sources").unwrap();
1658 let node = find_node_by_id(sources, NodeId(6871)).unwrap();
1660 let _ = extract_documentation(node);
1662 }
1663
1664 #[test]
1665 fn test_format_natspec_notice_and_params() {
1666 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";
1667 let formatted = format_natspec(text, None);
1668 assert!(formatted.contains("Initialize the state"));
1669 assert!(formatted.contains("**Parameters:**"));
1670 assert!(formatted.contains("`key`"));
1671 assert!(formatted.contains("**Returns:**"));
1672 assert!(formatted.contains("`tick`"));
1673 }
1674
1675 #[test]
1676 fn test_format_natspec_inheritdoc() {
1677 let text = "@inheritdoc IPoolManager";
1678 let formatted = format_natspec(text, None);
1679 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
1680 }
1681
1682 #[test]
1683 fn test_format_natspec_dev() {
1684 let text = "@notice Do something\n @dev This is an implementation detail";
1685 let formatted = format_natspec(text, None);
1686 assert!(formatted.contains("Do something"));
1687 assert!(formatted.contains("**@dev**"));
1688 assert!(formatted.contains("*This is an implementation detail*"));
1689 }
1690
1691 #[test]
1692 fn test_format_natspec_custom_tag() {
1693 let text = "@notice Do something\n @custom:security Non-reentrant";
1694 let formatted = format_natspec(text, None);
1695 assert!(formatted.contains("Do something"));
1696 assert!(formatted.contains("**@custom:security**"));
1697 assert!(formatted.contains("*Non-reentrant*"));
1698 }
1699
1700 #[test]
1701 fn test_build_function_signature_initialize() {
1702 let ast = load_test_ast();
1703 let sources = ast.get("sources").unwrap();
1704 let node = find_node_by_id(sources, NodeId(2003)).unwrap();
1705 let sig = build_function_signature(node).unwrap();
1706 assert!(sig.starts_with("function initialize("));
1707 assert!(sig.contains("returns"));
1708 }
1709
1710 #[test]
1711 fn test_build_signature_contract() {
1712 let ast = load_test_ast();
1713 let sources = ast.get("sources").unwrap();
1714 let node = find_node_by_id(sources, NodeId(1216)).unwrap();
1715 let sig = build_function_signature(node).unwrap();
1716 assert!(sig.contains("contract PoolManager"));
1717 assert!(sig.contains(" is "));
1718 }
1719
1720 #[test]
1721 fn test_build_signature_struct() {
1722 let ast = load_test_ast();
1723 let sources = ast.get("sources").unwrap();
1724 let node = find_node_by_id(sources, NodeId(6871)).unwrap();
1725 let sig = build_function_signature(node).unwrap();
1726 assert!(sig.starts_with("struct PoolKey"));
1727 assert!(sig.contains('{'));
1728 }
1729
1730 #[test]
1731 fn test_build_signature_error() {
1732 let ast = load_test_ast();
1733 let sources = ast.get("sources").unwrap();
1734 let node = find_node_by_id(sources, NodeId(1372)).unwrap();
1736 assert_eq!(
1737 node.get("nodeType").and_then(|v| v.as_str()),
1738 Some("ErrorDefinition")
1739 );
1740 let sig = build_function_signature(node).unwrap();
1741 assert!(sig.starts_with("error "));
1742 }
1743
1744 #[test]
1745 fn test_build_signature_event() {
1746 let ast = load_test_ast();
1747 let sources = ast.get("sources").unwrap();
1748 let node = find_node_by_id(sources, NodeId(7404)).unwrap();
1750 assert_eq!(
1751 node.get("nodeType").and_then(|v| v.as_str()),
1752 Some("EventDefinition")
1753 );
1754 let sig = build_function_signature(node).unwrap();
1755 assert!(sig.starts_with("event "));
1756 }
1757
1758 #[test]
1759 fn test_build_signature_variable() {
1760 let ast = load_test_ast();
1761 let sources = ast.get("sources").unwrap();
1762 let pm = find_node_by_id(sources, NodeId(1216)).unwrap();
1765 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
1766 for node in nodes {
1767 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
1768 let sig = build_function_signature(node);
1769 assert!(sig.is_some());
1770 break;
1771 }
1772 }
1773 }
1774 }
1775
1776 #[test]
1777 fn test_pool_manager_has_documentation() {
1778 let ast = load_test_ast();
1779 let sources = ast.get("sources").unwrap();
1780 let node = find_node_by_id(sources, NodeId(7455)).unwrap();
1782 let doc = extract_documentation(node).unwrap();
1783 assert!(doc.contains("@notice"));
1784 }
1785
1786 #[test]
1787 fn test_format_parameters_empty() {
1788 let result = format_parameters(None);
1789 assert_eq!(result, "");
1790 }
1791
1792 #[test]
1793 fn test_format_parameters_with_data() {
1794 let params: Value = serde_json::json!({
1795 "parameters": [
1796 {
1797 "name": "key",
1798 "typeDescriptions": { "typeString": "struct PoolKey" },
1799 "storageLocation": "memory"
1800 },
1801 {
1802 "name": "sqrtPriceX96",
1803 "typeDescriptions": { "typeString": "uint160" },
1804 "storageLocation": "default"
1805 }
1806 ]
1807 });
1808 let result = format_parameters(Some(¶ms));
1809 assert!(result.contains("struct PoolKey memory key"));
1810 assert!(result.contains("uint160 sqrtPriceX96"));
1811 }
1812
1813 #[test]
1816 fn test_extract_selector_function() {
1817 let ast = load_test_ast();
1818 let sources = ast.get("sources").unwrap();
1819 let node = find_node_by_id(sources, NodeId(616)).unwrap();
1821 let selector = extract_selector(node).unwrap();
1822 assert_eq!(selector, Selector::Func(FuncSelector::new("f3cd914c")));
1823 assert_eq!(selector.as_hex(), "f3cd914c");
1824 }
1825
1826 #[test]
1827 fn test_extract_selector_error() {
1828 let ast = load_test_ast();
1829 let sources = ast.get("sources").unwrap();
1830 let node = find_node_by_id(sources, NodeId(1372)).unwrap();
1832 let selector = extract_selector(node).unwrap();
1833 assert_eq!(selector, Selector::Func(FuncSelector::new("0d89438e")));
1834 assert_eq!(selector.as_hex(), "0d89438e");
1835 }
1836
1837 #[test]
1838 fn test_extract_selector_event() {
1839 let ast = load_test_ast();
1840 let sources = ast.get("sources").unwrap();
1841 let node = find_node_by_id(sources, NodeId(7404)).unwrap();
1843 let selector = extract_selector(node).unwrap();
1844 assert!(matches!(selector, Selector::Event(_)));
1845 assert_eq!(selector.as_hex().len(), 64); }
1847
1848 #[test]
1849 fn test_extract_selector_public_variable() {
1850 let ast = load_test_ast();
1851 let sources = ast.get("sources").unwrap();
1852 let node = find_node_by_id(sources, NodeId(7406)).unwrap();
1854 let selector = extract_selector(node).unwrap();
1855 assert_eq!(selector, Selector::Func(FuncSelector::new("8da5cb5b")));
1856 }
1857
1858 #[test]
1859 fn test_extract_selector_internal_function_none() {
1860 let ast = load_test_ast();
1861 let sources = ast.get("sources").unwrap();
1862 let node = find_node_by_id(sources, NodeId(5021)).unwrap();
1864 assert!(extract_selector(node).is_none());
1865 }
1866
1867 #[test]
1870 fn test_resolve_inheritdoc_swap() {
1871 let ast = load_test_ast();
1872 let sources = ast.get("sources").unwrap();
1873 let decl = find_node_by_id(sources, NodeId(616)).unwrap();
1875 let doc_text = extract_documentation(decl).unwrap();
1876 assert!(doc_text.contains("@inheritdoc"));
1877
1878 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1879 assert!(resolved.contains("@notice"));
1880 assert!(resolved.contains("Swap against the given pool"));
1881 }
1882
1883 #[test]
1884 fn test_resolve_inheritdoc_initialize() {
1885 let ast = load_test_ast();
1886 let sources = ast.get("sources").unwrap();
1887 let decl = find_node_by_id(sources, NodeId(330)).unwrap();
1889 let doc_text = extract_documentation(decl).unwrap();
1890
1891 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1892 assert!(resolved.contains("Initialize the state"));
1893 assert!(resolved.contains("@param key"));
1894 }
1895
1896 #[test]
1897 fn test_resolve_inheritdoc_extsload_overload() {
1898 let ast = load_test_ast();
1899 let sources = ast.get("sources").unwrap();
1900
1901 let decl = find_node_by_id(sources, NodeId(1306)).unwrap();
1903 let doc_text = extract_documentation(decl).unwrap();
1904 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1905 assert!(resolved.contains("granular pool state"));
1906 assert!(resolved.contains("@param slot"));
1908
1909 let decl2 = find_node_by_id(sources, NodeId(1319)).unwrap();
1911 let doc_text2 = extract_documentation(decl2).unwrap();
1912 let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
1913 assert!(resolved2.contains("@param startSlot"));
1914
1915 let decl3 = find_node_by_id(sources, NodeId(1331)).unwrap();
1917 let doc_text3 = extract_documentation(decl3).unwrap();
1918 let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
1919 assert!(resolved3.contains("sparse pool state"));
1920 }
1921
1922 #[test]
1923 fn test_resolve_inheritdoc_formats_in_hover() {
1924 let ast = load_test_ast();
1925 let sources = ast.get("sources").unwrap();
1926 let decl = find_node_by_id(sources, NodeId(616)).unwrap();
1928 let doc_text = extract_documentation(decl).unwrap();
1929 let inherited = resolve_inheritdoc(sources, decl, &doc_text);
1930 let formatted = format_natspec(&doc_text, inherited.as_deref());
1931 assert!(!formatted.contains("@inheritdoc"));
1933 assert!(formatted.contains("Swap against the given pool"));
1934 assert!(formatted.contains("**Parameters:**"));
1935 }
1936
1937 #[test]
1940 fn test_param_doc_error_parameter() {
1941 let ast = load_test_ast();
1942 let sources = ast.get("sources").unwrap();
1943 let doc_index = build_doc_index(&ast);
1944
1945 let param_node = find_node_by_id(sources, NodeId(3821)).unwrap();
1947 assert_eq!(
1948 param_node.get("name").and_then(|v| v.as_str()),
1949 Some("sqrtPriceCurrentX96")
1950 );
1951
1952 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1953 assert!(
1954 doc.contains("invalid"),
1955 "should describe the invalid price: {doc}"
1956 );
1957 }
1958
1959 #[test]
1960 fn test_param_doc_error_second_parameter() {
1961 let ast = load_test_ast();
1962 let sources = ast.get("sources").unwrap();
1963 let doc_index = build_doc_index(&ast);
1964
1965 let param_node = find_node_by_id(sources, NodeId(3823)).unwrap();
1967 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1968 assert!(
1969 doc.contains("surpassed price limit"),
1970 "should describe the surpassed limit: {doc}"
1971 );
1972 }
1973
1974 #[test]
1975 fn test_param_doc_function_return_value() {
1976 let ast = load_test_ast();
1977 let sources = ast.get("sources").unwrap();
1978 let doc_index = build_doc_index(&ast);
1979
1980 let param_node = find_node_by_id(sources, NodeId(4055)).unwrap();
1982 assert_eq!(
1983 param_node.get("name").and_then(|v| v.as_str()),
1984 Some("delta")
1985 );
1986
1987 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1988 assert!(
1989 doc.contains("deltas of the token balances"),
1990 "should have return doc: {doc}"
1991 );
1992 }
1993
1994 #[test]
1995 fn test_param_doc_function_input_parameter() {
1996 let ast = load_test_ast();
1997 let sources = ast.get("sources").unwrap();
1998 let doc_index = build_doc_index(&ast);
1999
2000 let fn_node = find_node_by_id(sources, NodeId(4371)).unwrap();
2003 let params_arr = fn_node
2004 .get("parameters")
2005 .and_then(|p| p.get("parameters"))
2006 .and_then(|p| p.as_array())
2007 .unwrap();
2008 let params_param = params_arr
2009 .iter()
2010 .find(|p| p.get("name").and_then(|v| v.as_str()) == Some("params"))
2011 .unwrap();
2012
2013 let doc = lookup_param_doc(&doc_index, params_param, sources).unwrap();
2014 assert!(
2015 doc.contains("position details"),
2016 "should have param doc: {doc}"
2017 );
2018 }
2019
2020 #[test]
2021 fn test_param_doc_inherited_function_via_docindex() {
2022 let ast = load_test_ast();
2023 let sources = ast.get("sources").unwrap();
2024 let doc_index = build_doc_index(&ast);
2025
2026 let param_node = find_node_by_id(sources, NodeId(478)).unwrap();
2029 assert_eq!(param_node.get("name").and_then(|v| v.as_str()), Some("key"));
2030
2031 let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
2032 assert!(
2033 doc.contains("pool to swap"),
2034 "should have inherited param doc: {doc}"
2035 );
2036 }
2037
2038 #[test]
2039 fn test_param_doc_non_parameter_returns_none() {
2040 let ast = load_test_ast();
2041 let sources = ast.get("sources").unwrap();
2042 let doc_index = build_doc_index(&ast);
2043
2044 let node = find_node_by_id(sources, NodeId(1216)).unwrap();
2046 assert!(lookup_param_doc(&doc_index, node, sources).is_none());
2047 }
2048
2049 fn load_solc_fixture() -> Value {
2052 let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
2053 let raw: Value = serde_json::from_str(&data).expect("valid json");
2054 crate::solc::normalize_solc_output(raw, None)
2055 }
2056
2057 #[test]
2058 fn test_doc_index_is_not_empty() {
2059 let ast = load_solc_fixture();
2060 let index = build_doc_index(&ast);
2061 assert!(!index.is_empty(), "DocIndex should contain entries");
2062 }
2063
2064 #[test]
2065 fn test_doc_index_has_contract_entries() {
2066 let ast = load_solc_fixture();
2067 let index = build_doc_index(&ast);
2068
2069 let pm_keys: Vec<_> = index
2071 .keys()
2072 .filter(|k| matches!(k, DocKey::Contract(s) if s.contains("PoolManager")))
2073 .collect();
2074 assert!(
2075 !pm_keys.is_empty(),
2076 "should have a Contract entry for PoolManager"
2077 );
2078
2079 let pm_key = DocKey::Contract(
2080 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2081 );
2082 let entry = index.get(&pm_key).expect("PoolManager contract entry");
2083 assert_eq!(entry.title.as_deref(), Some("PoolManager"));
2084 assert_eq!(
2085 entry.notice.as_deref(),
2086 Some("Holds the state for all pools")
2087 );
2088 }
2089
2090 #[test]
2091 fn test_doc_index_has_function_by_selector() {
2092 let ast = load_solc_fixture();
2093 let index = build_doc_index(&ast);
2094
2095 let init_key = DocKey::Func(FuncSelector::new("6276cbbe"));
2097 let entry = index
2098 .get(&init_key)
2099 .expect("should have initialize by selector");
2100 assert_eq!(
2101 entry.notice.as_deref(),
2102 Some("Initialize the state for a given pool ID")
2103 );
2104 assert!(
2105 entry
2106 .details
2107 .as_deref()
2108 .unwrap_or("")
2109 .contains("MAX_SWAP_FEE"),
2110 "devdoc details should mention MAX_SWAP_FEE"
2111 );
2112 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
2114 assert!(param_names.contains(&"key"), "should have param 'key'");
2115 assert!(
2116 param_names.contains(&"sqrtPriceX96"),
2117 "should have param 'sqrtPriceX96'"
2118 );
2119 let return_names: Vec<&str> = entry.returns.iter().map(|(n, _)| n.as_str()).collect();
2121 assert!(return_names.contains(&"tick"), "should have return 'tick'");
2122 }
2123
2124 #[test]
2125 fn test_doc_index_swap_by_selector() {
2126 let ast = load_solc_fixture();
2127 let index = build_doc_index(&ast);
2128
2129 let swap_key = DocKey::Func(FuncSelector::new("f3cd914c"));
2131 let entry = index.get(&swap_key).expect("should have swap by selector");
2132 assert!(
2133 entry
2134 .notice
2135 .as_deref()
2136 .unwrap_or("")
2137 .contains("Swap against the given pool"),
2138 "swap notice should describe swapping"
2139 );
2140 assert!(
2142 !entry.params.is_empty(),
2143 "swap should have param documentation"
2144 );
2145 }
2146
2147 #[test]
2148 fn test_doc_index_settle_by_selector() {
2149 let ast = load_solc_fixture();
2150 let index = build_doc_index(&ast);
2151
2152 let key = DocKey::Func(FuncSelector::new("11da60b4"));
2154 let entry = index.get(&key).expect("should have settle by selector");
2155 assert!(
2156 entry.notice.is_some(),
2157 "settle should have a notice from userdoc"
2158 );
2159 }
2160
2161 #[test]
2162 fn test_doc_index_has_error_entries() {
2163 let ast = load_solc_fixture();
2164 let index = build_doc_index(&ast);
2165
2166 let selector = compute_selector("AlreadyUnlocked()");
2168 let key = DocKey::Func(FuncSelector::new(&selector));
2169 let entry = index.get(&key).expect("should have AlreadyUnlocked error");
2170 assert!(
2171 entry
2172 .notice
2173 .as_deref()
2174 .unwrap_or("")
2175 .contains("already unlocked"),
2176 "AlreadyUnlocked notice: {:?}",
2177 entry.notice
2178 );
2179 }
2180
2181 #[test]
2182 fn test_doc_index_error_with_params() {
2183 let ast = load_solc_fixture();
2184 let index = build_doc_index(&ast);
2185
2186 let selector = compute_selector("CurrenciesOutOfOrderOrEqual(address,address)");
2188 let key = DocKey::Func(FuncSelector::new(&selector));
2189 let entry = index
2190 .get(&key)
2191 .expect("should have CurrenciesOutOfOrderOrEqual error");
2192 assert!(entry.notice.is_some(), "error should have notice");
2193 }
2194
2195 #[test]
2196 fn test_doc_index_has_event_entries() {
2197 let ast = load_solc_fixture();
2198 let index = build_doc_index(&ast);
2199
2200 let event_count = index
2202 .keys()
2203 .filter(|k| matches!(k, DocKey::Event(_)))
2204 .count();
2205 assert!(event_count > 0, "should have event entries in the DocIndex");
2206 }
2207
2208 #[test]
2209 fn test_doc_index_swap_event() {
2210 let ast = load_solc_fixture();
2211 let index = build_doc_index(&ast);
2212
2213 let topic =
2215 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2216 let key = DocKey::Event(EventSelector::new(&topic));
2217 let entry = index.get(&key).expect("should have Swap event");
2218
2219 assert!(
2221 entry
2222 .notice
2223 .as_deref()
2224 .unwrap_or("")
2225 .contains("swaps between currency0 and currency1"),
2226 "Swap event notice: {:?}",
2227 entry.notice
2228 );
2229
2230 let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
2232 assert!(
2233 param_names.contains(&"amount0"),
2234 "should have param 'amount0'"
2235 );
2236 assert!(
2237 param_names.contains(&"sender"),
2238 "should have param 'sender'"
2239 );
2240 assert!(param_names.contains(&"id"), "should have param 'id'");
2241 }
2242
2243 #[test]
2244 fn test_doc_index_initialize_event() {
2245 let ast = load_solc_fixture();
2246 let index = build_doc_index(&ast);
2247
2248 let topic = compute_event_topic(
2249 "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)",
2250 );
2251 let key = DocKey::Event(EventSelector::new(&topic));
2252 let entry = index.get(&key).expect("should have Initialize event");
2253 assert!(
2254 !entry.params.is_empty(),
2255 "Initialize event should have param docs"
2256 );
2257 }
2258
2259 #[test]
2260 fn test_doc_index_no_state_variables_for_pool_manager() {
2261 let ast = load_solc_fixture();
2262 let index = build_doc_index(&ast);
2263
2264 let sv_count = index
2266 .keys()
2267 .filter(|k| matches!(k, DocKey::StateVar(s) if s.contains("PoolManager")))
2268 .count();
2269 assert_eq!(
2270 sv_count, 0,
2271 "PoolManager should have no state variable doc entries"
2272 );
2273 }
2274
2275 #[test]
2276 fn test_doc_index_multiple_contracts() {
2277 let ast = load_solc_fixture();
2278 let index = build_doc_index(&ast);
2279
2280 let contract_count = index
2282 .keys()
2283 .filter(|k| matches!(k, DocKey::Contract(_)))
2284 .count();
2285 assert!(
2286 contract_count >= 5,
2287 "should have at least 5 contract-level entries, got {contract_count}"
2288 );
2289 }
2290
2291 #[test]
2292 fn test_doc_index_func_key_count() {
2293 let ast = load_solc_fixture();
2294 let index = build_doc_index(&ast);
2295
2296 let func_count = index
2297 .keys()
2298 .filter(|k| matches!(k, DocKey::Func(_)))
2299 .count();
2300 assert!(
2302 func_count >= 30,
2303 "should have at least 30 Func entries (methods + errors), got {func_count}"
2304 );
2305 }
2306
2307 #[test]
2308 fn test_doc_index_format_initialize_entry() {
2309 let ast = load_solc_fixture();
2310 let index = build_doc_index(&ast);
2311
2312 let key = DocKey::Func(FuncSelector::new("6276cbbe"));
2313 let entry = index.get(&key).expect("initialize entry");
2314 let formatted = format_doc_entry(entry);
2315
2316 assert!(
2317 formatted.contains("Initialize the state for a given pool ID"),
2318 "formatted should include notice"
2319 );
2320 assert!(
2321 formatted.contains("**@dev**"),
2322 "formatted should include dev section"
2323 );
2324 assert!(
2325 formatted.contains("**Parameters:**"),
2326 "formatted should include parameters"
2327 );
2328 assert!(
2329 formatted.contains("`key`"),
2330 "formatted should include key param"
2331 );
2332 assert!(
2333 formatted.contains("**Returns:**"),
2334 "formatted should include returns"
2335 );
2336 assert!(
2337 formatted.contains("`tick`"),
2338 "formatted should include tick return"
2339 );
2340 }
2341
2342 #[test]
2343 fn test_doc_index_format_contract_entry() {
2344 let ast = load_solc_fixture();
2345 let index = build_doc_index(&ast);
2346
2347 let key = DocKey::Contract(
2348 "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2349 );
2350 let entry = index.get(&key).expect("PoolManager contract entry");
2351 let formatted = format_doc_entry(entry);
2352
2353 assert!(
2354 formatted.contains("**PoolManager**"),
2355 "should include bold title"
2356 );
2357 assert!(
2358 formatted.contains("Holds the state for all pools"),
2359 "should include notice"
2360 );
2361 }
2362
2363 #[test]
2364 fn test_doc_index_inherited_docs_resolved() {
2365 let ast = load_solc_fixture();
2366 let index = build_doc_index(&ast);
2367
2368 let key = DocKey::Func(FuncSelector::new("f3cd914c"));
2372 let entry = index.get(&key).expect("swap entry");
2373 let notice = entry.notice.as_deref().unwrap_or("");
2375 assert!(
2376 !notice.contains("@inheritdoc"),
2377 "userdoc/devdoc should have resolved inherited docs, not raw @inheritdoc"
2378 );
2379 }
2380
2381 #[test]
2382 fn test_compute_selector_known_values() {
2383 let sel = compute_selector("AlreadyUnlocked()");
2385 assert_eq!(sel.len(), 8, "selector should be 8 hex chars");
2386
2387 let init_sel =
2389 compute_selector("initialize((address,address,uint24,int24,address),uint160)");
2390 assert_eq!(
2391 init_sel, "6276cbbe",
2392 "computed initialize selector should match evm.methodIdentifiers"
2393 );
2394 }
2395
2396 #[test]
2397 fn test_compute_event_topic_length() {
2398 let topic =
2399 compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2400 assert_eq!(
2401 topic.len(),
2402 64,
2403 "event topic should be 64 hex chars (32 bytes)"
2404 );
2405 }
2406
2407 #[test]
2408 fn test_doc_index_error_count_poolmanager() {
2409 let ast = load_solc_fixture();
2410 let index = build_doc_index(&ast);
2411
2412 let error_sigs = [
2415 "AlreadyUnlocked()",
2416 "CurrenciesOutOfOrderOrEqual(address,address)",
2417 "CurrencyNotSettled()",
2418 "InvalidCaller()",
2419 "ManagerLocked()",
2420 "MustClearExactPositiveDelta()",
2421 "NonzeroNativeValue()",
2422 "PoolNotInitialized()",
2423 "ProtocolFeeCurrencySynced()",
2424 "ProtocolFeeTooLarge(uint24)",
2425 "SwapAmountCannotBeZero()",
2426 "TickSpacingTooLarge(int24)",
2427 "TickSpacingTooSmall(int24)",
2428 "UnauthorizedDynamicLPFeeUpdate()",
2429 ];
2430 let mut found = 0;
2431 for sig in &error_sigs {
2432 let selector = compute_selector(sig);
2433 let key = DocKey::Func(FuncSelector::new(&selector));
2434 if index.contains_key(&key) {
2435 found += 1;
2436 }
2437 }
2438 assert_eq!(
2439 found,
2440 error_sigs.len(),
2441 "all 14 PoolManager errors should be in the DocIndex"
2442 );
2443 }
2444
2445 #[test]
2446 fn test_doc_index_extsload_overloads_have_different_selectors() {
2447 let ast = load_solc_fixture();
2448 let index = build_doc_index(&ast);
2449
2450 let sel1 = DocKey::Func(FuncSelector::new("1e2eaeaf"));
2455 let sel2 = DocKey::Func(FuncSelector::new("35fd631a"));
2456 let sel3 = DocKey::Func(FuncSelector::new("dbd035ff"));
2457
2458 assert!(index.contains_key(&sel1), "extsload(bytes32) should exist");
2459 assert!(
2460 index.contains_key(&sel2),
2461 "extsload(bytes32,uint256) should exist"
2462 );
2463 assert!(
2464 index.contains_key(&sel3),
2465 "extsload(bytes32[]) should exist"
2466 );
2467 }
2468
2469 #[test]
2470 fn test_build_parameter_strings_basic() {
2471 let node: Value = serde_json::json!({
2472 "parameters": {
2473 "parameters": [
2474 {
2475 "name": "amount",
2476 "typeDescriptions": { "typeString": "uint256" },
2477 "storageLocation": "default"
2478 },
2479 {
2480 "name": "tax",
2481 "typeDescriptions": { "typeString": "uint16" },
2482 "storageLocation": "default"
2483 }
2484 ]
2485 }
2486 });
2487 let params = build_parameter_strings(Some(node.get("parameters").unwrap()));
2488 assert_eq!(params, vec!["uint256 amount", "uint16 tax"]);
2489 }
2490
2491 #[test]
2492 fn test_build_parameter_strings_with_storage() {
2493 let node: Value = serde_json::json!({
2494 "parameters": {
2495 "parameters": [
2496 {
2497 "name": "key",
2498 "typeDescriptions": { "typeString": "struct PoolKey" },
2499 "storageLocation": "calldata"
2500 }
2501 ]
2502 }
2503 });
2504 let params = build_parameter_strings(Some(node.get("parameters").unwrap()));
2505 assert_eq!(params, vec!["struct PoolKey calldata key"]);
2506 }
2507
2508 #[test]
2509 fn test_build_parameter_strings_empty() {
2510 let node: Value = serde_json::json!({
2511 "parameters": { "parameters": [] }
2512 });
2513 let params = build_parameter_strings(Some(node.get("parameters").unwrap()));
2514 assert!(params.is_empty());
2515 }
2516
2517 #[test]
2518 fn test_build_parameter_strings_unnamed() {
2519 let node: Value = serde_json::json!({
2520 "parameters": {
2521 "parameters": [
2522 {
2523 "name": "",
2524 "typeDescriptions": { "typeString": "uint256" },
2525 "storageLocation": "default"
2526 }
2527 ]
2528 }
2529 });
2530 let params = build_parameter_strings(Some(node.get("parameters").unwrap()));
2531 assert_eq!(params, vec!["uint256"]);
2532 }
2533
2534 #[test]
2535 fn test_signature_help_parameter_offsets() {
2536 let label = "function addTax(uint256 amount, uint16 tax, uint16 base)";
2538 let param_strings = vec![
2539 "uint256 amount".to_string(),
2540 "uint16 tax".to_string(),
2541 "uint16 base".to_string(),
2542 ];
2543
2544 let params_start = label.find('(').unwrap() + 1;
2545 let mut offsets = Vec::new();
2546 let mut offset = params_start;
2547 for param_str in ¶m_strings {
2548 let start = offset;
2549 let end = start + param_str.len();
2550 offsets.push((start, end));
2551 offset = end + 2; }
2553
2554 assert_eq!(&label[offsets[0].0..offsets[0].1], "uint256 amount");
2556 assert_eq!(&label[offsets[1].0..offsets[1].1], "uint16 tax");
2557 assert_eq!(&label[offsets[2].0..offsets[2].1], "uint16 base");
2558 }
2559
2560 #[test]
2561 fn test_find_mapping_decl_by_name_pools() {
2562 let ast = load_test_ast();
2563 let sources = ast.get("sources").unwrap();
2564 let decl = find_mapping_decl_by_name(sources, "_pools").unwrap();
2565 assert_eq!(decl.get("name").and_then(|v| v.as_str()), Some("_pools"));
2566 assert_eq!(
2567 decl.get("typeName")
2568 .and_then(|t| t.get("nodeType"))
2569 .and_then(|v| v.as_str()),
2570 Some("Mapping")
2571 );
2572 }
2573
2574 #[test]
2575 fn test_find_mapping_decl_by_name_not_found() {
2576 let ast = load_test_ast();
2577 let sources = ast.get("sources").unwrap();
2578 assert!(find_mapping_decl_by_name(sources, "nonexistent").is_none());
2579 }
2580
2581 #[test]
2582 fn test_mapping_signature_help_pools() {
2583 let ast = load_test_ast();
2584 let sources = ast.get("sources").unwrap();
2585 let help = mapping_signature_help(sources, "_pools").unwrap();
2586
2587 assert_eq!(help.signatures.len(), 1);
2588 let sig = &help.signatures[0];
2589 assert_eq!(sig.label, "_pools[PoolId id]");
2591 assert_eq!(sig.active_parameter, Some(0));
2592
2593 let params = sig.parameters.as_ref().unwrap();
2595 assert_eq!(params.len(), 1);
2596 if let ParameterLabel::LabelOffsets([start, end]) = params[0].label {
2597 assert_eq!(&sig.label[start as usize..end as usize], "PoolId id");
2598 } else {
2599 panic!("expected LabelOffsets");
2600 }
2601
2602 let doc = sig.documentation.as_ref().unwrap();
2604 if let Documentation::MarkupContent(mc) = doc {
2605 assert!(mc.value.contains("struct Pool.State"));
2606 }
2607 }
2608
2609 #[test]
2610 fn test_mapping_signature_help_protocol_fees() {
2611 let ast = load_test_ast();
2612 let sources = ast.get("sources").unwrap();
2613 let help = mapping_signature_help(sources, "protocolFeesAccrued").unwrap();
2614 let sig = &help.signatures[0];
2615 assert_eq!(sig.label, "protocolFeesAccrued[Currency currency]");
2616 }
2617
2618 #[test]
2619 fn test_mapping_signature_help_non_mapping() {
2620 let ast = load_test_ast();
2621 let sources = ast.get("sources").unwrap();
2622 assert!(mapping_signature_help(sources, "owner").is_none());
2624 }
2625}