Skip to main content

solidity_language_server/
hover.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Url};
4
5use crate::gas::{self, GasIndex};
6use crate::goto::{CHILD_KEYS, cache_ids, pos_to_bytes};
7use crate::inlay_hints::HintIndex;
8use crate::references::{byte_to_decl_via_external_refs, byte_to_id};
9use crate::types::{EventSelector, FuncSelector, MethodId, NodeId, Selector};
10
11// ── DocIndex — pre-built userdoc/devdoc lookup ─────────────────────────────
12
13/// Merged documentation from solc userdoc + devdoc for a single declaration.
14#[derive(Debug, Clone, Default)]
15pub struct DocEntry {
16    /// `@notice` from userdoc.
17    pub notice: Option<String>,
18    /// `@dev` / `details` from devdoc.
19    pub details: Option<String>,
20    /// `@param` descriptions from devdoc, keyed by parameter name.
21    pub params: Vec<(String, String)>,
22    /// `@return` descriptions from devdoc, keyed by return name.
23    pub returns: Vec<(String, String)>,
24    /// `@title` from devdoc (contract-level only).
25    pub title: Option<String>,
26    /// `@author` from devdoc (contract-level only).
27    pub author: Option<String>,
28}
29
30/// Key for looking up documentation in the [`DocIndex`].
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum DocKey {
33    /// 4-byte selector for functions, public variables, and errors.
34    Func(FuncSelector),
35    /// 32-byte topic hash for events.
36    Event(EventSelector),
37    /// Contract-level docs, keyed by `"path:Name"`.
38    Contract(String),
39    /// State variable docs, keyed by `"path:ContractName:varName"`.
40    StateVar(String),
41    /// Fallback for methods without a selector (shouldn't happen, but safe).
42    Method(String),
43}
44
45/// Pre-built documentation index from solc contract output.
46///
47/// Keyed by [`DocKey`] for type-safe lookup from AST nodes.
48pub type DocIndex = HashMap<DocKey, DocEntry>;
49
50/// Build a documentation index from normalized AST output.
51///
52/// Iterates over `contracts[path][name]` and merges userdoc + devdoc
53/// into `DocEntry` values keyed for fast lookup from AST nodes.
54pub fn build_doc_index(ast_data: &Value) -> DocIndex {
55    let mut index = DocIndex::new();
56
57    let contracts = match ast_data.get("contracts").and_then(|c| c.as_object()) {
58        Some(c) => c,
59        None => return index,
60    };
61
62    for (path, names) in contracts {
63        let names_obj = match names.as_object() {
64            Some(n) => n,
65            None => continue,
66        };
67
68        for (name, contract) in names_obj {
69            let userdoc = contract.get("userdoc");
70            let devdoc = contract.get("devdoc");
71            let method_ids = contract
72                .get("evm")
73                .and_then(|e| e.get("methodIdentifiers"))
74                .and_then(|m| m.as_object());
75
76            // Build canonical_sig → selector for userdoc/devdoc key lookups
77            let sig_to_selector: HashMap<&str, &str> = method_ids
78                .map(|mi| {
79                    mi.iter()
80                        .filter_map(|(sig, sel)| sel.as_str().map(|s| (sig.as_str(), s)))
81                        .collect()
82                })
83                .unwrap_or_default();
84
85            // ── Contract-level docs ──
86            let mut contract_entry = DocEntry::default();
87            if let Some(ud) = userdoc {
88                contract_entry.notice = ud
89                    .get("notice")
90                    .and_then(|v| v.as_str())
91                    .map(|s| s.to_string());
92            }
93            if let Some(dd) = devdoc {
94                contract_entry.title = dd
95                    .get("title")
96                    .and_then(|v| v.as_str())
97                    .map(|s| s.to_string());
98                contract_entry.details = dd
99                    .get("details")
100                    .and_then(|v| v.as_str())
101                    .map(|s| s.to_string());
102                contract_entry.author = dd
103                    .get("author")
104                    .and_then(|v| v.as_str())
105                    .map(|s| s.to_string());
106            }
107            if contract_entry.notice.is_some()
108                || contract_entry.title.is_some()
109                || contract_entry.details.is_some()
110            {
111                let key = DocKey::Contract(format!("{path}:{name}"));
112                index.insert(key, contract_entry);
113            }
114
115            // ── Method docs (functions + public state variable getters) ──
116            let ud_methods = userdoc
117                .and_then(|u| u.get("methods"))
118                .and_then(|m| m.as_object());
119            let dd_methods = devdoc
120                .and_then(|d| d.get("methods"))
121                .and_then(|m| m.as_object());
122
123            // Collect all canonical sigs from both userdoc and devdoc methods
124            let mut all_sigs: Vec<&str> = Vec::new();
125            if let Some(um) = ud_methods {
126                all_sigs.extend(um.keys().map(|k| k.as_str()));
127            }
128            if let Some(dm) = dd_methods {
129                for k in dm.keys() {
130                    if !all_sigs.contains(&k.as_str()) {
131                        all_sigs.push(k.as_str());
132                    }
133                }
134            }
135
136            for sig in &all_sigs {
137                let mut entry = DocEntry::default();
138
139                // userdoc notice
140                if let Some(um) = ud_methods
141                    && let Some(method) = um.get(*sig)
142                {
143                    entry.notice = method
144                        .get("notice")
145                        .and_then(|v| v.as_str())
146                        .map(|s| s.to_string());
147                }
148
149                // devdoc details + params + returns
150                if let Some(dm) = dd_methods
151                    && let Some(method) = dm.get(*sig)
152                {
153                    entry.details = method
154                        .get("details")
155                        .and_then(|v| v.as_str())
156                        .map(|s| s.to_string());
157
158                    if let Some(params) = method.get("params").and_then(|p| p.as_object()) {
159                        for (pname, pdesc) in params {
160                            if let Some(desc) = pdesc.as_str() {
161                                entry.params.push((pname.clone(), desc.to_string()));
162                            }
163                        }
164                    }
165
166                    if let Some(returns) = method.get("returns").and_then(|r| r.as_object()) {
167                        for (rname, rdesc) in returns {
168                            if let Some(desc) = rdesc.as_str() {
169                                entry.returns.push((rname.clone(), desc.to_string()));
170                            }
171                        }
172                    }
173                }
174
175                if entry.notice.is_none()
176                    && entry.details.is_none()
177                    && entry.params.is_empty()
178                    && entry.returns.is_empty()
179                {
180                    continue;
181                }
182
183                // Key by selector (for AST node matching)
184                if let Some(selector) = sig_to_selector.get(sig) {
185                    let key = DocKey::Func(FuncSelector::new(*selector));
186                    index.insert(key, entry);
187                } else {
188                    // No selector (shouldn't happen for methods, but be safe)
189                    // Key by function name for fallback matching
190                    let fn_name = sig.split('(').next().unwrap_or(sig);
191                    let key = DocKey::Method(format!("{path}:{name}:{fn_name}"));
192                    index.insert(key, entry);
193                }
194            }
195
196            // ── Error docs ──
197            let ud_errors = userdoc
198                .and_then(|u| u.get("errors"))
199                .and_then(|e| e.as_object());
200            let dd_errors = devdoc
201                .and_then(|d| d.get("errors"))
202                .and_then(|e| e.as_object());
203
204            let mut all_error_sigs: Vec<&str> = Vec::new();
205            if let Some(ue) = ud_errors {
206                all_error_sigs.extend(ue.keys().map(|k| k.as_str()));
207            }
208            if let Some(de) = dd_errors {
209                for k in de.keys() {
210                    if !all_error_sigs.contains(&k.as_str()) {
211                        all_error_sigs.push(k.as_str());
212                    }
213                }
214            }
215
216            for sig in &all_error_sigs {
217                let mut entry = DocEntry::default();
218
219                // userdoc: errors are arrays of { notice }
220                if let Some(ue) = ud_errors
221                    && let Some(arr) = ue.get(*sig).and_then(|v| v.as_array())
222                    && let Some(first) = arr.first()
223                {
224                    entry.notice = first
225                        .get("notice")
226                        .and_then(|v| v.as_str())
227                        .map(|s| s.to_string());
228                }
229
230                // devdoc: errors are also arrays
231                if let Some(de) = dd_errors
232                    && let Some(arr) = de.get(*sig).and_then(|v| v.as_array())
233                    && let Some(first) = arr.first()
234                {
235                    entry.details = first
236                        .get("details")
237                        .and_then(|v| v.as_str())
238                        .map(|s| s.to_string());
239                    if let Some(params) = first.get("params").and_then(|p| p.as_object()) {
240                        for (pname, pdesc) in params {
241                            if let Some(desc) = pdesc.as_str() {
242                                entry.params.push((pname.clone(), desc.to_string()));
243                            }
244                        }
245                    }
246                }
247
248                if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
249                    continue;
250                }
251
252                // Compute 4-byte error selector from the canonical signature
253                // errorSelector = keccak256(sig)[0..4]
254                let selector = FuncSelector::new(compute_selector(sig));
255                index.insert(DocKey::Func(selector), entry);
256            }
257
258            // ── Event docs ──
259            let ud_events = userdoc
260                .and_then(|u| u.get("events"))
261                .and_then(|e| e.as_object());
262            let dd_events = devdoc
263                .and_then(|d| d.get("events"))
264                .and_then(|e| e.as_object());
265
266            let mut all_event_sigs: Vec<&str> = Vec::new();
267            if let Some(ue) = ud_events {
268                all_event_sigs.extend(ue.keys().map(|k| k.as_str()));
269            }
270            if let Some(de) = dd_events {
271                for k in de.keys() {
272                    if !all_event_sigs.contains(&k.as_str()) {
273                        all_event_sigs.push(k.as_str());
274                    }
275                }
276            }
277
278            for sig in &all_event_sigs {
279                let mut entry = DocEntry::default();
280
281                if let Some(ue) = ud_events
282                    && let Some(ev) = ue.get(*sig)
283                {
284                    entry.notice = ev
285                        .get("notice")
286                        .and_then(|v| v.as_str())
287                        .map(|s| s.to_string());
288                }
289
290                if let Some(de) = dd_events
291                    && let Some(ev) = de.get(*sig)
292                {
293                    entry.details = ev
294                        .get("details")
295                        .and_then(|v| v.as_str())
296                        .map(|s| s.to_string());
297                    if let Some(params) = ev.get("params").and_then(|p| p.as_object()) {
298                        for (pname, pdesc) in params {
299                            if let Some(desc) = pdesc.as_str() {
300                                entry.params.push((pname.clone(), desc.to_string()));
301                            }
302                        }
303                    }
304                }
305
306                if entry.notice.is_none() && entry.details.is_none() && entry.params.is_empty() {
307                    continue;
308                }
309
310                // Event topic = full keccak256 hash of canonical signature
311                let topic = EventSelector::new(compute_event_topic(sig));
312                index.insert(DocKey::Event(topic), entry);
313            }
314
315            // ── State variable docs (from devdoc) ──
316            if let Some(dd) = devdoc
317                && let Some(state_vars) = dd.get("stateVariables").and_then(|s| s.as_object())
318            {
319                for (var_name, var_doc) in state_vars {
320                    let mut entry = DocEntry::default();
321                    entry.details = var_doc
322                        .get("details")
323                        .and_then(|v| v.as_str())
324                        .map(|s| s.to_string());
325
326                    if let Some(returns) = var_doc.get("return").and_then(|v| v.as_str()) {
327                        entry.returns.push(("_0".to_string(), returns.to_string()));
328                    }
329                    if let Some(returns) = var_doc.get("returns").and_then(|r| r.as_object()) {
330                        for (rname, rdesc) in returns {
331                            if let Some(desc) = rdesc.as_str() {
332                                entry.returns.push((rname.clone(), desc.to_string()));
333                            }
334                        }
335                    }
336
337                    if entry.details.is_some() || !entry.returns.is_empty() {
338                        let key = DocKey::StateVar(format!("{path}:{name}:{var_name}"));
339                        index.insert(key, entry);
340                    }
341                }
342            }
343        }
344    }
345
346    index
347}
348
349/// Compute a 4-byte function/error selector from a canonical ABI signature.
350///
351/// `keccak256("transfer(address,uint256)")` → first 4 bytes as hex.
352fn compute_selector(sig: &str) -> String {
353    use tiny_keccak::{Hasher, Keccak};
354    let mut hasher = Keccak::v256();
355    hasher.update(sig.as_bytes());
356    let mut output = [0u8; 32];
357    hasher.finalize(&mut output);
358    hex::encode(&output[..4])
359}
360
361/// Compute a full 32-byte event topic from a canonical ABI signature.
362///
363/// `keccak256("Transfer(address,address,uint256)")` → full hash as hex.
364fn compute_event_topic(sig: &str) -> String {
365    use tiny_keccak::{Hasher, Keccak};
366    let mut hasher = Keccak::v256();
367    hasher.update(sig.as_bytes());
368    let mut output = [0u8; 32];
369    hasher.finalize(&mut output);
370    hex::encode(output)
371}
372
373/// Look up documentation for an AST declaration node from the DocIndex.
374///
375/// Returns a cloned DocEntry since key construction is dynamic.
376pub fn lookup_doc_entry(
377    doc_index: &DocIndex,
378    decl_node: &Value,
379    sources: &Value,
380) -> Option<DocEntry> {
381    let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
382
383    match node_type {
384        "FunctionDefinition" | "VariableDeclaration" => {
385            // Try by functionSelector first
386            if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str()) {
387                let key = DocKey::Func(FuncSelector::new(selector));
388                if let Some(entry) = doc_index.get(&key) {
389                    return Some(entry.clone());
390                }
391            }
392
393            // For state variables without selector, try statevar key
394            if node_type == "VariableDeclaration" {
395                let var_name = decl_node.get("name").and_then(|v| v.as_str())?;
396                // Find containing contract via scope
397                let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
398                let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
399                let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
400
401                // Need to find the path — walk source units
402                let path = find_source_path_for_node(sources, scope_id)?;
403                let key = DocKey::StateVar(format!("{path}:{contract_name}:{var_name}"));
404                if let Some(entry) = doc_index.get(&key) {
405                    return Some(entry.clone());
406                }
407            }
408
409            // Fallback: try method by name
410            let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
411            let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
412            let scope_node = find_node_by_id(sources, NodeId(scope_id))?;
413            let contract_name = scope_node.get("name").and_then(|v| v.as_str())?;
414            let path = find_source_path_for_node(sources, scope_id)?;
415            let key = DocKey::Method(format!("{path}:{contract_name}:{fn_name}"));
416            doc_index.get(&key).cloned()
417        }
418        "ErrorDefinition" => {
419            if let Some(selector) = decl_node.get("errorSelector").and_then(|v| v.as_str()) {
420                let key = DocKey::Func(FuncSelector::new(selector));
421                return doc_index.get(&key).cloned();
422            }
423            None
424        }
425        "EventDefinition" => {
426            if let Some(selector) = decl_node.get("eventSelector").and_then(|v| v.as_str()) {
427                let key = DocKey::Event(EventSelector::new(selector));
428                return doc_index.get(&key).cloned();
429            }
430            None
431        }
432        "ContractDefinition" => {
433            let contract_name = decl_node.get("name").and_then(|v| v.as_str())?;
434            // Find the source path for this contract
435            let node_id = decl_node.get("id").and_then(|v| v.as_u64())?;
436            let path = find_source_path_for_node(sources, node_id)?;
437            let key = DocKey::Contract(format!("{path}:{contract_name}"));
438            doc_index.get(&key).cloned()
439        }
440        _ => None,
441    }
442}
443
444/// Look up documentation for a parameter from its parent function/error/event.
445///
446/// When hovering a `VariableDeclaration` that is a parameter or return value,
447/// this walks up to the parent declaration (via `scope`) and extracts the
448/// relevant `@param` or `@return` entry for this specific name.
449///
450/// Tries the DocIndex first (structured devdoc), then falls back to parsing
451/// the raw AST `documentation` field.
452pub fn lookup_param_doc(
453    doc_index: &DocIndex,
454    decl_node: &Value,
455    sources: &Value,
456) -> Option<String> {
457    let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
458    if node_type != "VariableDeclaration" {
459        return None;
460    }
461
462    let param_name = decl_node.get("name").and_then(|v| v.as_str())?;
463    if param_name.is_empty() {
464        return None;
465    }
466
467    // Walk up to the parent via scope
468    let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
469    let parent_node = find_node_by_id(sources, NodeId(scope_id))?;
470    let parent_type = parent_node.get("nodeType").and_then(|v| v.as_str())?;
471
472    // Only handle function/error/event parents
473    if !matches!(
474        parent_type,
475        "FunctionDefinition" | "ErrorDefinition" | "EventDefinition" | "ModifierDefinition"
476    ) {
477        return None;
478    }
479
480    // Determine if this param is an input parameter or a return value
481    let is_return = if parent_type == "FunctionDefinition" {
482        parent_node
483            .get("returnParameters")
484            .and_then(|rp| rp.get("parameters"))
485            .and_then(|p| p.as_array())
486            .map(|arr| {
487                let decl_id = decl_node.get("id").and_then(|v| v.as_u64());
488                arr.iter()
489                    .any(|p| p.get("id").and_then(|v| v.as_u64()) == decl_id)
490            })
491            .unwrap_or(false)
492    } else {
493        false
494    };
495
496    // Try DocIndex first (structured devdoc)
497    if let Some(parent_doc) = lookup_doc_entry(doc_index, parent_node, sources) {
498        if is_return {
499            // Look in returns
500            for (rname, rdesc) in &parent_doc.returns {
501                if rname == param_name {
502                    return Some(rdesc.clone());
503                }
504            }
505        } else {
506            // Look in params
507            for (pname, pdesc) in &parent_doc.params {
508                if pname == param_name {
509                    return Some(pdesc.clone());
510                }
511            }
512        }
513    }
514
515    // Fallback: parse raw AST documentation on the parent
516    if let Some(doc_text) = extract_documentation(parent_node) {
517        // Resolve @inheritdoc if present
518        let resolved = if doc_text.contains("@inheritdoc") {
519            resolve_inheritdoc(sources, parent_node, &doc_text)
520        } else {
521            None
522        };
523        let text = resolved.as_deref().unwrap_or(&doc_text);
524
525        let tag = if is_return { "@return " } else { "@param " };
526        for line in text.lines() {
527            let trimmed = line.trim().trim_start_matches('*').trim();
528            if let Some(rest) = trimmed.strip_prefix(tag) {
529                if let Some((name, desc)) = rest.split_once(' ') {
530                    if name == param_name {
531                        return Some(desc.to_string());
532                    }
533                } else if rest == param_name {
534                    return Some(String::new());
535                }
536            }
537        }
538    }
539
540    None
541}
542
543/// Find the source file path that contains a given node id.
544fn find_source_path_for_node(sources: &Value, target_id: u64) -> Option<String> {
545    let sources_obj = sources.as_object()?;
546    for (path, source_data) in sources_obj {
547        let ast = source_data.get("ast")?;
548        // Check if this source unit contains the node (check source unit id first)
549        let source_id = ast.get("id").and_then(|v| v.as_u64())?;
550        if source_id == target_id {
551            return Some(path.clone());
552        }
553
554        // Check nodes in this source
555        if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
556            for node in nodes {
557                if let Some(id) = node.get("id").and_then(|v| v.as_u64())
558                    && id == target_id
559                {
560                    return Some(path.clone());
561                }
562                // Check one more level (functions inside contracts)
563                if let Some(sub_nodes) = node.get("nodes").and_then(|n| n.as_array()) {
564                    for sub in sub_nodes {
565                        if let Some(id) = sub.get("id").and_then(|v| v.as_u64())
566                            && id == target_id
567                        {
568                            return Some(path.clone());
569                        }
570                    }
571                }
572            }
573        }
574    }
575    None
576}
577
578/// Format a `DocEntry` as markdown for hover display.
579pub fn format_doc_entry(entry: &DocEntry) -> String {
580    let mut lines: Vec<String> = Vec::new();
581
582    // Title (contract-level)
583    if let Some(title) = &entry.title {
584        lines.push(format!("**{title}**"));
585        lines.push(String::new());
586    }
587
588    // Notice (@notice)
589    if let Some(notice) = &entry.notice {
590        lines.push(notice.clone());
591    }
592
593    // Author
594    if let Some(author) = &entry.author {
595        lines.push(format!("*@author {author}*"));
596    }
597
598    // Details (@dev)
599    if let Some(details) = &entry.details {
600        lines.push(String::new());
601        lines.push("**@dev**".to_string());
602        lines.push(format!("*{details}*"));
603    }
604
605    // Parameters (@param)
606    if !entry.params.is_empty() {
607        lines.push(String::new());
608        lines.push("**Parameters:**".to_string());
609        for (name, desc) in &entry.params {
610            lines.push(format!("- `{name}` — {desc}"));
611        }
612    }
613
614    // Returns (@return)
615    if !entry.returns.is_empty() {
616        lines.push(String::new());
617        lines.push("**Returns:**".to_string());
618        for (name, desc) in &entry.returns {
619            if name.starts_with('_') && name.len() <= 3 {
620                // Unnamed return (e.g. "_0") — just show description
621                lines.push(format!("- {desc}"));
622            } else {
623                lines.push(format!("- `{name}` — {desc}"));
624            }
625        }
626    }
627
628    lines.join("\n")
629}
630
631/// Find the raw AST node with the given id by walking all sources.
632pub fn find_node_by_id(sources: &Value, target_id: NodeId) -> Option<&Value> {
633    let sources_obj = sources.as_object()?;
634    for (_path, source_data) in sources_obj {
635        let ast = source_data.get("ast")?;
636
637        // Check root
638        if ast.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
639            return Some(ast);
640        }
641
642        let mut stack = vec![ast];
643        while let Some(node) = stack.pop() {
644            if node.get("id").and_then(|v| v.as_u64()) == Some(target_id.0) {
645                return Some(node);
646            }
647            for key in CHILD_KEYS {
648                if let Some(value) = node.get(key) {
649                    match value {
650                        Value::Array(arr) => stack.extend(arr.iter()),
651                        Value::Object(_) => stack.push(value),
652                        _ => {}
653                    }
654                }
655            }
656        }
657    }
658    None
659}
660
661/// Extract documentation text from a node.
662/// Handles both object form `{text: "..."}` and plain string form.
663pub fn extract_documentation(node: &Value) -> Option<String> {
664    let doc = node.get("documentation")?;
665    match doc {
666        Value::Object(_) => doc
667            .get("text")
668            .and_then(|v| v.as_str())
669            .map(|s| s.to_string()),
670        Value::String(s) => Some(s.clone()),
671        _ => None,
672    }
673}
674
675/// Extract the selector from a declaration node.
676///
677/// Returns a [`Selector`] — either a 4-byte [`FuncSelector`] (for functions,
678/// public variables, and errors) or a 32-byte [`EventSelector`] (for events).
679pub fn extract_selector(node: &Value) -> Option<Selector> {
680    let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
681    match node_type {
682        "FunctionDefinition" | "VariableDeclaration" => node
683            .get("functionSelector")
684            .and_then(|v| v.as_str())
685            .map(|s| Selector::Func(FuncSelector::new(s))),
686        "ErrorDefinition" => node
687            .get("errorSelector")
688            .and_then(|v| v.as_str())
689            .map(|s| Selector::Func(FuncSelector::new(s))),
690        "EventDefinition" => node
691            .get("eventSelector")
692            .and_then(|v| v.as_str())
693            .map(|s| Selector::Event(EventSelector::new(s))),
694        _ => None,
695    }
696}
697
698/// Resolve `@inheritdoc ParentName` by matching function selectors.
699///
700/// 1. Parse the parent contract name from `@inheritdoc ParentName`
701/// 2. Get the declaration's `functionSelector`
702/// 3. Find the parent contract in `baseContracts` of the scope contract
703/// 4. Match by selector in the parent's child nodes
704/// 5. Return the matched parent node's documentation
705pub fn resolve_inheritdoc<'a>(
706    sources: &'a Value,
707    decl_node: &'a Value,
708    doc_text: &str,
709) -> Option<String> {
710    // Parse "@inheritdoc ParentName"
711    let parent_name = doc_text
712        .lines()
713        .find_map(|line| {
714            let trimmed = line.trim().trim_start_matches('*').trim();
715            trimmed.strip_prefix("@inheritdoc ")
716        })?
717        .trim();
718
719    // Get the selector from the implementation function
720    let impl_selector = extract_selector(decl_node)?;
721
722    // Get the scope (containing contract id)
723    let scope_id = decl_node.get("scope").and_then(|v| v.as_u64())?;
724
725    // Find the scope contract
726    let scope_contract = find_node_by_id(sources, NodeId(scope_id))?;
727
728    // Find the parent contract in baseContracts by name
729    let base_contracts = scope_contract
730        .get("baseContracts")
731        .and_then(|v| v.as_array())?;
732    let parent_id = base_contracts.iter().find_map(|base| {
733        let name = base
734            .get("baseName")
735            .and_then(|bn| bn.get("name"))
736            .and_then(|n| n.as_str())?;
737        if name == parent_name {
738            base.get("baseName")
739                .and_then(|bn| bn.get("referencedDeclaration"))
740                .and_then(|v| v.as_u64())
741        } else {
742            None
743        }
744    })?;
745
746    // Find the parent contract node
747    let parent_contract = find_node_by_id(sources, NodeId(parent_id))?;
748
749    // Search parent's children for matching selector
750    let parent_nodes = parent_contract.get("nodes").and_then(|v| v.as_array())?;
751    for child in parent_nodes {
752        if let Some(child_selector) = extract_selector(child)
753            && child_selector == impl_selector
754        {
755            return extract_documentation(child);
756        }
757    }
758
759    None
760}
761
762/// Format NatSpec documentation as markdown.
763/// Strips leading `@` tags and formats them nicely.
764/// When `inherited_doc` is provided, it replaces `@inheritdoc` lines with the resolved content.
765pub fn format_natspec(text: &str, inherited_doc: Option<&str>) -> String {
766    let mut lines: Vec<String> = Vec::new();
767    let mut in_params = false;
768    let mut in_returns = false;
769
770    for raw_line in text.lines() {
771        let line = raw_line.trim().trim_start_matches('*').trim();
772        if line.is_empty() {
773            continue;
774        }
775
776        if let Some(rest) = line.strip_prefix("@title ") {
777            in_params = false;
778            in_returns = false;
779            lines.push(format!("**{rest}**"));
780            lines.push(String::new());
781        } else if let Some(rest) = line.strip_prefix("@notice ") {
782            in_params = false;
783            in_returns = false;
784            lines.push(rest.to_string());
785        } else if let Some(rest) = line.strip_prefix("@dev ") {
786            in_params = false;
787            in_returns = false;
788            lines.push(String::new());
789            lines.push("**@dev**".to_string());
790            lines.push(format!("*{rest}*"));
791        } else if let Some(rest) = line.strip_prefix("@param ") {
792            if !in_params {
793                in_params = true;
794                in_returns = false;
795                lines.push(String::new());
796                lines.push("**Parameters:**".to_string());
797            }
798            if let Some((name, desc)) = rest.split_once(' ') {
799                lines.push(format!("- `{name}` — {desc}"));
800            } else {
801                lines.push(format!("- `{rest}`"));
802            }
803        } else if let Some(rest) = line.strip_prefix("@return ") {
804            if !in_returns {
805                in_returns = true;
806                in_params = false;
807                lines.push(String::new());
808                lines.push("**Returns:**".to_string());
809            }
810            if let Some((name, desc)) = rest.split_once(' ') {
811                lines.push(format!("- `{name}` — {desc}"));
812            } else {
813                lines.push(format!("- `{rest}`"));
814            }
815        } else if let Some(rest) = line.strip_prefix("@author ") {
816            in_params = false;
817            in_returns = false;
818            lines.push(format!("*@author {rest}*"));
819        } else if line.starts_with("@inheritdoc ") {
820            // Resolve inherited docs if available
821            if let Some(inherited) = inherited_doc {
822                // Recursively format the inherited doc (it won't have another @inheritdoc)
823                let formatted = format_natspec(inherited, None);
824                if !formatted.is_empty() {
825                    lines.push(formatted);
826                }
827            } else {
828                let parent = line.strip_prefix("@inheritdoc ").unwrap_or("");
829                lines.push(format!("*Inherits documentation from `{parent}`*"));
830            }
831        } else if line.starts_with('@') {
832            // Any other tag (@custom:xyz, @dev, etc.)
833            in_params = false;
834            in_returns = false;
835            if let Some((tag, rest)) = line.split_once(' ') {
836                lines.push(String::new());
837                lines.push(format!("**{tag}**"));
838                lines.push(format!("*{rest}*"));
839            } else {
840                lines.push(String::new());
841                lines.push(format!("**{line}**"));
842            }
843        } else {
844            // Continuation line
845            lines.push(line.to_string());
846        }
847    }
848
849    lines.join("\n")
850}
851
852/// Build a function/modifier signature string from a raw AST node.
853fn build_function_signature(node: &Value) -> Option<String> {
854    let node_type = node.get("nodeType").and_then(|v| v.as_str())?;
855    let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
856
857    match node_type {
858        "FunctionDefinition" => {
859            let kind = node
860                .get("kind")
861                .and_then(|v| v.as_str())
862                .unwrap_or("function");
863            let visibility = node
864                .get("visibility")
865                .and_then(|v| v.as_str())
866                .unwrap_or("");
867            let state_mutability = node
868                .get("stateMutability")
869                .and_then(|v| v.as_str())
870                .unwrap_or("");
871
872            let params = format_parameters(node.get("parameters"));
873            let returns = format_parameters(node.get("returnParameters"));
874
875            let mut sig = match kind {
876                "constructor" => format!("constructor({params})"),
877                "receive" => "receive() external payable".to_string(),
878                "fallback" => format!("fallback({params})"),
879                _ => format!("function {name}({params})"),
880            };
881
882            if !visibility.is_empty() && kind != "constructor" && kind != "receive" {
883                sig.push_str(&format!(" {visibility}"));
884            }
885            if !state_mutability.is_empty() && state_mutability != "nonpayable" {
886                sig.push_str(&format!(" {state_mutability}"));
887            }
888            if !returns.is_empty() {
889                sig.push_str(&format!(" returns ({returns})"));
890            }
891            Some(sig)
892        }
893        "ModifierDefinition" => {
894            let params = format_parameters(node.get("parameters"));
895            Some(format!("modifier {name}({params})"))
896        }
897        "EventDefinition" => {
898            let params = format_parameters(node.get("parameters"));
899            Some(format!("event {name}({params})"))
900        }
901        "ErrorDefinition" => {
902            let params = format_parameters(node.get("parameters"));
903            Some(format!("error {name}({params})"))
904        }
905        "VariableDeclaration" => {
906            let type_str = node
907                .get("typeDescriptions")
908                .and_then(|v| v.get("typeString"))
909                .and_then(|v| v.as_str())
910                .unwrap_or("unknown");
911            let visibility = node
912                .get("visibility")
913                .and_then(|v| v.as_str())
914                .unwrap_or("");
915            let mutability = node
916                .get("mutability")
917                .and_then(|v| v.as_str())
918                .unwrap_or("");
919
920            let mut sig = type_str.to_string();
921            if !visibility.is_empty() {
922                sig.push_str(&format!(" {visibility}"));
923            }
924            if mutability == "constant" || mutability == "immutable" {
925                sig.push_str(&format!(" {mutability}"));
926            }
927            sig.push_str(&format!(" {name}"));
928            Some(sig)
929        }
930        "ContractDefinition" => {
931            let contract_kind = node
932                .get("contractKind")
933                .and_then(|v| v.as_str())
934                .unwrap_or("contract");
935
936            let mut sig = format!("{contract_kind} {name}");
937
938            // Add base contracts
939            if let Some(bases) = node.get("baseContracts").and_then(|v| v.as_array())
940                && !bases.is_empty()
941            {
942                let base_names: Vec<&str> = bases
943                    .iter()
944                    .filter_map(|b| {
945                        b.get("baseName")
946                            .and_then(|bn| bn.get("name"))
947                            .and_then(|n| n.as_str())
948                    })
949                    .collect();
950                if !base_names.is_empty() {
951                    sig.push_str(&format!(" is {}", base_names.join(", ")));
952                }
953            }
954            Some(sig)
955        }
956        "StructDefinition" => {
957            let mut sig = format!("struct {name} {{\n");
958            if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
959                for member in members {
960                    let mname = member.get("name").and_then(|v| v.as_str()).unwrap_or("?");
961                    let mtype = member
962                        .get("typeDescriptions")
963                        .and_then(|v| v.get("typeString"))
964                        .and_then(|v| v.as_str())
965                        .unwrap_or("?");
966                    sig.push_str(&format!("    {mtype} {mname};\n"));
967                }
968            }
969            sig.push('}');
970            Some(sig)
971        }
972        "EnumDefinition" => {
973            let mut sig = format!("enum {name} {{\n");
974            if let Some(members) = node.get("members").and_then(|v| v.as_array()) {
975                let names: Vec<&str> = members
976                    .iter()
977                    .filter_map(|m| m.get("name").and_then(|v| v.as_str()))
978                    .collect();
979                for n in &names {
980                    sig.push_str(&format!("    {n},\n"));
981                }
982            }
983            sig.push('}');
984            Some(sig)
985        }
986        "UserDefinedValueTypeDefinition" => {
987            let underlying = node
988                .get("underlyingType")
989                .and_then(|v| v.get("typeDescriptions"))
990                .and_then(|v| v.get("typeString"))
991                .and_then(|v| v.as_str())
992                .unwrap_or("unknown");
993            Some(format!("type {name} is {underlying}"))
994        }
995        _ => None,
996    }
997}
998
999/// Format parameter list from a parameters node.
1000fn format_parameters(params_node: Option<&Value>) -> String {
1001    let params_node = match params_node {
1002        Some(v) => v,
1003        None => return String::new(),
1004    };
1005    let params = match params_node.get("parameters").and_then(|v| v.as_array()) {
1006        Some(arr) => arr,
1007        None => return String::new(),
1008    };
1009
1010    let parts: Vec<String> = params
1011        .iter()
1012        .map(|p| {
1013            let type_str = p
1014                .get("typeDescriptions")
1015                .and_then(|v| v.get("typeString"))
1016                .and_then(|v| v.as_str())
1017                .unwrap_or("?");
1018            let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
1019            let storage = p
1020                .get("storageLocation")
1021                .and_then(|v| v.as_str())
1022                .unwrap_or("default");
1023
1024            if name.is_empty() {
1025                type_str.to_string()
1026            } else if storage != "default" {
1027                format!("{type_str} {storage} {name}")
1028            } else {
1029                format!("{type_str} {name}")
1030            }
1031        })
1032        .collect();
1033
1034    parts.join(", ")
1035}
1036
1037/// Build gas hover text for a function declaration.
1038fn gas_hover_for_function(
1039    decl_node: &Value,
1040    sources: &Value,
1041    gas_index: &GasIndex,
1042) -> Option<String> {
1043    let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1044    if node_type != "FunctionDefinition" {
1045        return None;
1046    }
1047
1048    // Try by selector first (external/public functions)
1049    if let Some(selector) = decl_node.get("functionSelector").and_then(|v| v.as_str())
1050        && let Some((_contract, cost)) =
1051            gas::gas_by_selector(gas_index, &FuncSelector::new(selector))
1052    {
1053        return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1054    }
1055
1056    // Try by name (internal functions)
1057    let fn_name = decl_node.get("name").and_then(|v| v.as_str())?;
1058    let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1059    let contract_gas = gas_index.get(&contract_key)?;
1060
1061    // Match by name prefix in internal gas estimates
1062    let prefix = format!("{fn_name}(");
1063    for (sig, cost) in &contract_gas.internal {
1064        if sig.starts_with(&prefix) {
1065            return Some(format!("Gas: `{}`", gas::format_gas(cost)));
1066        }
1067    }
1068
1069    None
1070}
1071
1072/// Build gas hover text for a contract declaration.
1073fn gas_hover_for_contract(
1074    decl_node: &Value,
1075    sources: &Value,
1076    gas_index: &GasIndex,
1077) -> Option<String> {
1078    let node_type = decl_node.get("nodeType").and_then(|v| v.as_str())?;
1079    if node_type != "ContractDefinition" {
1080        return None;
1081    }
1082
1083    let contract_key = gas::resolve_contract_key(sources, decl_node, gas_index)?;
1084    let contract_gas = gas_index.get(&contract_key)?;
1085
1086    let mut lines = Vec::new();
1087
1088    // Creation/deploy costs
1089    if !contract_gas.creation.is_empty() {
1090        lines.push("**Deploy Cost**".to_string());
1091        if let Some(cost) = contract_gas.creation.get("totalCost") {
1092            lines.push(format!("- Total: `{}`", gas::format_gas(cost)));
1093        }
1094        if let Some(cost) = contract_gas.creation.get("codeDepositCost") {
1095            lines.push(format!("- Code deposit: `{}`", gas::format_gas(cost)));
1096        }
1097        if let Some(cost) = contract_gas.creation.get("executionCost") {
1098            lines.push(format!("- Execution: `{}`", gas::format_gas(cost)));
1099        }
1100    }
1101
1102    // External function gas
1103    if !contract_gas.external_by_sig.is_empty() {
1104        lines.push(String::new());
1105        lines.push("**Function Gas**".to_string());
1106
1107        let mut fns: Vec<(&MethodId, &String)> = contract_gas.external_by_sig.iter().collect();
1108        fns.sort_by_key(|(k, _)| k.as_str().to_string());
1109
1110        for (sig, cost) in fns {
1111            lines.push(format!("- `{}`: `{}`", sig.name(), gas::format_gas(cost)));
1112        }
1113    }
1114
1115    if lines.is_empty() {
1116        return None;
1117    }
1118
1119    Some(lines.join("\n"))
1120}
1121
1122/// Produce hover information for the symbol at the given position.
1123pub fn hover_info(
1124    ast_data: &Value,
1125    file_uri: &Url,
1126    position: Position,
1127    source_bytes: &[u8],
1128    gas_index: &GasIndex,
1129    doc_index: &DocIndex,
1130    hint_index: &HintIndex,
1131) -> Option<Hover> {
1132    let sources = ast_data.get("sources")?;
1133    let source_id_to_path = ast_data
1134        .get("source_id_to_path")
1135        .and_then(|v| v.as_object())?;
1136
1137    let id_to_path: HashMap<String, String> = source_id_to_path
1138        .iter()
1139        .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
1140        .collect();
1141
1142    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
1143
1144    // Resolve the file path
1145    let file_path = file_uri.to_file_path().ok()?;
1146    let file_path_str = file_path.to_str()?;
1147
1148    // Find the absolute path for this file
1149    let abs_path = path_to_abs
1150        .iter()
1151        .find(|(k, _)| file_path_str.ends_with(k.as_str()))
1152        .map(|(_, v)| v.clone())?;
1153
1154    let byte_pos = pos_to_bytes(source_bytes, position);
1155
1156    // Resolve: first try Yul external refs, then normal node lookup
1157    let node_id = byte_to_decl_via_external_refs(&external_refs, &id_to_path, &abs_path, byte_pos)
1158        .or_else(|| byte_to_id(&nodes, &abs_path, byte_pos))?;
1159
1160    // Get the NodeInfo for this node
1161    let node_info = nodes
1162        .values()
1163        .find_map(|file_nodes| file_nodes.get(&node_id))?;
1164
1165    // Follow referenced_declaration to the declaration node
1166    let decl_id = node_info.referenced_declaration.unwrap_or(node_id);
1167
1168    // Find the raw AST node for the declaration
1169    let decl_node = find_node_by_id(sources, decl_id)?;
1170
1171    // Build hover content
1172    let mut parts: Vec<String> = Vec::new();
1173
1174    // Signature in a code block
1175    if let Some(sig) = build_function_signature(decl_node) {
1176        parts.push(format!("```solidity\n{sig}\n```"));
1177    } else {
1178        // Fallback: show type description for any node
1179        if let Some(type_str) = decl_node
1180            .get("typeDescriptions")
1181            .and_then(|v| v.get("typeString"))
1182            .and_then(|v| v.as_str())
1183        {
1184            let name = decl_node.get("name").and_then(|v| v.as_str()).unwrap_or("");
1185            parts.push(format!("```solidity\n{type_str} {name}\n```"));
1186        }
1187    }
1188
1189    // Selector (function, error, or event)
1190    if let Some(selector) = extract_selector(decl_node) {
1191        parts.push(format!("Selector: `{}`", selector.to_prefixed()));
1192    }
1193
1194    // Gas estimates
1195    if !gas_index.is_empty() {
1196        if let Some(gas_text) = gas_hover_for_function(decl_node, sources, gas_index) {
1197            parts.push(gas_text);
1198        } else if let Some(gas_text) = gas_hover_for_contract(decl_node, sources, gas_index) {
1199            parts.push(gas_text);
1200        }
1201    }
1202
1203    // Documentation — try userdoc/devdoc first, fall back to AST docs
1204    if let Some(doc_entry) = lookup_doc_entry(doc_index, decl_node, sources) {
1205        let formatted = format_doc_entry(&doc_entry);
1206        if !formatted.is_empty() {
1207            parts.push(format!("---\n{formatted}"));
1208        }
1209    } else if let Some(doc_text) = extract_documentation(decl_node) {
1210        let inherited_doc = resolve_inheritdoc(sources, decl_node, &doc_text);
1211        let formatted = format_natspec(&doc_text, inherited_doc.as_deref());
1212        if !formatted.is_empty() {
1213            parts.push(format!("---\n{formatted}"));
1214        }
1215    } else if let Some(param_doc) = lookup_param_doc(doc_index, decl_node, sources) {
1216        // Parameter/return value — show the @param/@return description from parent
1217        if !param_doc.is_empty() {
1218            parts.push(format!("---\n{param_doc}"));
1219        }
1220    }
1221
1222    // Call-site parameter doc: when the hovered node is used as an argument
1223    // in a function call, show the @param doc from the called function's definition.
1224    // Uses tree-sitter on the live buffer to find the enclosing call and argument
1225    // index, then resolves via HintIndex for the param name and declaration id.
1226    if let Some(hint_lookup) = hint_index.get(&abs_path) {
1227        let source_str = String::from_utf8_lossy(source_bytes);
1228        if let Some(tree) = crate::inlay_hints::ts_parse(&source_str) {
1229            if let Some(ctx) =
1230                crate::inlay_hints::ts_find_call_at_byte(tree.root_node(), &source_str, byte_pos)
1231            {
1232                if let Some(resolved) = hint_lookup.resolve_callsite_param(
1233                    ctx.call_start_byte,
1234                    ctx.name,
1235                    ctx.arg_count,
1236                    ctx.arg_index,
1237                ) {
1238                    // Look up @param doc using the declaration id
1239                    let fn_decl = find_node_by_id(sources, NodeId(resolved.decl_id));
1240                    let param_doc = fn_decl.and_then(|decl| {
1241                        // Try DocIndex first (structured devdoc)
1242                        if let Some(doc_entry) = lookup_doc_entry(doc_index, decl, sources) {
1243                            for (pname, pdesc) in &doc_entry.params {
1244                                if pname == &resolved.param_name {
1245                                    return Some(pdesc.clone());
1246                                }
1247                            }
1248                        }
1249                        // Fallback: parse raw NatSpec on the function definition
1250                        if let Some(doc_text) = extract_documentation(decl) {
1251                            let resolved_doc = if doc_text.contains("@inheritdoc") {
1252                                resolve_inheritdoc(sources, decl, &doc_text)
1253                            } else {
1254                                None
1255                            };
1256                            let text = resolved_doc.as_deref().unwrap_or(&doc_text);
1257                            for line in text.lines() {
1258                                let trimmed = line.trim().trim_start_matches('*').trim();
1259                                if let Some(rest) = trimmed.strip_prefix("@param ") {
1260                                    if let Some((name, desc)) = rest.split_once(' ') {
1261                                        if name == resolved.param_name {
1262                                            return Some(desc.to_string());
1263                                        }
1264                                    }
1265                                }
1266                            }
1267                        }
1268                        None
1269                    });
1270                    if let Some(desc) = param_doc {
1271                        if !desc.is_empty() {
1272                            parts.push(format!("**@param `{}`** — {desc}", resolved.param_name));
1273                        }
1274                    }
1275                }
1276            }
1277        }
1278    }
1279
1280    if parts.is_empty() {
1281        return None;
1282    }
1283
1284    Some(Hover {
1285        contents: HoverContents::Markup(MarkupContent {
1286            kind: MarkupKind::Markdown,
1287            value: parts.join("\n\n"),
1288        }),
1289        range: None,
1290    })
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::*;
1296
1297    fn load_test_ast() -> Value {
1298        let data = std::fs::read_to_string("pool-manager-ast.json").expect("test fixture");
1299        let raw: Value = serde_json::from_str(&data).expect("valid json");
1300        crate::solc::normalize_forge_output(raw)
1301    }
1302
1303    #[test]
1304    fn test_find_node_by_id_pool_manager() {
1305        let ast = load_test_ast();
1306        let sources = ast.get("sources").unwrap();
1307        let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1308        assert_eq!(
1309            node.get("name").and_then(|v| v.as_str()),
1310            Some("PoolManager")
1311        );
1312        assert_eq!(
1313            node.get("nodeType").and_then(|v| v.as_str()),
1314            Some("ContractDefinition")
1315        );
1316    }
1317
1318    #[test]
1319    fn test_find_node_by_id_initialize() {
1320        let ast = load_test_ast();
1321        let sources = ast.get("sources").unwrap();
1322        // IPoolManager.initialize has the full docs
1323        let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1324        assert_eq!(
1325            node.get("name").and_then(|v| v.as_str()),
1326            Some("initialize")
1327        );
1328    }
1329
1330    #[test]
1331    fn test_extract_documentation_object() {
1332        let ast = load_test_ast();
1333        let sources = ast.get("sources").unwrap();
1334        // IPoolManager.initialize (id=2411) has full NatSpec
1335        let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1336        let doc = extract_documentation(node).unwrap();
1337        assert!(doc.contains("@notice"));
1338        assert!(doc.contains("@param key"));
1339    }
1340
1341    #[test]
1342    fn test_extract_documentation_none() {
1343        let ast = load_test_ast();
1344        let sources = ast.get("sources").unwrap();
1345        // PoolKey struct (id=8887) — check if it has docs
1346        let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1347        // PoolKey may or may not have docs, just verify no crash
1348        let _ = extract_documentation(node);
1349    }
1350
1351    #[test]
1352    fn test_format_natspec_notice_and_params() {
1353        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";
1354        let formatted = format_natspec(text, None);
1355        assert!(formatted.contains("Initialize the state"));
1356        assert!(formatted.contains("**Parameters:**"));
1357        assert!(formatted.contains("`key`"));
1358        assert!(formatted.contains("**Returns:**"));
1359        assert!(formatted.contains("`tick`"));
1360    }
1361
1362    #[test]
1363    fn test_format_natspec_inheritdoc() {
1364        let text = "@inheritdoc IPoolManager";
1365        let formatted = format_natspec(text, None);
1366        assert!(formatted.contains("Inherits documentation from `IPoolManager`"));
1367    }
1368
1369    #[test]
1370    fn test_format_natspec_dev() {
1371        let text = "@notice Do something\n @dev This is an implementation detail";
1372        let formatted = format_natspec(text, None);
1373        assert!(formatted.contains("Do something"));
1374        assert!(formatted.contains("**@dev**"));
1375        assert!(formatted.contains("*This is an implementation detail*"));
1376    }
1377
1378    #[test]
1379    fn test_format_natspec_custom_tag() {
1380        let text = "@notice Do something\n @custom:security Non-reentrant";
1381        let formatted = format_natspec(text, None);
1382        assert!(formatted.contains("Do something"));
1383        assert!(formatted.contains("**@custom:security**"));
1384        assert!(formatted.contains("*Non-reentrant*"));
1385    }
1386
1387    #[test]
1388    fn test_build_function_signature_initialize() {
1389        let ast = load_test_ast();
1390        let sources = ast.get("sources").unwrap();
1391        let node = find_node_by_id(sources, NodeId(2411)).unwrap();
1392        let sig = build_function_signature(node).unwrap();
1393        assert!(sig.starts_with("function initialize("));
1394        assert!(sig.contains("returns"));
1395    }
1396
1397    #[test]
1398    fn test_build_signature_contract() {
1399        let ast = load_test_ast();
1400        let sources = ast.get("sources").unwrap();
1401        let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1402        let sig = build_function_signature(node).unwrap();
1403        assert!(sig.contains("contract PoolManager"));
1404        assert!(sig.contains(" is "));
1405    }
1406
1407    #[test]
1408    fn test_build_signature_struct() {
1409        let ast = load_test_ast();
1410        let sources = ast.get("sources").unwrap();
1411        let node = find_node_by_id(sources, NodeId(8887)).unwrap();
1412        let sig = build_function_signature(node).unwrap();
1413        assert!(sig.starts_with("struct PoolKey"));
1414        assert!(sig.contains('{'));
1415    }
1416
1417    #[test]
1418    fn test_build_signature_error() {
1419        let ast = load_test_ast();
1420        let sources = ast.get("sources").unwrap();
1421        // Find an ErrorDefinition
1422        let node = find_node_by_id(sources, NodeId(508)).unwrap();
1423        assert_eq!(
1424            node.get("nodeType").and_then(|v| v.as_str()),
1425            Some("ErrorDefinition")
1426        );
1427        let sig = build_function_signature(node).unwrap();
1428        assert!(sig.starts_with("error "));
1429    }
1430
1431    #[test]
1432    fn test_build_signature_event() {
1433        let ast = load_test_ast();
1434        let sources = ast.get("sources").unwrap();
1435        // Find an EventDefinition
1436        let node = find_node_by_id(sources, NodeId(8)).unwrap();
1437        assert_eq!(
1438            node.get("nodeType").and_then(|v| v.as_str()),
1439            Some("EventDefinition")
1440        );
1441        let sig = build_function_signature(node).unwrap();
1442        assert!(sig.starts_with("event "));
1443    }
1444
1445    #[test]
1446    fn test_build_signature_variable() {
1447        let ast = load_test_ast();
1448        let sources = ast.get("sources").unwrap();
1449        // Find a VariableDeclaration with documentation — check a state var
1450        // PoolManager has state variables, find one
1451        let pm = find_node_by_id(sources, NodeId(1767)).unwrap();
1452        if let Some(nodes) = pm.get("nodes").and_then(|v| v.as_array()) {
1453            for node in nodes {
1454                if node.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration") {
1455                    let sig = build_function_signature(node);
1456                    assert!(sig.is_some());
1457                    break;
1458                }
1459            }
1460        }
1461    }
1462
1463    #[test]
1464    fn test_pool_manager_has_documentation() {
1465        let ast = load_test_ast();
1466        let sources = ast.get("sources").unwrap();
1467        // Owned contract (id=59) has NatSpec
1468        let node = find_node_by_id(sources, NodeId(59)).unwrap();
1469        let doc = extract_documentation(node).unwrap();
1470        assert!(doc.contains("@notice"));
1471    }
1472
1473    #[test]
1474    fn test_format_parameters_empty() {
1475        let result = format_parameters(None);
1476        assert_eq!(result, "");
1477    }
1478
1479    #[test]
1480    fn test_format_parameters_with_data() {
1481        let params: Value = serde_json::json!({
1482            "parameters": [
1483                {
1484                    "name": "key",
1485                    "typeDescriptions": { "typeString": "struct PoolKey" },
1486                    "storageLocation": "memory"
1487                },
1488                {
1489                    "name": "sqrtPriceX96",
1490                    "typeDescriptions": { "typeString": "uint160" },
1491                    "storageLocation": "default"
1492                }
1493            ]
1494        });
1495        let result = format_parameters(Some(&params));
1496        assert!(result.contains("struct PoolKey memory key"));
1497        assert!(result.contains("uint160 sqrtPriceX96"));
1498    }
1499
1500    // --- Selector tests ---
1501
1502    #[test]
1503    fn test_extract_selector_function() {
1504        let ast = load_test_ast();
1505        let sources = ast.get("sources").unwrap();
1506        // PoolManager.swap (id=1167) has functionSelector "f3cd914c"
1507        let node = find_node_by_id(sources, NodeId(1167)).unwrap();
1508        let selector = extract_selector(node).unwrap();
1509        assert_eq!(selector, Selector::Func(FuncSelector::new("f3cd914c")));
1510        assert_eq!(selector.as_hex(), "f3cd914c");
1511    }
1512
1513    #[test]
1514    fn test_extract_selector_error() {
1515        let ast = load_test_ast();
1516        let sources = ast.get("sources").unwrap();
1517        // DelegateCallNotAllowed (id=508) has errorSelector
1518        let node = find_node_by_id(sources, NodeId(508)).unwrap();
1519        let selector = extract_selector(node).unwrap();
1520        assert_eq!(selector, Selector::Func(FuncSelector::new("0d89438e")));
1521        assert_eq!(selector.as_hex(), "0d89438e");
1522    }
1523
1524    #[test]
1525    fn test_extract_selector_event() {
1526        let ast = load_test_ast();
1527        let sources = ast.get("sources").unwrap();
1528        // OwnershipTransferred (id=8) has eventSelector
1529        let node = find_node_by_id(sources, NodeId(8)).unwrap();
1530        let selector = extract_selector(node).unwrap();
1531        assert!(matches!(selector, Selector::Event(_)));
1532        assert_eq!(selector.as_hex().len(), 64); // 32-byte keccak hash
1533    }
1534
1535    #[test]
1536    fn test_extract_selector_public_variable() {
1537        let ast = load_test_ast();
1538        let sources = ast.get("sources").unwrap();
1539        // owner (id=10) is public, has functionSelector
1540        let node = find_node_by_id(sources, NodeId(10)).unwrap();
1541        let selector = extract_selector(node).unwrap();
1542        assert_eq!(selector, Selector::Func(FuncSelector::new("8da5cb5b")));
1543    }
1544
1545    #[test]
1546    fn test_extract_selector_internal_function_none() {
1547        let ast = load_test_ast();
1548        let sources = ast.get("sources").unwrap();
1549        // Pool.swap (id=5960) is internal, no selector
1550        let node = find_node_by_id(sources, NodeId(5960)).unwrap();
1551        assert!(extract_selector(node).is_none());
1552    }
1553
1554    // --- @inheritdoc resolution tests ---
1555
1556    #[test]
1557    fn test_resolve_inheritdoc_swap() {
1558        let ast = load_test_ast();
1559        let sources = ast.get("sources").unwrap();
1560        // PoolManager.swap (id=1167) has "@inheritdoc IPoolManager"
1561        let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1562        let doc_text = extract_documentation(decl).unwrap();
1563        assert!(doc_text.contains("@inheritdoc"));
1564
1565        let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1566        assert!(resolved.contains("@notice"));
1567        assert!(resolved.contains("Swap against the given pool"));
1568    }
1569
1570    #[test]
1571    fn test_resolve_inheritdoc_initialize() {
1572        let ast = load_test_ast();
1573        let sources = ast.get("sources").unwrap();
1574        // PoolManager.initialize (id=881) has "@inheritdoc IPoolManager"
1575        let decl = find_node_by_id(sources, NodeId(881)).unwrap();
1576        let doc_text = extract_documentation(decl).unwrap();
1577
1578        let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1579        assert!(resolved.contains("Initialize the state"));
1580        assert!(resolved.contains("@param key"));
1581    }
1582
1583    #[test]
1584    fn test_resolve_inheritdoc_extsload_overload() {
1585        let ast = load_test_ast();
1586        let sources = ast.get("sources").unwrap();
1587
1588        // extsload(bytes32) — id=442, selector "1e2eaeaf"
1589        let decl = find_node_by_id(sources, NodeId(442)).unwrap();
1590        let doc_text = extract_documentation(decl).unwrap();
1591        let resolved = resolve_inheritdoc(sources, decl, &doc_text).unwrap();
1592        assert!(resolved.contains("granular pool state"));
1593        // Should match the single-slot overload doc
1594        assert!(resolved.contains("@param slot"));
1595
1596        // extsload(bytes32, uint256) — id=455, selector "35fd631a"
1597        let decl2 = find_node_by_id(sources, NodeId(455)).unwrap();
1598        let doc_text2 = extract_documentation(decl2).unwrap();
1599        let resolved2 = resolve_inheritdoc(sources, decl2, &doc_text2).unwrap();
1600        assert!(resolved2.contains("@param startSlot"));
1601
1602        // extsload(bytes32[]) — id=467, selector "dbd035ff"
1603        let decl3 = find_node_by_id(sources, NodeId(467)).unwrap();
1604        let doc_text3 = extract_documentation(decl3).unwrap();
1605        let resolved3 = resolve_inheritdoc(sources, decl3, &doc_text3).unwrap();
1606        assert!(resolved3.contains("sparse pool state"));
1607    }
1608
1609    #[test]
1610    fn test_resolve_inheritdoc_formats_in_hover() {
1611        let ast = load_test_ast();
1612        let sources = ast.get("sources").unwrap();
1613        // PoolManager.swap with @inheritdoc — verify format_natspec resolves it
1614        let decl = find_node_by_id(sources, NodeId(1167)).unwrap();
1615        let doc_text = extract_documentation(decl).unwrap();
1616        let inherited = resolve_inheritdoc(sources, decl, &doc_text);
1617        let formatted = format_natspec(&doc_text, inherited.as_deref());
1618        // Should have the resolved content, not "@inheritdoc"
1619        assert!(!formatted.contains("@inheritdoc"));
1620        assert!(formatted.contains("Swap against the given pool"));
1621        assert!(formatted.contains("**Parameters:**"));
1622    }
1623
1624    // --- Parameter/return doc tests ---
1625
1626    #[test]
1627    fn test_param_doc_error_parameter() {
1628        let ast = load_test_ast();
1629        let sources = ast.get("sources").unwrap();
1630        let doc_index = build_doc_index(&ast);
1631
1632        // PriceLimitAlreadyExceeded.sqrtPriceCurrentX96 (id=4760)
1633        let param_node = find_node_by_id(sources, NodeId(4760)).unwrap();
1634        assert_eq!(
1635            param_node.get("name").and_then(|v| v.as_str()),
1636            Some("sqrtPriceCurrentX96")
1637        );
1638
1639        let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1640        assert!(
1641            doc.contains("invalid"),
1642            "should describe the invalid price: {doc}"
1643        );
1644    }
1645
1646    #[test]
1647    fn test_param_doc_error_second_parameter() {
1648        let ast = load_test_ast();
1649        let sources = ast.get("sources").unwrap();
1650        let doc_index = build_doc_index(&ast);
1651
1652        // PriceLimitAlreadyExceeded.sqrtPriceLimitX96 (id=4762)
1653        let param_node = find_node_by_id(sources, NodeId(4762)).unwrap();
1654        let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1655        assert!(
1656            doc.contains("surpassed price limit"),
1657            "should describe the surpassed limit: {doc}"
1658        );
1659    }
1660
1661    #[test]
1662    fn test_param_doc_function_return_value() {
1663        let ast = load_test_ast();
1664        let sources = ast.get("sources").unwrap();
1665        let doc_index = build_doc_index(&ast);
1666
1667        // Pool.modifyLiquidity return param "delta" (id=4994)
1668        let param_node = find_node_by_id(sources, NodeId(4994)).unwrap();
1669        assert_eq!(
1670            param_node.get("name").and_then(|v| v.as_str()),
1671            Some("delta")
1672        );
1673
1674        let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1675        assert!(
1676            doc.contains("deltas of the token balances"),
1677            "should have return doc: {doc}"
1678        );
1679    }
1680
1681    #[test]
1682    fn test_param_doc_function_input_parameter() {
1683        let ast = load_test_ast();
1684        let sources = ast.get("sources").unwrap();
1685        let doc_index = build_doc_index(&ast);
1686
1687        // Pool.modifyLiquidity input param "params" (id 4992 or similar)
1688        // Find it via the function's parameters
1689        let fn_node = find_node_by_id(sources, NodeId(5310)).unwrap();
1690        let params_arr = fn_node
1691            .get("parameters")
1692            .and_then(|p| p.get("parameters"))
1693            .and_then(|p| p.as_array())
1694            .unwrap();
1695        let params_param = params_arr
1696            .iter()
1697            .find(|p| p.get("name").and_then(|v| v.as_str()) == Some("params"))
1698            .unwrap();
1699
1700        let doc = lookup_param_doc(&doc_index, params_param, sources).unwrap();
1701        assert!(
1702            doc.contains("position details"),
1703            "should have param doc: {doc}"
1704        );
1705    }
1706
1707    #[test]
1708    fn test_param_doc_inherited_function_via_docindex() {
1709        let ast = load_test_ast();
1710        let sources = ast.get("sources").unwrap();
1711        let doc_index = build_doc_index(&ast);
1712
1713        // PoolManager.swap `key` param (id=1029) — parent has @inheritdoc IPoolManager
1714        // The DocIndex should have the resolved devdoc from IPoolManager
1715        let param_node = find_node_by_id(sources, NodeId(1029)).unwrap();
1716        assert_eq!(param_node.get("name").and_then(|v| v.as_str()), Some("key"));
1717
1718        let doc = lookup_param_doc(&doc_index, param_node, sources).unwrap();
1719        assert!(
1720            doc.contains("pool to swap"),
1721            "should have inherited param doc: {doc}"
1722        );
1723    }
1724
1725    #[test]
1726    fn test_param_doc_non_parameter_returns_none() {
1727        let ast = load_test_ast();
1728        let sources = ast.get("sources").unwrap();
1729        let doc_index = build_doc_index(&ast);
1730
1731        // PoolManager contract (id=1767) is not a parameter
1732        let node = find_node_by_id(sources, NodeId(1767)).unwrap();
1733        assert!(lookup_param_doc(&doc_index, node, sources).is_none());
1734    }
1735
1736    // ── DocIndex integration tests (poolmanager.json) ──
1737
1738    fn load_solc_fixture() -> Value {
1739        let data = std::fs::read_to_string("poolmanager.json").expect("test fixture");
1740        let raw: Value = serde_json::from_str(&data).expect("valid json");
1741        crate::solc::normalize_solc_output(raw, None)
1742    }
1743
1744    #[test]
1745    fn test_doc_index_is_not_empty() {
1746        let ast = load_solc_fixture();
1747        let index = build_doc_index(&ast);
1748        assert!(!index.is_empty(), "DocIndex should contain entries");
1749    }
1750
1751    #[test]
1752    fn test_doc_index_has_contract_entries() {
1753        let ast = load_solc_fixture();
1754        let index = build_doc_index(&ast);
1755
1756        // PoolManager has both title and notice
1757        let pm_keys: Vec<_> = index
1758            .keys()
1759            .filter(|k| matches!(k, DocKey::Contract(s) if s.contains("PoolManager")))
1760            .collect();
1761        assert!(
1762            !pm_keys.is_empty(),
1763            "should have a Contract entry for PoolManager"
1764        );
1765
1766        let pm_key = DocKey::Contract(
1767            "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
1768        );
1769        let entry = index.get(&pm_key).expect("PoolManager contract entry");
1770        assert_eq!(entry.title.as_deref(), Some("PoolManager"));
1771        assert_eq!(
1772            entry.notice.as_deref(),
1773            Some("Holds the state for all pools")
1774        );
1775    }
1776
1777    #[test]
1778    fn test_doc_index_has_function_by_selector() {
1779        let ast = load_solc_fixture();
1780        let index = build_doc_index(&ast);
1781
1782        // initialize selector = 6276cbbe
1783        let init_key = DocKey::Func(FuncSelector::new("6276cbbe"));
1784        let entry = index
1785            .get(&init_key)
1786            .expect("should have initialize by selector");
1787        assert_eq!(
1788            entry.notice.as_deref(),
1789            Some("Initialize the state for a given pool ID")
1790        );
1791        assert!(
1792            entry
1793                .details
1794                .as_deref()
1795                .unwrap_or("")
1796                .contains("MAX_SWAP_FEE"),
1797            "devdoc details should mention MAX_SWAP_FEE"
1798        );
1799        // params: key, sqrtPriceX96
1800        let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
1801        assert!(param_names.contains(&"key"), "should have param 'key'");
1802        assert!(
1803            param_names.contains(&"sqrtPriceX96"),
1804            "should have param 'sqrtPriceX96'"
1805        );
1806        // returns: tick
1807        let return_names: Vec<&str> = entry.returns.iter().map(|(n, _)| n.as_str()).collect();
1808        assert!(return_names.contains(&"tick"), "should have return 'tick'");
1809    }
1810
1811    #[test]
1812    fn test_doc_index_swap_by_selector() {
1813        let ast = load_solc_fixture();
1814        let index = build_doc_index(&ast);
1815
1816        // swap selector = f3cd914c
1817        let swap_key = DocKey::Func(FuncSelector::new("f3cd914c"));
1818        let entry = index.get(&swap_key).expect("should have swap by selector");
1819        assert!(
1820            entry
1821                .notice
1822                .as_deref()
1823                .unwrap_or("")
1824                .contains("Swap against the given pool"),
1825            "swap notice should describe swapping"
1826        );
1827        // devdoc params: key, params, hookData
1828        assert!(
1829            !entry.params.is_empty(),
1830            "swap should have param documentation"
1831        );
1832    }
1833
1834    #[test]
1835    fn test_doc_index_settle_by_selector() {
1836        let ast = load_solc_fixture();
1837        let index = build_doc_index(&ast);
1838
1839        // settle() selector = 11da60b4
1840        let key = DocKey::Func(FuncSelector::new("11da60b4"));
1841        let entry = index.get(&key).expect("should have settle by selector");
1842        assert!(
1843            entry.notice.is_some(),
1844            "settle should have a notice from userdoc"
1845        );
1846    }
1847
1848    #[test]
1849    fn test_doc_index_has_error_entries() {
1850        let ast = load_solc_fixture();
1851        let index = build_doc_index(&ast);
1852
1853        // AlreadyUnlocked() → keccak256("AlreadyUnlocked()")[0..4]
1854        let selector = compute_selector("AlreadyUnlocked()");
1855        let key = DocKey::Func(FuncSelector::new(&selector));
1856        let entry = index.get(&key).expect("should have AlreadyUnlocked error");
1857        assert!(
1858            entry
1859                .notice
1860                .as_deref()
1861                .unwrap_or("")
1862                .contains("already unlocked"),
1863            "AlreadyUnlocked notice: {:?}",
1864            entry.notice
1865        );
1866    }
1867
1868    #[test]
1869    fn test_doc_index_error_with_params() {
1870        let ast = load_solc_fixture();
1871        let index = build_doc_index(&ast);
1872
1873        // CurrenciesOutOfOrderOrEqual(address,address) has a notice
1874        let selector = compute_selector("CurrenciesOutOfOrderOrEqual(address,address)");
1875        let key = DocKey::Func(FuncSelector::new(&selector));
1876        let entry = index
1877            .get(&key)
1878            .expect("should have CurrenciesOutOfOrderOrEqual error");
1879        assert!(entry.notice.is_some(), "error should have notice");
1880    }
1881
1882    #[test]
1883    fn test_doc_index_has_event_entries() {
1884        let ast = load_solc_fixture();
1885        let index = build_doc_index(&ast);
1886
1887        // Count event entries
1888        let event_count = index
1889            .keys()
1890            .filter(|k| matches!(k, DocKey::Event(_)))
1891            .count();
1892        assert!(event_count > 0, "should have event entries in the DocIndex");
1893    }
1894
1895    #[test]
1896    fn test_doc_index_swap_event() {
1897        let ast = load_solc_fixture();
1898        let index = build_doc_index(&ast);
1899
1900        // Swap event topic = keccak256("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)")
1901        let topic =
1902            compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
1903        let key = DocKey::Event(EventSelector::new(&topic));
1904        let entry = index.get(&key).expect("should have Swap event");
1905
1906        // userdoc notice
1907        assert!(
1908            entry
1909                .notice
1910                .as_deref()
1911                .unwrap_or("")
1912                .contains("swaps between currency0 and currency1"),
1913            "Swap event notice: {:?}",
1914            entry.notice
1915        );
1916
1917        // devdoc params (amount0, amount1, id, sender, sqrtPriceX96, etc.)
1918        let param_names: Vec<&str> = entry.params.iter().map(|(n, _)| n.as_str()).collect();
1919        assert!(
1920            param_names.contains(&"amount0"),
1921            "should have param 'amount0'"
1922        );
1923        assert!(
1924            param_names.contains(&"sender"),
1925            "should have param 'sender'"
1926        );
1927        assert!(param_names.contains(&"id"), "should have param 'id'");
1928    }
1929
1930    #[test]
1931    fn test_doc_index_initialize_event() {
1932        let ast = load_solc_fixture();
1933        let index = build_doc_index(&ast);
1934
1935        let topic = compute_event_topic(
1936            "Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)",
1937        );
1938        let key = DocKey::Event(EventSelector::new(&topic));
1939        let entry = index.get(&key).expect("should have Initialize event");
1940        assert!(
1941            !entry.params.is_empty(),
1942            "Initialize event should have param docs"
1943        );
1944    }
1945
1946    #[test]
1947    fn test_doc_index_no_state_variables_for_pool_manager() {
1948        let ast = load_solc_fixture();
1949        let index = build_doc_index(&ast);
1950
1951        // PoolManager has no devdoc.stateVariables, so no StateVar keys for it
1952        let sv_count = index
1953            .keys()
1954            .filter(|k| matches!(k, DocKey::StateVar(s) if s.contains("PoolManager")))
1955            .count();
1956        assert_eq!(
1957            sv_count, 0,
1958            "PoolManager should have no state variable doc entries"
1959        );
1960    }
1961
1962    #[test]
1963    fn test_doc_index_multiple_contracts() {
1964        let ast = load_solc_fixture();
1965        let index = build_doc_index(&ast);
1966
1967        // Should have contract entries for multiple contracts (ERC6909, Extsload, IPoolManager, etc.)
1968        let contract_count = index
1969            .keys()
1970            .filter(|k| matches!(k, DocKey::Contract(_)))
1971            .count();
1972        assert!(
1973            contract_count >= 5,
1974            "should have at least 5 contract-level entries, got {contract_count}"
1975        );
1976    }
1977
1978    #[test]
1979    fn test_doc_index_func_key_count() {
1980        let ast = load_solc_fixture();
1981        let index = build_doc_index(&ast);
1982
1983        let func_count = index
1984            .keys()
1985            .filter(|k| matches!(k, DocKey::Func(_)))
1986            .count();
1987        // We have methods + errors keyed by selector across all 43 contracts
1988        assert!(
1989            func_count >= 30,
1990            "should have at least 30 Func entries (methods + errors), got {func_count}"
1991        );
1992    }
1993
1994    #[test]
1995    fn test_doc_index_format_initialize_entry() {
1996        let ast = load_solc_fixture();
1997        let index = build_doc_index(&ast);
1998
1999        let key = DocKey::Func(FuncSelector::new("6276cbbe"));
2000        let entry = index.get(&key).expect("initialize entry");
2001        let formatted = format_doc_entry(entry);
2002
2003        assert!(
2004            formatted.contains("Initialize the state for a given pool ID"),
2005            "formatted should include notice"
2006        );
2007        assert!(
2008            formatted.contains("**@dev**"),
2009            "formatted should include dev section"
2010        );
2011        assert!(
2012            formatted.contains("**Parameters:**"),
2013            "formatted should include parameters"
2014        );
2015        assert!(
2016            formatted.contains("`key`"),
2017            "formatted should include key param"
2018        );
2019        assert!(
2020            formatted.contains("**Returns:**"),
2021            "formatted should include returns"
2022        );
2023        assert!(
2024            formatted.contains("`tick`"),
2025            "formatted should include tick return"
2026        );
2027    }
2028
2029    #[test]
2030    fn test_doc_index_format_contract_entry() {
2031        let ast = load_solc_fixture();
2032        let index = build_doc_index(&ast);
2033
2034        let key = DocKey::Contract(
2035            "/Users/meek/developer/mmsaki/solidity-language-server/v4-core/src/PoolManager.sol:PoolManager".to_string(),
2036        );
2037        let entry = index.get(&key).expect("PoolManager contract entry");
2038        let formatted = format_doc_entry(entry);
2039
2040        assert!(
2041            formatted.contains("**PoolManager**"),
2042            "should include bold title"
2043        );
2044        assert!(
2045            formatted.contains("Holds the state for all pools"),
2046            "should include notice"
2047        );
2048    }
2049
2050    #[test]
2051    fn test_doc_index_inherited_docs_resolved() {
2052        let ast = load_solc_fixture();
2053        let index = build_doc_index(&ast);
2054
2055        // Both PoolManager and IPoolManager define methods with the same selector.
2056        // The last one written wins (PoolManager overwrites IPoolManager for same selector).
2057        // Either way, swap(f3cd914c) should have full docs, not just "@inheritdoc".
2058        let key = DocKey::Func(FuncSelector::new("f3cd914c"));
2059        let entry = index.get(&key).expect("swap entry");
2060        // The notice should be the resolved text, not "@inheritdoc IPoolManager"
2061        let notice = entry.notice.as_deref().unwrap_or("");
2062        assert!(
2063            !notice.contains("@inheritdoc"),
2064            "userdoc/devdoc should have resolved inherited docs, not raw @inheritdoc"
2065        );
2066    }
2067
2068    #[test]
2069    fn test_compute_selector_known_values() {
2070        // keccak256("AlreadyUnlocked()") first 4 bytes
2071        let sel = compute_selector("AlreadyUnlocked()");
2072        assert_eq!(sel.len(), 8, "selector should be 8 hex chars");
2073
2074        // Verify against a known selector from evm.methodIdentifiers
2075        let init_sel =
2076            compute_selector("initialize((address,address,uint24,int24,address),uint160)");
2077        assert_eq!(
2078            init_sel, "6276cbbe",
2079            "computed initialize selector should match evm.methodIdentifiers"
2080        );
2081    }
2082
2083    #[test]
2084    fn test_compute_event_topic_length() {
2085        let topic =
2086            compute_event_topic("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)");
2087        assert_eq!(
2088            topic.len(),
2089            64,
2090            "event topic should be 64 hex chars (32 bytes)"
2091        );
2092    }
2093
2094    #[test]
2095    fn test_doc_index_error_count_poolmanager() {
2096        let ast = load_solc_fixture();
2097        let index = build_doc_index(&ast);
2098
2099        // PoolManager userdoc has 14 errors. Check that they're all indexed.
2100        // Compute selectors for all 14 error signatures and verify they exist.
2101        let error_sigs = [
2102            "AlreadyUnlocked()",
2103            "CurrenciesOutOfOrderOrEqual(address,address)",
2104            "CurrencyNotSettled()",
2105            "InvalidCaller()",
2106            "ManagerLocked()",
2107            "MustClearExactPositiveDelta()",
2108            "NonzeroNativeValue()",
2109            "PoolNotInitialized()",
2110            "ProtocolFeeCurrencySynced()",
2111            "ProtocolFeeTooLarge(uint24)",
2112            "SwapAmountCannotBeZero()",
2113            "TickSpacingTooLarge(int24)",
2114            "TickSpacingTooSmall(int24)",
2115            "UnauthorizedDynamicLPFeeUpdate()",
2116        ];
2117        let mut found = 0;
2118        for sig in &error_sigs {
2119            let selector = compute_selector(sig);
2120            let key = DocKey::Func(FuncSelector::new(&selector));
2121            if index.contains_key(&key) {
2122                found += 1;
2123            }
2124        }
2125        assert_eq!(
2126            found,
2127            error_sigs.len(),
2128            "all 14 PoolManager errors should be in the DocIndex"
2129        );
2130    }
2131
2132    #[test]
2133    fn test_doc_index_extsload_overloads_have_different_selectors() {
2134        let ast = load_solc_fixture();
2135        let index = build_doc_index(&ast);
2136
2137        // Three extsload overloads should each have their own selector entry
2138        // extsload(bytes32) = 1e2eaeaf
2139        // extsload(bytes32,uint256) = 35fd631a
2140        // extsload(bytes32[]) = dbd035ff
2141        let sel1 = DocKey::Func(FuncSelector::new("1e2eaeaf"));
2142        let sel2 = DocKey::Func(FuncSelector::new("35fd631a"));
2143        let sel3 = DocKey::Func(FuncSelector::new("dbd035ff"));
2144
2145        assert!(index.contains_key(&sel1), "extsload(bytes32) should exist");
2146        assert!(
2147            index.contains_key(&sel2),
2148            "extsload(bytes32,uint256) should exist"
2149        );
2150        assert!(
2151            index.contains_key(&sel3),
2152            "extsload(bytes32[]) should exist"
2153        );
2154    }
2155}