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<'a>(sources: &'a Value, target_id: u64) -> Option<&'a 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 if child_selector == impl_selector {
136 return extract_documentation(child);
137 }
138 }
139 }
140
141 None
142}
143
144pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
148 let mut lines: Vec<String> = Vec::new();
149 let mut in_params = false;
150 let mut in_returns = false;
151
152 for raw_line in text.lines() {
153 let line = raw_line.trim().trim_start_matches('*').trim();
154 if line.is_empty() {
155 continue;
156 }
157
158 if let Some(rest) = line.strip_prefix("@notice ") {
159 in_params = false;
160 in_returns = false;
161 lines.push(rest.to_string());
162 } else if let Some(rest) = line.strip_prefix("@dev ") {
163 in_params = false;
164 in_returns = false;
165 lines.push(String::new());
166 lines.push(format!("*{rest}*"));
167 } else if let Some(rest) = line.strip_prefix("@param ") {
168 if !in_params {
169 in_params = true;
170 in_returns = false;
171 lines.push(String::new());
172 lines.push("**Parameters:**".to_string());
173 }
174 if let Some((name, desc)) = rest.split_once(' ') {
175 lines.push(format!("- `{name}` — {desc}"));
176 } else {
177 lines.push(format!("- `{rest}`"));
178 }
179 } else if let Some(rest) = line.strip_prefix("@return ") {
180 if !in_returns {
181 in_returns = true;
182 in_params = false;
183 lines.push(String::new());
184 lines.push("**Returns:**".to_string());
185 }
186 if let Some((name, desc)) = rest.split_once(' ') {
187 lines.push(format!("- `{name}` — {desc}"));
188 } else {
189 lines.push(format!("- `{rest}`"));
190 }
191 } else if line.starts_with("@author ") {
192 } else if line.starts_with("@inheritdoc ") {
194 if let Some(inherited) = inherited_doc {
196 let formatted = format_natspec(inherited, None);
198 if !formatted.is_empty() {
199 lines.push(formatted);
200 }
201 } else {
202 let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
203 lines.push(format!("*Inherits documentation from `{parent}`*"));
204 }
205 } else {
206 lines.push(line.to_string());
208 }
209 }
210
211 lines.join("\n")
212}
213
214fn build_function_signature(node: &Value) -> Option<String> {
216 let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
217 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
218
219 match node_type {
220 "FunctionDefinition" => {
221 let kind = node
222 .get("kind")
223 .and_then(|v| v.as_str())
224 .unwrap_or("function");
225 let visibility = node
226 .get("visibility")
227 .and_then(|v| v.as_str())
228 .unwrap_or("");
229 let state_mutability = node
230 .get("stateMutability")
231 .and_then(|v| v.as_str())
232 .unwrap_or("");
233
234 let params = format_parameters(node.get("parameters"));
235 let returns = format_parameters(node.get("returnParameters"));
236
237 let mut sig = match kind {
238 "constructor" => format!("constructor({params})"),
239 "receive" => "receive() external payable".to_string(),
240 "fallback" => format!("fallback({params})"),
241 _ => format!("function {name}({params})"),
242 };
243
244 if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
245 sig.push_str(&format!(" {visibility}"));
246 }
247 if !state_mutability.is_empty() && state_mutability != "nonpayable" {
248 sig.push_str(&format!(" {state_mutability}"));
249 }
250 if !returns.is_empty() {
251 sig.push_str(&format!(" returns ({returns})"));
252 }
253 Some(sig)
254 }
255 "ModifierDefinition" => {
256 let params = format_parameters(node.get("parameters"));
257 Some(format!("modifier {name}({params})"))
258 }
259 "EventDefinition" => {
260 let params = format_parameters(node.get("parameters"));
261 Some(format!("event {name}({params})"))
262 }
263 "ErrorDefinition" => {
264 let params = format_parameters(node.get("parameters"));
265 Some(format!("error {name}({params})"))
266 }
267 "VariableDeclaration" => {
268 let type_str = node
269 .get("typeDescriptions")
270 .and_then(|v| v.get("typeString"))
271 .and_then(|v| v.as_str())
272 .unwrap_or("unknown");
273 let visibility = node
274 .get("visibility")
275 .and_then(|v| v.as_str())
276 .unwrap_or("");
277 let mutability = node
278 .get("mutability")
279 .and_then(|v| v.as_str())
280 .unwrap_or("");
281
282 let mut sig = format!("{type_str}");
283 if !visibility.is_empty() {
284 sig.push_str(&format!(" {visibility}"));
285 }
286 if mutability == "constant" || mutability == "immutable" {
287 sig.push_str(&format!(" {mutability}"));
288 }
289 sig.push_str(&format!(" {name}"));
290 Some(sig)
291 }
292 "ContractDefinition" => {
293 let contract_kind = node
294 .get("contractKind")
295 .and_then(|v| v.as_str())
296 .unwrap_or("contract");
297
298 let mut sig = format!("{contract_kind} {name}");
299
300 if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array()) {
302 if !bases.is_empty() {
303 let base_names: Vec<&str> = bases
304 .iter()
305 .filter_map(|b| {
306 b.get("baseName")
307 .and_then(|bn| bn.get("name"))
308 .and_then(|n| n.as_str())
309 })
310 .collect();
311 if !base_names.is_empty() {
312 sig.push_str(&format!(" is {}", base_names.join(", ")));
313 }
314 }
315 }
316 Some(sig)
317 }
318 "StructDefinition" => {
319 let mut sig = format!("struct {name} {{\n");
320 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
321 for member in members {
322 let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
323 let mtype = member
324 .get("typeDescriptions")
325 .and_then(|v| v.get("typeString"))
326 .and_then(|v| v.as_str())
327 .unwrap_or("?");
328 sig.push_str(&format!(" {mtype} {mname};\n"));
329 }
330 }
331 sig.push('}');
332 Some(sig)
333 }
334 "EnumDefinition" => {
335 let mut sig = format!("enum {name} {{\n");
336 if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
337 let names: Vec<&str> = members
338 .iter()
339 .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
340 .collect();
341 for n in &names {
342 sig.push_str(&format!(" {n},\n"));
343 }
344 }
345 sig.push('}');
346 Some(sig)
347 }
348 "UserDefinedValueTypeDefinition" => {
349 let underlying = node
350 .get("underlyingType")
351 .and_then(|v| v.get("typeDescriptions"))
352 .and_then(|v| v.get("typeString"))
353 .and_then(|v| v.as_str())
354 .unwrap_or("unknown");
355 Some(format!("type {name} is {underlying}"))
356 }
357 _ => None,
358 }
359}
360
361fn format_parameters(params_node: Option<&Value>) -> String {
363 let params_node = match params_node {
364 Some(v) => v,
365 None => return String::new(),
366 };
367 let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
368 Some(arr) => arr,
369 None => return String::new(),
370 };
371
372 let parts: Vec<String> = params
373 .iter()
374 .map(|p| {
375 let type_str = p
376 .get("typeDescriptions")
377 .and_then(|v| v.get("typeString"))
378 .and_then(|v| v.as_str())
379 .unwrap_or("?");
380 let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
381 let storage = p
382 .get("storageLocation")
383 .and_then(|v| v.as_str())
384 .unwrap_or("default");
385
386 if name.is_empty() {
387 type_str.to_string()
388 } else if storage != "default" {
389 format!("{type_str} {storage} {name}")
390 } else {
391 format!("{type_str} {name}")
392 }
393 })
394 .collect();
395
396 parts.join(", ")
397}
398
399pub fn hover_info(
401 ast_data: &Value,
402 file_uri: &Url,
403 position: Position,
404 source_bytes: &[u8],
405) -> Option<Hover> {
406 let sources = ast_data.get("sources")?;
407 let build_infos = ast_data.get("build_infos").and_then(|v| v.as_array())?;
408 let first_build = build_infos.first()?;
409 let source_id_to_path = first_build
410 .get("source_id_to_path")
411 .and_then(|v| v.as_object())?;
412
413 let id_to_path: HashMap<String, String> = source_id_to_path
414 .iter()
415 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
416 .collect();
417
418 let (nodes, path_to_abs, external_refs) = cache_ids(sources);
419
420 let file_path = file_uri.to_file_path().ok()?;
422 let file_path_str = file_path.to_str()?;
423
424 let abs_path = path_to_abs
426 .iter()
427 .find(|(k, _)| file_path_str.ends_with(k.as_str()))
428 .map(|(_, v)| v.clone())?;
429
430 let byte_pos = pos_to_bytes(source_bytes, position);
431
432 let node_id = byte_to_decl_via_external_refs(&external_refs, &id_to_path, &abs_path, byte_pos)
434 .or_else(|| byte_to_id(&nodes, &abs_path, byte_pos))?;
435
436 let node_info = nodes
438 .values()
439 .find_map(|file_nodes| file_nodes.get(&node_id))?;
440
441 let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
443
444 let decl_node = find_node_by_id(sources, decl_id)?;
446
447 let mut parts: Vec<String> = Vec::new();
449
450 if let Some(sig) = build_function_signature(decl_node) {
452 parts.push(format!("```solidity\n{sig}\n```"));
453 } else {
454 if let Some(type_str) = decl_node
456 .get("typeDescriptions")
457 .and_then(|v| v.get("typeString"))
458 .and_then(|v| v.as_str())
459 {
460 let name = decl_node.get("name").and_then(|v| v.as_str()).unwrap_or("");
461 parts.push(format!("```solidity\n{type_str} {name}\n```"));
462 }
463 }
464
465 if let Some((selector, kind)) = extract_selector(decl_node) {
467 match kind {
468 "event" => parts.push(format!("Selector: `0x{selector}`")),
469 _ => parts.push(format!("Selector: `0x{selector}`")),
470 }
471 }
472
473 if let Some(doc_text) = extract_documentation(decl_node) {
475 let inherited_doc = resolve_inheritdoc(sources, decl_node, &doc_text);
476 let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
477 if !formatted.is_empty() {
478 parts.push(format!("---\n{formatted}"));
479 }
480 }
481
482 if parts.is_empty() {
483 return None;
484 }
485
486 Some(Hover {
487 contents: HoverContents::Markup(MarkupContent {
488 kind: MarkupKind::Markdown,
489 value: parts.join("\n\n"),
490 }),
491 range: None,
492 })
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 fn load_test_ast() -> Value {
500 let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
501 serde_json::from_str(&data).expect("valid json")
502 }
503
504 #[test]
505 fn test_find_node_by_id_pool_manager() {
506 let ast = load_test_ast();
507 let sources = ast.get("sources").unwrap();
508 let node = find_node_by_id(sources, 1767).unwrap();
509 assert_eq!(
510 node.get("name").and_then(|v| v.as_str()),
511 Some("PoolManager")
512 );
513 assert_eq!(
514 node.get("nodeType").and_then(|v| v.as_str()),
515 Some("ContractDefinition")
516 );
517 }
518
519 #[test]
520 fn test_find_node_by_id_initialize() {
521 let ast = load_test_ast();
522 let sources = ast.get("sources").unwrap();
523 let node = find_node_by_id(sources, 2411).unwrap();
525 assert_eq!(
526 node.get("name").and_then(|v| v.as_str()),
527 Some("initialize")
528 );
529 }
530
531 #[test]
532 fn test_extract_documentation_object() {
533 let ast = load_test_ast();
534 let sources = ast.get("sources").unwrap();
535 let node = find_node_by_id(sources, 2411).unwrap();
537 let doc = extract_documentation(node).unwrap();
538 assert!(doc.contains("@notice"));
539 assert!(doc.contains("@param key"));
540 }
541
542 #[test]
543 fn test_extract_documentation_none() {
544 let ast = load_test_ast();
545 let sources = ast.get("sources").unwrap();
546 let node = find_node_by_id(sources, 8887).unwrap();
548 let _ = extract_documentation(node);
550 }
551
552 #[test]
553 fn test_format_natspec_notice_and_params() {
554 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";
555 let formatted = format_natspec(text, None);
556 assert!(formatted.contains("Initialize the state"));
557 assert!(formatted.contains("**Parameters:**"));
558 assert!(formatted.contains("`key`"));
559 assert!(formatted.contains("**Returns:**"));
560 assert!(formatted.contains("`tick`"));
561 }
562
563 #[test]
564 fn test_format_natspec_inheritdoc() {
565 let text = "@inheritdoc IPoolManager";
566 let formatted = format_natspec(text, None);
567 assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
568 }
569
570 #[test]
571 fn test_format_natspec_dev() {
572 let text = "@notice Do something\n @dev This is an implementation detail";
573 let formatted = format_natspec(text, None);
574 assert!(formatted.contains("Do something"));
575 assert!(formatted.contains("*This is an implementation detail*"));
576 }
577
578 #[test]
579 fn test_build_function_signature_initialize() {
580 let ast = load_test_ast();
581 let sources = ast.get("sources").unwrap();
582 let node = find_node_by_id(sources, 2411).unwrap();
583 let sig = build_function_signature(node).unwrap();
584 assert!(sig.starts_with("function initialize("));
585 assert!(sig.contains("returns"));
586 }
587
588 #[test]
589 fn test_build_signature_contract() {
590 let ast = load_test_ast();
591 let sources = ast.get("sources").unwrap();
592 let node = find_node_by_id(sources, 1767).unwrap();
593 let sig = build_function_signature(node).unwrap();
594 assert!(sig.contains("contract PoolManager"));
595 assert!(sig.contains(" is "));
596 }
597
598 #[test]
599 fn test_build_signature_struct() {
600 let ast = load_test_ast();
601 let sources = ast.get("sources").unwrap();
602 let node = find_node_by_id(sources, 8887).unwrap();
603 let sig = build_function_signature(node).unwrap();
604 assert!(sig.starts_with("struct PoolKey"));
605 assert!(sig.contains('{'));
606 }
607
608 #[test]
609 fn test_build_signature_error() {
610 let ast = load_test_ast();
611 let sources = ast.get("sources").unwrap();
612 let node = find_node_by_id(sources, 508).unwrap();
614 assert_eq!(
615 node.get("nodeType").and_then(|v| v.as_str()),
616 Some("ErrorDefinition")
617 );
618 let sig = build_function_signature(node).unwrap();
619 assert!(sig.starts_with("error "));
620 }
621
622 #[test]
623 fn test_build_signature_event() {
624 let ast = load_test_ast();
625 let sources = ast.get("sources").unwrap();
626 let node = find_node_by_id(sources, 8).unwrap();
628 assert_eq!(
629 node.get("nodeType").and_then(|v| v.as_str()),
630 Some("EventDefinition")
631 );
632 let sig = build_function_signature(node).unwrap();
633 assert!(sig.starts_with("event "));
634 }
635
636 #[test]
637 fn test_build_signature_variable() {
638 let ast = load_test_ast();
639 let sources = ast.get("sources").unwrap();
640 let pm = find_node_by_id(sources, 1767).unwrap();
643 if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
644 for node in nodes {
645 if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
646 let sig = build_function_signature(node);
647 assert!(sig.is_some());
648 break;
649 }
650 }
651 }
652 }
653
654 #[test]
655 fn test_pool_manager_has_documentation() {
656 let ast = load_test_ast();
657 let sources = ast.get("sources").unwrap();
658 let node = find_node_by_id(sources, 59).unwrap();
660 let doc = extract_documentation(node).unwrap();
661 assert!(doc.contains("@notice"));
662 }
663
664 #[test]
665 fn test_format_parameters_empty() {
666 let result = format_parameters(None);
667 assert_eq!(result, "");
668 }
669
670 #[test]
671 fn test_format_parameters_with_data() {
672 let params: Value = serde_json::json!({
673 "parameters": [
674 {
675 "name": "key",
676 "typeDescriptions": { "typeString": "struct PoolKey" },
677 "storageLocation": "memory"
678 },
679 {
680 "name": "sqrtPriceX96",
681 "typeDescriptions": { "typeString": "uint160" },
682 "storageLocation": "default"
683 }
684 ]
685 });
686 let result = format_parameters(Some(¶ms));
687 assert!(result.contains("struct PoolKey memory key"));
688 assert!(result.contains("uint160 sqrtPriceX96"));
689 }
690
691 #[test]
694 fn test_extract_selector_function() {
695 let ast = load_test_ast();
696 let sources = ast.get("sources").unwrap();
697 let node = find_node_by_id(sources, 1167).unwrap();
699 let (selector, kind) = extract_selector(node).unwrap();
700 assert_eq!(selector, "f3cd914c");
701 assert_eq!(kind, "function");
702 }
703
704 #[test]
705 fn test_extract_selector_error() {
706 let ast = load_test_ast();
707 let sources = ast.get("sources").unwrap();
708 let node = find_node_by_id(sources, 508).unwrap();
710 let (selector, kind) = extract_selector(node).unwrap();
711 assert_eq!(selector, "0d89438e");
712 assert_eq!(kind, "error");
713 }
714
715 #[test]
716 fn test_extract_selector_event() {
717 let ast = load_test_ast();
718 let sources = ast.get("sources").unwrap();
719 let node = find_node_by_id(sources, 8).unwrap();
721 let (selector, kind) = extract_selector(node).unwrap();
722 assert!(selector.len() == 64); assert_eq!(kind, "event");
724 }
725
726 #[test]
727 fn test_extract_selector_public_variable() {
728 let ast = load_test_ast();
729 let sources = ast.get("sources").unwrap();
730 let node = find_node_by_id(sources, 10).unwrap();
732 let (selector, kind) = extract_selector(node).unwrap();
733 assert_eq!(selector, "8da5cb5b");
734 assert_eq!(kind, "function");
735 }
736
737 #[test]
738 fn test_extract_selector_internal_function_none() {
739 let ast = load_test_ast();
740 let sources = ast.get("sources").unwrap();
741 let node = find_node_by_id(sources, 5960).unwrap();
743 assert!(extract_selector(node).is_none());
744 }
745
746 #[test]
749 fn test_resolve_inheritdoc_swap() {
750 let ast = load_test_ast();
751 let sources = ast.get("sources").unwrap();
752 let decl = find_node_by_id(sources, 1167).unwrap();
754 let doc_text = extract_documentation(decl).unwrap();
755 assert!(doc_text.contains("@inheritdoc"));
756
757 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
758 assert!(resolved.contains("@notice"));
759 assert!(resolved.contains("Swap against the given pool"));
760 }
761
762 #[test]
763 fn test_resolve_inheritdoc_initialize() {
764 let ast = load_test_ast();
765 let sources = ast.get("sources").unwrap();
766 let decl = find_node_by_id(sources, 881).unwrap();
768 let doc_text = extract_documentation(decl).unwrap();
769
770 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
771 assert!(resolved.contains("Initialize the state"));
772 assert!(resolved.contains("@param key"));
773 }
774
775 #[test]
776 fn test_resolve_inheritdoc_extsload_overload() {
777 let ast = load_test_ast();
778 let sources = ast.get("sources").unwrap();
779
780 let decl = find_node_by_id(sources, 442).unwrap();
782 let doc_text = extract_documentation(decl).unwrap();
783 let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
784 assert!(resolved.contains("granular pool state"));
785 assert!(resolved.contains("@param slot"));
787
788 let decl2 = find_node_by_id(sources, 455).unwrap();
790 let doc_text2 = extract_documentation(decl2).unwrap();
791 let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
792 assert!(resolved2.contains("@param startSlot"));
793
794 let decl3 = find_node_by_id(sources, 467).unwrap();
796 let doc_text3 = extract_documentation(decl3).unwrap();
797 let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
798 assert!(resolved3.contains("sparse pool state"));
799 }
800
801 #[test]
802 fn test_resolve_inheritdoc_formats_in_hover() {
803 let ast = load_test_ast();
804 let sources = ast.get("sources").unwrap();
805 let decl = find_node_by_id(sources, 1167).unwrap();
807 let doc_text = extract_documentation(decl).unwrap();
808 let inherited = resolve_inheritdoc(sources, decl, &doc_text);
809 let formatted = format_natspec(&doc_text, inherited.as_deref());
810 assert!(!formatted.contains("@inheritdoc"));
812 assert!(formatted.contains("Swap against the given pool"));
813 assert!(formatted.contains("**Parameters:**"));
814 }
815}