solidity_language_server/
hover.rs1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Url};
4
5use crate::goto::{cache_ids, pos_to_bytes, CHILD_KEYS};
6use crate::references::{byte_to_decl_via_external_refs, byte_to_id};
7
8pub fn find_node_by_id(sources: &Value, target_id: u64) -> Option<&Value> {
10 let sources_obj = sources.as_object()?;
11 for (_path, contents) in sources_obj {
12 let contents_array = contents.as_array()?;
13 let first_content = contents_array.first()?;
14 let source_file = first_content.get("source_file")?;
15 let ast = source_file.get("ast")?;
16
17 if ast.get("id").and_then(|v| v.as_u64()) == Some(target_id) {
19 return Some(ast);
20 }
21
22 let mut stack = vec![ast];
23 while let Some(node) = stack.pop() {
24 if node.get("id").and_then(|v| v.as_u64()) == Some(target_id) {
25 return Some(node);
26 }
27 for key in CHILD_KEYS {
28 if let Some(value) = node.get(key) {
29 match value {
30 Value::Array(arr) => stack.extend(arr.iter()),
31 Value::Object(_) => stack.push(value),
32 _ => {}
33 }
34 }
35 }
36 }
37 }
38 None
39}
40
41pub fn extract_documentation(node: &Value) -> Option<String> {
44 let doc = node.get("documentation")?;
45 match doc {
46 Value::Object(_) => doc
47 .get("text")
48 .and_then(|v| v.as_str())
49 .map(|s| s.to_string()),
50 Value::String(s) => Some(s.clone()),
51 _ => None,
52 }
53}
54
55pub fn extract_selector(node: &Value) -> Option<(String, &'static str)> {
58 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
59 match node_type {
60 "FunctionDefinition" => node
61 .get("functionSelector")
62 .and_then(|v| v.as_str())
63 .map(|s| (s.to_string(), "function")),
64 "VariableDeclaration" => node
65 .get("functionSelector")
66 .and_then(|v| v.as_str())
67 .map(|s| (s.to_string(), "function")),
68 "ErrorDefinition" => node
69 .get("errorSelector")
70 .and_then(|v| v.as_str())
71 .map(|s| (s.to_string(), "error")),
72 "EventDefinition" => node
73 .get("eventSelector")
74 .and_then(|v| v.as_str())
75 .map(|s| (s.to_string(), "event")),
76 _ => None,
77 }
78}
79
80pub fn resolve_inheritdoc<'a>(
88 sources: &'a Value,
89 decl_node: &'a Value,
90 doc_text: &str,
91) -> Option<String> {
92 let parent_name = doc_text
94 .lines()
95 .find_map(|line| {
96 let trimmed = line.trim().trim_start_matches('*').trim();
97 trimmed.strip_prefix("@inheritdoc ")
98 })?
99 .trim();
100
101 let (impl_selector, _) = extract_selector(decl_node)?;
103
104 let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
106
107 let scope_contract = find_node_by_id(sources, scope_id)?;
109
110 let base_contracts = scope_contract
112 .get("baseContracts")
113 .and_then(|v| v.as_array())?;
114 let parent_id = base_contracts.iter().find_map(|base| {
115 let name = base
116 .get("baseName")
117 .and_then(|bn| bn.get("name"))
118 .and_then(|n| n.as_str())?;
119 if name == parent_name {
120 base.get("baseName")
121 .and_then(|bn| bn.get("referencedDeclaration"))
122 .and_then(|v| v.as_u64())
123 } else {
124 None
125 }
126 })?;
127
128 let parent_contract = find_node_by_id(sources, parent_id)?;
130
131 let parent_nodes = parent_contract.get("nodes").and_then(|v| v.as_array())?;
133 for child in parent_nodes {
134 if let Some((child_selector, _)) = extract_selector(child)
135 && child_selector == impl_selector {
136 return extract_documentation(child);
137 }
138 }
139
140 None
141}
142
143pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
147 let mut lines: Vec<String> = Vec::new();
148 let mut in_params = false;
149 let mut in_returns = false;
150
151 for raw_line in text.lines() {
152 let line = raw_line.trim().trim_start_matches('*').trim();
153 if line.is_empty() {
154 continue;
155 }
156
157 if let Some(rest) = line.strip_prefix("@notice ") {
158 in_params = false;
159 in_returns = false;
160 lines.push(rest.to_string());
161 } else if let Some(rest) = line.strip_prefix("@dev ") {
162 in_params = false;
163 in_returns = false;
164 lines.push(String::new());
165 lines.push(format!("*{rest}*"));
166 } else if let Some(rest) = line.strip_prefix("@param ") {
167 if !in_params {
168 in_params = true;
169 in_returns = false;
170 lines.push(String::new());
171 lines.push("**Parameters:**".to_string());
172 }
173 if let Some((name, desc)) = rest.split_once(' ') {
174 lines.push(format!("- `{name}` — {desc}"));
175 } else {
176 lines.push(format!("- `{rest}`"));
177 }
178 } else if let Some(rest) = line.strip_prefix("@return ") {
179 if !in_returns {
180 in_returns = true;
181 in_params = false;
182 lines.push(String::new());
183 lines.push("**Returns:**".to_string());
184 }
185 if let Some((name, desc)) = rest.split_once(' ') {
186 lines.push(format!("- `{name}` — {desc}"));
187 } else {
188 lines.push(format!("- `{rest}`"));
189 }
190 } else if line.starts_with("@author ") {
191 } else if line.starts_with("@inheritdoc ") {
193 if let Some(inherited) = inherited_doc {
195 let formatted = format_natspec(inherited, None);
197 if !formatted.is_empty() {
198 lines.push(formatted);
199 }
200 } else {
201 let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
202 lines.push(format!("*Inherits documentation from `{parent}`*"));
203 }
204 } else {
205 lines.push(line.to_string());
207 }
208 }
209
210 lines.join("\n")
211}
212
213fn build_function_signature(node: &Value) -> Option<String> {
215 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
216 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
217
218 match node_type {
219 "FunctionDefinition" => {
220 let kind = node
221 .get("kind")
222 .and_then(|v| v.as_str())
223 .unwrap_or("function");
224 let visibility = node
225 .get("visibility")
226 .and_then(|v| v.as_str())
227 .unwrap_or("");
228 let state_mutability = node
229 .get("stateMutability")
230 .and_then(|v| v.as_str())
231 .unwrap_or("");
232
233 let params = format_parameters(node.get("parameters"));
234 let returns = format_parameters(node.get("returnParameters"));
235
236 let mut sig = match kind {
237 "constructor" => format!("constructor({params})"),
238 "receive" => "receive() external payable".to_string(),
239 "fallback" => format!("fallback({params})"),
240 _ => format!("function {name}({params})"),
241 };
242
243 if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
244 sig.push_str(&format!(" {visibility}"));
245 }
246 if !state_mutability.is_empty() && state_mutability != "nonpayable" {
247 sig.push_str(&format!(" {state_mutability}"));
248 }
249 if !returns.is_empty() {
250 sig.push_str(&format!(" returns ({returns})"));
251 }
252 Some(sig)
253 }
254 "ModifierDefinition" => {
255 let params = format_parameters(node.get("parameters"));
256 Some(format!("modifier {name}({params})"))
257 }
258 "EventDefinition" => {
259 let params = format_parameters(node.get("parameters"));
260 Some(format!("event {name}({params})"))
261 }
262 "ErrorDefinition" => {
263 let params = format_parameters(node.get("parameters"));
264 Some(format!("error {name}({params})"))
265 }
266 "VariableDeclaration" => {
267 let type_str = node
268 .get("typeDescriptions")
269 .and_then(|v| v.get("typeString"))
270 .and_then(|v| v.as_str())
271 .unwrap_or("unknown");
272 let visibility = node
273 .get("visibility")
274 .and_then(|v| v.as_str())
275 .unwrap_or("");
276 let mutability = node
277 .get("mutability")
278 .and_then(|v| v.as_str())
279 .unwrap_or("");
280
281 let mut sig = type_str.to_string();
282 if !visibility.is_empty() {
283 sig.push_str(&format!(" {visibility}"));
284 }
285 if mutability == "constant" || mutability == "immutable" {
286 sig.push_str(&format!(" {mutability}"));
287 }
288 sig.push_str(&format!(" {name}"));
289 Some(sig)
290 }
291 "ContractDefinition" => {
292 let contract_kind = node
293 .get("contractKind")
294 .and_then(|v| v.as_str())
295 .unwrap_or("contract");
296
297 let mut sig = format!("{contract_kind} {name}");
298
299 if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array())
301 && !bases.is_empty() {
302 let base_names: Vec<&str> = bases
303 .iter()
304 .filter_map(|b| {
305 b.get("baseName")
306 .and_then(|bn| bn.get("name"))
307 .and_then(|n| n.as_str())
308 })
309 .collect();
310 if !base_names.is_empty() {
311 sig.push_str(&format!(" is {}", base_names.join(", ")));
312 }
313 }
314 Some(sig)
315 }
316 "StructDefinition" => {
317 let mut sig = format!("struct {name} {{\n");
318 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
319 for member in members {
320 let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
321 let mtype = member
322 .get("typeDescriptions")
323 .and_then(|v| v.get("typeString"))
324 .and_then(|v| v.as_str())
325 .unwrap_or("?");
326 sig.push_str(&format!(" {mtype} {mname};\n"));
327 }
328 }
329 sig.push('}');
330 Some(sig)
331 }
332 "EnumDefinition" => {
333 let mut sig = format!("enum {name} {{\n");
334 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
335 let names: Vec<&str> = members
336 .iter()
337 .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
338 .collect();
339 for n in &names {
340 sig.push_str(&format!(" {n},\n"));
341 }
342 }
343 sig.push('}');
344 Some(sig)
345 }
346 "UserDefinedValueTypeDefinition" => {
347 let underlying = node
348 .get("underlyingType")
349 .and_then(|v| v.get("typeDescriptions"))
350 .and_then(|v| v.get("typeString"))
351 .and_then(|v| v.as_str())
352 .unwrap_or("unknown");
353 Some(format!("type {name} is {underlying}"))
354 }
355 _ => None,
356 }
357}
358
359fn format_parameters(params_node: Option<&Value>) -> String {
361 let params_node = match params_node {
362 Some(v) => v,
363 None => return String::new(),
364 };
365 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
366 Some(arr) => arr,
367 None => return String::new(),
368 };
369
370 let parts: Vec<String> = params
371 .iter()
372 .map(|p| {
373 let type_str = p
374 .get("typeDescriptions")
375 .and_then(|v| v.get("typeString"))
376 .and_then(|v| v.as_str())
377 .unwrap_or("?");
378 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
379 let storage = p
380 .get("storageLocation")
381 .and_then(|v| v.as_str())
382 .unwrap_or("default");
383
384 if name.is_empty() {
385 type_str.to_string()
386 } else if storage != "default" {
387 format!("{type_str} {storage} {name}")
388 } else {
389 format!("{type_str} {name}")
390 }
391 })
392 .collect();
393
394 parts.join(", ")
395}
396
397pub fn hover_info(
399 ast_data: &Value,
400 file_uri: &Url,
401 position: Position,
402 source_bytes: &[u8],
403) -> Option<Hover> {
404 let sources = ast_data.get("sources")?;
405 let build_infos = ast_data.get("build_infos").and_then(|v| v.as_array())?;
406 let first_build = build_infos.first()?;
407 let source_id_to_path = first_build
408 .get("source_id_to_path")
409 .and_then(|v| v.as_object())?;
410
411 let id_to_path: HashMap<String, String> = source_id_to_path
412 .iter()
413 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
414 .collect();
415
416 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
417
418 let file_path = file_uri.to_file_path().ok()?;
420 let file_path_str = file_path.to_str()?;
421
422 let abs_path = path_to_abs
424 .iter()
425 .find(|(k, _)| file_path_str.ends_with(k.as_str()))
426 .map(|(_, v)| v.clone())?;
427
428 let byte_pos = pos_to_bytes(source_bytes, position);
429
430 let node_id = byte_to_decl_via_external_refs(&external_refs, &id_to_path, &abs_path, byte_pos)
432 .or_else(|| byte_to_id(&nodes, &abs_path, byte_pos))?;
433
434 let node_info = nodes
436 .values()
437 .find_map(|file_nodes| file_nodes.get(&node_id))?;
438
439 let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
441
442 let decl_node = find_node_by_id(sources, decl_id)?;
444
445 let mut parts: Vec<String> = Vec::new();
447
448 if let Some(sig) = build_function_signature(decl_node) {
450 parts.push(format!("```solidity\n{sig}\n```"));
451 } else {
452 if let Some(type_str) = decl_node
454 .get("typeDescriptions")
455 .and_then(|v| v.get("typeString"))
456 .and_then(|v| v.as_str())
457 {
458 let name = decl_node.get("name").and_then(|v| v.as_str()).unwrap_or("");
459 parts.push(format!("```solidity\n{type_str} {name}\n```"));
460 }
461 }
462
463 if let Some((selector, kind)) = extract_selector(decl_node) {
465 match kind {
466 "event" => parts.push(format!("Selector: `0x{selector}`")),
467 _ => parts.push(format!("Selector: `0x{selector}`")),
468 }
469 }
470
471 if let Some(doc_text) = extract_documentation(decl_node) {
473 let inherited_doc = resolve_inheritdoc(sources, decl_node, &doc_text);
474 let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
475 if !formatted.is_empty() {
476 parts.push(format!("---\n{formatted}"));
477 }
478 }
479
480 if parts.is_empty() {
481 return None;
482 }
483
484 Some(Hover {
485 contents: HoverContents::Markup(MarkupContent {
486 kind: MarkupKind::Markdown,
487 value: parts.join("\n\n"),
488 }),
489 range: None,
490 })
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 fn load_test_ast() -> Value {
498 let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
499 serde_json::from_str(&data).expect("valid json")
500 }
501
502 #[test]
503 fn test_find_node_by_id_pool_manager() {
504 let ast = load_test_ast();
505 let sources = ast.get("sources").unwrap();
506 let node = find_node_by_id(sources, 1767).unwrap();
507 assert_eq!(
508 node.get("name").and_then(|v| v.as_str()),
509 Some("PoolManager")
510 );
511 assert_eq!(
512 node.get("nodeType").and_then(|v| v.as_str()),
513 Some("ContractDefinition")
514 );
515 }
516
517 #[test]
518 fn test_find_node_by_id_initialize() {
519 let ast = load_test_ast();
520 let sources = ast.get("sources").unwrap();
521 let node = find_node_by_id(sources, 2411).unwrap();
523 assert_eq!(
524 node.get("name").and_then(|v| v.as_str()),
525 Some("initialize")
526 );
527 }
528
529 #[test]
530 fn test_extract_documentation_object() {
531 let ast = load_test_ast();
532 let sources = ast.get("sources").unwrap();
533 let node = find_node_by_id(sources, 2411).unwrap();
535 let doc = extract_documentation(node).unwrap();
536 assert!(doc.contains("@notice"));
537 assert!(doc.contains("@param key"));
538 }
539
540 #[test]
541 fn test_extract_documentation_none() {
542 let ast = load_test_ast();
543 let sources = ast.get("sources").unwrap();
544 let node = find_node_by_id(sources, 8887).unwrap();
546 let _ = extract_documentation(node);
548 }
549
550 #[test]
551 fn test_format_natspec_notice_and_params() {
552 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";
553 let formatted = format_natspec(text, None);
554 assert!(formatted.contains("Initialize the state"));
555 assert!(formatted.contains("**Parameters:**"));
556 assert!(formatted.contains("`key`"));
557 assert!(formatted.contains("**Returns:**"));
558 assert!(formatted.contains("`tick`"));
559 }
560
561 #[test]
562 fn test_format_natspec_inheritdoc() {
563 let text = "@inheritdoc IPoolManager";
564 let formatted = format_natspec(text, None);
565 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
566 }
567
568 #[test]
569 fn test_format_natspec_dev() {
570 let text = "@notice Do something\n @dev This is an implementation detail";
571 let formatted = format_natspec(text, None);
572 assert!(formatted.contains("Do something"));
573 assert!(formatted.contains("*This is an implementation detail*"));
574 }
575
576 #[test]
577 fn test_build_function_signature_initialize() {
578 let ast = load_test_ast();
579 let sources = ast.get("sources").unwrap();
580 let node = find_node_by_id(sources, 2411).unwrap();
581 let sig = build_function_signature(node).unwrap();
582 assert!(sig.starts_with("function initialize("));
583 assert!(sig.contains("returns"));
584 }
585
586 #[test]
587 fn test_build_signature_contract() {
588 let ast = load_test_ast();
589 let sources = ast.get("sources").unwrap();
590 let node = find_node_by_id(sources, 1767).unwrap();
591 let sig = build_function_signature(node).unwrap();
592 assert!(sig.contains("contract PoolManager"));
593 assert!(sig.contains(" is "));
594 }
595
596 #[test]
597 fn test_build_signature_struct() {
598 let ast = load_test_ast();
599 let sources = ast.get("sources").unwrap();
600 let node = find_node_by_id(sources, 8887).unwrap();
601 let sig = build_function_signature(node).unwrap();
602 assert!(sig.starts_with("struct PoolKey"));
603 assert!(sig.contains('{'));
604 }
605
606 #[test]
607 fn test_build_signature_error() {
608 let ast = load_test_ast();
609 let sources = ast.get("sources").unwrap();
610 let node = find_node_by_id(sources, 508).unwrap();
612 assert_eq!(
613 node.get("nodeType").and_then(|v| v.as_str()),
614 Some("ErrorDefinition")
615 );
616 let sig = build_function_signature(node).unwrap();
617 assert!(sig.starts_with("error "));
618 }
619
620 #[test]
621 fn test_build_signature_event() {
622 let ast = load_test_ast();
623 let sources = ast.get("sources").unwrap();
624 let node = find_node_by_id(sources, 8).unwrap();
626 assert_eq!(
627 node.get("nodeType").and_then(|v| v.as_str()),
628 Some("EventDefinition")
629 );
630 let sig = build_function_signature(node).unwrap();
631 assert!(sig.starts_with("event "));
632 }
633
634 #[test]
635 fn test_build_signature_variable() {
636 let ast = load_test_ast();
637 let sources = ast.get("sources").unwrap();
638 let pm = find_node_by_id(sources, 1767).unwrap();
641 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
642 for node in nodes {
643 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
644 let sig = build_function_signature(node);
645 assert!(sig.is_some());
646 break;
647 }
648 }
649 }
650 }
651
652 #[test]
653 fn test_pool_manager_has_documentation() {
654 let ast = load_test_ast();
655 let sources = ast.get("sources").unwrap();
656 let node = find_node_by_id(sources, 59).unwrap();
658 let doc = extract_documentation(node).unwrap();
659 assert!(doc.contains("@notice"));
660 }
661
662 #[test]
663 fn test_format_parameters_empty() {
664 let result = format_parameters(None);
665 assert_eq!(result, "");
666 }
667
668 #[test]
669 fn test_format_parameters_with_data() {
670 let params: Value = serde_json::json!({
671 "parameters": [
672 {
673 "name": "key",
674 "typeDescriptions": { "typeString": "struct PoolKey" },
675 "storageLocation": "memory"
676 },
677 {
678 "name": "sqrtPriceX96",
679 "typeDescriptions": { "typeString": "uint160" },
680 "storageLocation": "default"
681 }
682 ]
683 });
684 let result = format_parameters(Some(¶ms));
685 assert!(result.contains("struct PoolKey memory key"));
686 assert!(result.contains("uint160 sqrtPriceX96"));
687 }
688
689 #[test]
692 fn test_extract_selector_function() {
693 let ast = load_test_ast();
694 let sources = ast.get("sources").unwrap();
695 let node = find_node_by_id(sources, 1167).unwrap();
697 let (selector, kind) = extract_selector(node).unwrap();
698 assert_eq!(selector, "f3cd914c");
699 assert_eq!(kind, "function");
700 }
701
702 #[test]
703 fn test_extract_selector_error() {
704 let ast = load_test_ast();
705 let sources = ast.get("sources").unwrap();
706 let node = find_node_by_id(sources, 508).unwrap();
708 let (selector, kind) = extract_selector(node).unwrap();
709 assert_eq!(selector, "0d89438e");
710 assert_eq!(kind, "error");
711 }
712
713 #[test]
714 fn test_extract_selector_event() {
715 let ast = load_test_ast();
716 let sources = ast.get("sources").unwrap();
717 let node = find_node_by_id(sources, 8).unwrap();
719 let (selector, kind) = extract_selector(node).unwrap();
720 assert!(selector.len() == 64); assert_eq!(kind, "event");
722 }
723
724 #[test]
725 fn test_extract_selector_public_variable() {
726 let ast = load_test_ast();
727 let sources = ast.get("sources").unwrap();
728 let node = find_node_by_id(sources, 10).unwrap();
730 let (selector, kind) = extract_selector(node).unwrap();
731 assert_eq!(selector, "8da5cb5b");
732 assert_eq!(kind, "function");
733 }
734
735 #[test]
736 fn test_extract_selector_internal_function_none() {
737 let ast = load_test_ast();
738 let sources = ast.get("sources").unwrap();
739 let node = find_node_by_id(sources, 5960).unwrap();
741 assert!(extract_selector(node).is_none());
742 }
743
744 #[test]
747 fn test_resolve_inheritdoc_swap() {
748 let ast = load_test_ast();
749 let sources = ast.get("sources").unwrap();
750 let decl = find_node_by_id(sources, 1167).unwrap();
752 let doc_text = extract_documentation(decl).unwrap();
753 assert!(doc_text.contains("@inheritdoc"));
754
755 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
756 assert!(resolved.contains("@notice"));
757 assert!(resolved.contains("Swap against the given pool"));
758 }
759
760 #[test]
761 fn test_resolve_inheritdoc_initialize() {
762 let ast = load_test_ast();
763 let sources = ast.get("sources").unwrap();
764 let decl = find_node_by_id(sources, 881).unwrap();
766 let doc_text = extract_documentation(decl).unwrap();
767
768 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
769 assert!(resolved.contains("Initialize the state"));
770 assert!(resolved.contains("@param key"));
771 }
772
773 #[test]
774 fn test_resolve_inheritdoc_extsload_overload() {
775 let ast = load_test_ast();
776 let sources = ast.get("sources").unwrap();
777
778 let decl = find_node_by_id(sources, 442).unwrap();
780 let doc_text = extract_documentation(decl).unwrap();
781 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
782 assert!(resolved.contains("granular pool state"));
783 assert!(resolved.contains("@param slot"));
785
786 let decl2 = find_node_by_id(sources, 455).unwrap();
788 let doc_text2 = extract_documentation(decl2).unwrap();
789 let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
790 assert!(resolved2.contains("@param startSlot"));
791
792 let decl3 = find_node_by_id(sources, 467).unwrap();
794 let doc_text3 = extract_documentation(decl3).unwrap();
795 let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
796 assert!(resolved3.contains("sparse pool state"));
797 }
798
799 #[test]
800 fn test_resolve_inheritdoc_formats_in_hover() {
801 let ast = load_test_ast();
802 let sources = ast.get("sources").unwrap();
803 let decl = find_node_by_id(sources, 1167).unwrap();
805 let doc_text = extract_documentation(decl).unwrap();
806 let inherited = resolve_inheritdoc(sources, decl, &doc_text);
807 let formatted = format_natspec(&doc_text, inherited.as_deref());
808 assert!(!formatted.contains("@inheritdoc"));
810 assert!(formatted.contains("Swap against the given pool"));
811 assert!(formatted.contains("**Parameters:**"));
812 }
813}