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