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