Skip to main content

solidity_language_server/
hover.rs

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