Skip to main content

solidity_language_server/solc_ast/
mod.rs

1//! Typed Solidity AST deserialized from solc's JSON output.
2//!
3//! This module provides Rust structs that mirror the AST node types emitted
4//! by the Solidity compiler (`solc --standard-json`), plus a lightweight
5//! declaration extraction function ([`extract_decl_nodes`]) that avoids
6//! deserializing the full AST.
7//!
8//! # Design
9//!
10//! - All structs derive `serde::Deserialize` to parse directly from solc JSON.
11//! - Fields use `Option<T>` liberally for cross-version compatibility.
12//! - An `AstVisitor` trait (gated behind `#[cfg(test)]`) follows the official
13//!   C++ `ASTConstVisitor` pattern for test traversal.
14
15pub mod contracts;
16pub mod enums;
17pub mod events;
18pub mod expressions;
19pub mod functions;
20pub mod source_units;
21pub mod statements;
22pub mod types;
23pub mod variables;
24#[cfg(test)]
25pub mod visitor;
26pub mod yul;
27
28// Re-export everything for convenient access via `use crate::solc_ast::*`.
29pub use contracts::*;
30pub use enums::*;
31pub use events::*;
32pub use expressions::*;
33pub use functions::*;
34pub use source_units::*;
35pub use statements::*;
36pub use types::*;
37pub use variables::*;
38#[cfg(test)]
39pub use visitor::*;
40pub use yul::*;
41
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44
45// ── Typed declaration node ────────────────────────────────────────────────
46
47/// A reference to any declaration-level AST node.
48///
49/// This enum covers the node types that hover, goto, and references care
50/// about: functions, variables, contracts, events, errors, structs, enums,
51/// modifiers, and user-defined value types.
52///
53/// Built by [`extract_decl_nodes`] and stored in `CachedBuild` for O(1)
54/// typed node lookup by ID.
55#[derive(Clone, Debug)]
56pub enum DeclNode {
57    FunctionDefinition(FunctionDefinition),
58    VariableDeclaration(VariableDeclaration),
59    ContractDefinition(ContractDefinition),
60    EventDefinition(EventDefinition),
61    ErrorDefinition(ErrorDefinition),
62    StructDefinition(StructDefinition),
63    EnumDefinition(EnumDefinition),
64    ModifierDefinition(ModifierDefinition),
65    UserDefinedValueTypeDefinition(UserDefinedValueTypeDefinition),
66}
67
68impl DeclNode {
69    /// Node ID of the declaration.
70    pub fn id(&self) -> NodeID {
71        match self {
72            Self::FunctionDefinition(n) => n.id,
73            Self::VariableDeclaration(n) => n.id,
74            Self::ContractDefinition(n) => n.id,
75            Self::EventDefinition(n) => n.id,
76            Self::ErrorDefinition(n) => n.id,
77            Self::StructDefinition(n) => n.id,
78            Self::EnumDefinition(n) => n.id,
79            Self::ModifierDefinition(n) => n.id,
80            Self::UserDefinedValueTypeDefinition(n) => n.id,
81        }
82    }
83
84    /// Name of the declaration.
85    pub fn name(&self) -> &str {
86        match self {
87            Self::FunctionDefinition(n) => &n.name,
88            Self::VariableDeclaration(n) => &n.name,
89            Self::ContractDefinition(n) => &n.name,
90            Self::EventDefinition(n) => &n.name,
91            Self::ErrorDefinition(n) => &n.name,
92            Self::StructDefinition(n) => &n.name,
93            Self::EnumDefinition(n) => &n.name,
94            Self::ModifierDefinition(n) => &n.name,
95            Self::UserDefinedValueTypeDefinition(n) => &n.name,
96        }
97    }
98
99    /// Source location string (`"offset:length:fileId"`).
100    pub fn src(&self) -> &str {
101        match self {
102            Self::FunctionDefinition(n) => &n.src,
103            Self::VariableDeclaration(n) => &n.src,
104            Self::ContractDefinition(n) => &n.src,
105            Self::EventDefinition(n) => &n.src,
106            Self::ErrorDefinition(n) => &n.src,
107            Self::StructDefinition(n) => &n.src,
108            Self::EnumDefinition(n) => &n.src,
109            Self::ModifierDefinition(n) => &n.src,
110            Self::UserDefinedValueTypeDefinition(n) => &n.src,
111        }
112    }
113
114    /// Scope (parent) node ID, if available.
115    pub fn scope(&self) -> Option<NodeID> {
116        match self {
117            Self::FunctionDefinition(n) => n.scope,
118            Self::VariableDeclaration(n) => n.scope,
119            Self::ContractDefinition(n) => n.scope,
120            Self::EventDefinition(_) => None,
121            Self::ErrorDefinition(_) => None,
122            Self::StructDefinition(n) => n.scope,
123            Self::EnumDefinition(_) => None,
124            Self::ModifierDefinition(_) => None,
125            Self::UserDefinedValueTypeDefinition(_) => None,
126        }
127    }
128
129    /// Base functions/modifiers this declaration overrides.
130    ///
131    /// Returns IDs of interface/abstract declarations that this function or
132    /// modifier implements or overrides.
133    pub fn base_functions(&self) -> Option<&[NodeID]> {
134        match self {
135            Self::FunctionDefinition(n) => n.base_functions.as_deref(),
136            Self::ModifierDefinition(n) => n.base_modifiers.as_deref(),
137            Self::VariableDeclaration(n) => n.base_functions.as_deref(),
138            _ => None,
139        }
140    }
141
142    /// Documentation attached to the declaration.
143    pub fn documentation(&self) -> Option<&Documentation> {
144        match self {
145            Self::FunctionDefinition(n) => n.documentation.as_ref(),
146            Self::VariableDeclaration(n) => n.documentation.as_ref(),
147            Self::ContractDefinition(n) => n.documentation.as_ref(),
148            Self::EventDefinition(n) => n.documentation.as_ref(),
149            Self::ErrorDefinition(n) => n.documentation.as_ref(),
150            Self::StructDefinition(n) => n.documentation.as_ref(),
151            Self::EnumDefinition(n) => n.documentation.as_ref(),
152            Self::ModifierDefinition(n) => n.documentation.as_ref(),
153            Self::UserDefinedValueTypeDefinition(_) => None,
154        }
155    }
156
157    /// Function selector (4-byte hex for functions/errors, 32-byte for events).
158    pub fn selector(&self) -> Option<&str> {
159        match self {
160            Self::FunctionDefinition(n) => n.function_selector.as_deref(),
161            Self::VariableDeclaration(n) => n.function_selector.as_deref(),
162            Self::EventDefinition(n) => n.event_selector.as_deref(),
163            Self::ErrorDefinition(n) => n.error_selector.as_deref(),
164            _ => None,
165        }
166    }
167
168    /// Extract a typed [`Selector`] from this declaration.
169    ///
170    /// Returns `Selector::Func` for functions, public variables, and errors,
171    /// or `Selector::Event` for events. Equivalent to `extract_selector(&Value)`.
172    pub fn extract_typed_selector(&self) -> Option<crate::types::Selector> {
173        match self {
174            Self::FunctionDefinition(n) => n
175                .function_selector
176                .as_deref()
177                .map(|s| crate::types::Selector::Func(crate::types::FuncSelector::new(s))),
178            Self::VariableDeclaration(n) => n
179                .function_selector
180                .as_deref()
181                .map(|s| crate::types::Selector::Func(crate::types::FuncSelector::new(s))),
182            Self::ErrorDefinition(n) => n
183                .error_selector
184                .as_deref()
185                .map(|s| crate::types::Selector::Func(crate::types::FuncSelector::new(s))),
186            Self::EventDefinition(n) => n
187                .event_selector
188                .as_deref()
189                .map(|s| crate::types::Selector::Event(crate::types::EventSelector::new(s))),
190            _ => None,
191        }
192    }
193
194    /// Extract documentation text from this declaration.
195    ///
196    /// Returns the NatSpec text string, equivalent to the raw `extract_documentation()`.
197    pub fn extract_doc_text(&self) -> Option<String> {
198        self.documentation().map(|doc| match doc {
199            Documentation::String(s) => s.clone(),
200            Documentation::Structured(s) => s.text.clone(),
201        })
202    }
203
204    /// Build a Solidity-style signature string for this declaration.
205    ///
206    /// Typed equivalent of `build_function_signature(&Value)` in `hover.rs`.
207    /// Uses direct field access instead of `.get("field").and_then(|v| v.as_str())` chains.
208    pub fn build_signature(&self) -> Option<String> {
209        match self {
210            Self::FunctionDefinition(n) => {
211                let params = format_params_typed(&n.parameters);
212                let returns = format_params_typed(&n.return_parameters);
213
214                let mut sig = match n.kind {
215                    FunctionKind::Constructor => format!("constructor({params})"),
216                    FunctionKind::Receive => "receive() external payable".to_string(),
217                    FunctionKind::Fallback => format!("fallback({params})"),
218                    _ => format!("function {}({params})", n.name),
219                };
220
221                // Add visibility (skip for constructor and receive)
222                if !matches!(n.kind, FunctionKind::Constructor | FunctionKind::Receive)
223                    && let Some(vis) = &n.visibility
224                {
225                    let vis_str = vis.to_string();
226                    if !vis_str.is_empty() {
227                        sig.push_str(&format!(" {vis_str}"));
228                    }
229                }
230
231                // Add state mutability (skip "nonpayable")
232                if !matches!(n.state_mutability, StateMutability::Nonpayable) {
233                    sig.push_str(&format!(" {}", n.state_mutability));
234                }
235
236                if !returns.is_empty() {
237                    sig.push_str(&format!(" returns ({returns})"));
238                }
239                Some(sig)
240            }
241            Self::ModifierDefinition(n) => {
242                let params = format_params_typed(&n.parameters);
243                Some(format!("modifier {}({params})", n.name))
244            }
245            Self::EventDefinition(n) => {
246                let params = format_params_typed(&n.parameters);
247                Some(format!("event {}({params})", n.name))
248            }
249            Self::ErrorDefinition(n) => {
250                let params = format_params_typed(&n.parameters);
251                Some(format!("error {}({params})", n.name))
252            }
253            Self::VariableDeclaration(n) => {
254                let type_str = n
255                    .type_descriptions
256                    .type_string
257                    .as_deref()
258                    .unwrap_or("unknown");
259                let vis = n
260                    .visibility
261                    .as_ref()
262                    .map(|v| v.to_string())
263                    .unwrap_or_default();
264
265                let mut sig = type_str.to_string();
266                if !vis.is_empty() {
267                    sig.push_str(&format!(" {vis}"));
268                }
269                match &n.mutability {
270                    Some(Mutability::Constant) => sig.push_str(" constant"),
271                    Some(Mutability::Immutable) => sig.push_str(" immutable"),
272                    _ => {}
273                }
274                sig.push_str(&format!(" {}", n.name));
275                Some(sig)
276            }
277            Self::ContractDefinition(n) => {
278                let mut sig = format!("{} {}", n.contract_kind, n.name);
279
280                if !n.base_contracts.is_empty() {
281                    let base_names: Vec<&str> = n
282                        .base_contracts
283                        .iter()
284                        .map(|b| b.base_name.name.as_str())
285                        .collect();
286                    if !base_names.is_empty() {
287                        sig.push_str(&format!(" is {}", base_names.join(", ")));
288                    }
289                }
290                Some(sig)
291            }
292            Self::StructDefinition(n) => {
293                let mut sig = format!("struct {} {{\n", n.name);
294                for member in &n.members {
295                    let mtype = member
296                        .type_descriptions
297                        .type_string
298                        .as_deref()
299                        .unwrap_or("?");
300                    sig.push_str(&format!("    {mtype} {};\n", member.name));
301                }
302                sig.push('}');
303                Some(sig)
304            }
305            Self::EnumDefinition(n) => {
306                let mut sig = format!("enum {} {{\n", n.name);
307                for member in &n.members {
308                    sig.push_str(&format!("    {},\n", member.name));
309                }
310                sig.push('}');
311                Some(sig)
312            }
313            Self::UserDefinedValueTypeDefinition(n) => {
314                let underlying = type_name_to_str(&n.underlying_type);
315                Some(format!("type {} is {underlying}", n.name))
316            }
317        }
318    }
319
320    /// Build individual parameter strings for signature help.
321    ///
322    /// Returns a vec of strings like `["uint256 amount", "uint16 tax"]`.
323    /// Typed equivalent of `build_parameter_strings(&Value)` in `hover.rs`.
324    pub fn param_strings(&self) -> Vec<String> {
325        match self {
326            Self::FunctionDefinition(n) => build_param_strings_typed(&n.parameters),
327            Self::ModifierDefinition(n) => build_param_strings_typed(&n.parameters),
328            Self::EventDefinition(n) => build_param_strings_typed(&n.parameters),
329            Self::ErrorDefinition(n) => build_param_strings_typed(&n.parameters),
330            _ => Vec::new(),
331        }
332    }
333
334    /// Returns the `ParameterList` for this declaration's parameters, if any.
335    pub fn parameters(&self) -> Option<&ParameterList> {
336        match self {
337            Self::FunctionDefinition(n) => Some(&n.parameters),
338            Self::ModifierDefinition(n) => Some(&n.parameters),
339            Self::EventDefinition(n) => Some(&n.parameters),
340            Self::ErrorDefinition(n) => Some(&n.parameters),
341            _ => None,
342        }
343    }
344
345    /// Returns the `ParameterList` for this declaration's return parameters, if any.
346    pub fn return_parameters(&self) -> Option<&ParameterList> {
347        match self {
348            Self::FunctionDefinition(n) => Some(&n.return_parameters),
349            _ => None,
350        }
351    }
352
353    /// Returns the node type string, matching the JSON `nodeType` field.
354    pub fn node_type(&self) -> &'static str {
355        match self {
356            Self::FunctionDefinition(_) => "FunctionDefinition",
357            Self::VariableDeclaration(_) => "VariableDeclaration",
358            Self::ContractDefinition(_) => "ContractDefinition",
359            Self::EventDefinition(_) => "EventDefinition",
360            Self::ErrorDefinition(_) => "ErrorDefinition",
361            Self::StructDefinition(_) => "StructDefinition",
362            Self::EnumDefinition(_) => "EnumDefinition",
363            Self::ModifierDefinition(_) => "ModifierDefinition",
364            Self::UserDefinedValueTypeDefinition(_) => "UserDefinedValueTypeDefinition",
365        }
366    }
367
368    /// Returns the type description string for this declaration, if available.
369    pub fn type_string(&self) -> Option<&str> {
370        match self {
371            Self::VariableDeclaration(n) => n.type_descriptions.type_string.as_deref(),
372            _ => None,
373        }
374    }
375
376    /// Returns parameter/member names for inlay hint resolution.
377    ///
378    /// For functions, events, errors, modifiers: returns `parameters.parameters[].name`.
379    /// For structs: returns `members[].name`.
380    /// Typed equivalent of `get_parameter_names(&Value)` in `inlay_hints.rs`.
381    pub fn param_names(&self) -> Option<Vec<String>> {
382        match self {
383            Self::FunctionDefinition(n) => Some(
384                n.parameters
385                    .parameters
386                    .iter()
387                    .map(|p| p.name.clone())
388                    .collect(),
389            ),
390            Self::ModifierDefinition(n) => Some(
391                n.parameters
392                    .parameters
393                    .iter()
394                    .map(|p| p.name.clone())
395                    .collect(),
396            ),
397            Self::EventDefinition(n) => Some(
398                n.parameters
399                    .parameters
400                    .iter()
401                    .map(|p| p.name.clone())
402                    .collect(),
403            ),
404            Self::ErrorDefinition(n) => Some(
405                n.parameters
406                    .parameters
407                    .iter()
408                    .map(|p| p.name.clone())
409                    .collect(),
410            ),
411            Self::StructDefinition(n) => Some(n.members.iter().map(|m| m.name.clone()).collect()),
412            _ => None,
413        }
414    }
415
416    /// For constructors, returns true if this is a constructor function.
417    pub fn is_constructor(&self) -> bool {
418        matches!(self, Self::FunctionDefinition(n) if matches!(n.kind, crate::solc_ast::enums::FunctionKind::Constructor))
419    }
420}
421
422// ── Typed formatting helpers ──────────────────────────────────────────────────
423
424/// Format a typed `ParameterList` into a comma-separated parameter string.
425///
426/// Typed equivalent of `format_parameters(Option<&Value>)` in `hover.rs`.
427pub fn format_params_typed(params: &ParameterList) -> String {
428    let parts: Vec<String> = params
429        .parameters
430        .iter()
431        .map(|p| {
432            let type_str = p.type_descriptions.type_string.as_deref().unwrap_or("?");
433            let name = &p.name;
434            let storage = p
435                .storage_location
436                .as_ref()
437                .map(|s| s.to_string())
438                .unwrap_or_else(|| "default".to_string());
439
440            if name.is_empty() {
441                type_str.to_string()
442            } else if storage != "default" {
443                format!("{type_str} {storage} {name}")
444            } else {
445                format!("{type_str} {name}")
446            }
447        })
448        .collect();
449
450    parts.join(", ")
451}
452
453/// Build individual parameter strings from a typed `ParameterList`.
454///
455/// Returns a vec of strings like `["uint256 amount", "uint16 tax"]`.
456fn build_param_strings_typed(params: &ParameterList) -> Vec<String> {
457    params
458        .parameters
459        .iter()
460        .map(|p| {
461            let type_str = p.type_descriptions.type_string.as_deref().unwrap_or("?");
462            let name = &p.name;
463            let storage = p
464                .storage_location
465                .as_ref()
466                .map(|s| s.to_string())
467                .unwrap_or_else(|| "default".to_string());
468
469            if name.is_empty() {
470                type_str.to_string()
471            } else if storage != "default" {
472                format!("{type_str} {storage} {name}")
473            } else {
474                format!("{type_str} {name}")
475            }
476        })
477        .collect()
478}
479
480/// Extract a human-readable type string from a `TypeName` node.
481pub fn type_name_to_str(tn: &TypeName) -> &str {
482    match tn {
483        TypeName::ElementaryTypeName(e) => e
484            .type_descriptions
485            .type_string
486            .as_deref()
487            .unwrap_or(&e.name),
488        TypeName::UserDefinedTypeName(u) => u
489            .type_descriptions
490            .type_string
491            .as_deref()
492            .unwrap_or("unknown"),
493        TypeName::FunctionTypeName(f) => f
494            .type_descriptions
495            .type_string
496            .as_deref()
497            .unwrap_or("function"),
498        TypeName::Mapping(m) => m
499            .type_descriptions
500            .type_string
501            .as_deref()
502            .unwrap_or("mapping"),
503        TypeName::ArrayTypeName(a) => a
504            .type_descriptions
505            .type_string
506            .as_deref()
507            .unwrap_or("array"),
508    }
509}
510
511// ── Declaration-only extraction from raw Value ────────────────────────────
512
513/// The 9 declaration `nodeType` strings we care about.
514const DECL_NODE_TYPES: &[&str] = &[
515    "FunctionDefinition",
516    "VariableDeclaration",
517    "ContractDefinition",
518    "EventDefinition",
519    "ErrorDefinition",
520    "StructDefinition",
521    "EnumDefinition",
522    "ModifierDefinition",
523    "UserDefinedValueTypeDefinition",
524];
525
526/// Fields to strip from declaration nodes before deserializing.
527/// These contain large AST subtrees (function bodies, expressions, etc.)
528/// that are never read through `DeclNode`.
529const STRIP_FIELDS: &[&str] = &[
530    "body",
531    "modifiers",
532    "value",
533    "overrides",
534    "nameLocation",
535    "implemented",
536    "isVirtual",
537    "abstract",
538    "contractDependencies",
539    "usedErrors",
540    "usedEvents",
541    "fullyImplemented",
542    "linearizedBaseContracts",
543    "canonicalName",
544    "constant",
545    "indexed",
546];
547
548/// Fields to strip from children inside `ContractDefinition.nodes`.
549/// Same as `STRIP_FIELDS` — each contract child also has bodies, values, etc.
550const STRIP_CHILD_FIELDS: &[&str] = &[
551    "body",
552    "modifiers",
553    "value",
554    "overrides",
555    "nameLocation",
556    "implemented",
557    "isVirtual",
558    "constant",
559    "indexed",
560    "canonicalName",
561];
562
563/// Result of extracting declaration nodes from the raw sources Value.
564pub struct ExtractedDecls {
565    /// Declaration index: node ID → typed `DeclNode`.
566    pub decl_index: HashMap<NodeID, DeclNode>,
567    /// Reverse index: node ID → source file path.
568    pub node_id_to_source_path: HashMap<NodeID, String>,
569}
570
571/// Extract declaration nodes directly from the raw `sources` section of solc output.
572///
573/// Instead of deserializing the entire typed AST (SourceUnit, all expressions,
574/// statements, Yul blocks), this walks the raw JSON Value tree and only
575/// deserializes nodes whose `nodeType` matches one of the 9 declaration types.
576///
577/// Heavy fields (`body`, `modifiers`, `value`, etc.) are stripped from the
578/// Value **before** deserialization, so function bodies are never parsed.
579///
580/// For `ContractDefinition`, the `nodes` array is preserved (needed for
581/// `resolve_inheritdoc_typed`) but each child node within it also has its
582/// heavy fields stripped.
583///
584/// This eliminates:
585/// - The full `SourceUnit` deserialization per source file
586/// - The `ast_node.clone()` per source file (~80 MB for large projects)
587/// - All transient expression/statement/yul parsing (~40 MB)
588pub fn extract_decl_nodes(sources: &serde_json::Value) -> Option<ExtractedDecls> {
589    let sources_obj = sources.as_object()?;
590    // Pre-size based on source count. Typical project averages ~30 decl nodes
591    // per source file and ~30 id-to-path entries per source file.
592    let source_count = sources_obj.len();
593    let mut decl_index = HashMap::with_capacity(source_count * 32);
594    let mut id_to_path = HashMap::with_capacity(source_count * 32);
595
596    for (path, source_data) in sources_obj {
597        let ast_node = source_data.get("ast")?;
598
599        // Record the source unit id
600        if let Some(su_id) = ast_node.get("id").and_then(|v| v.as_i64()) {
601            id_to_path.insert(su_id, path.clone());
602        }
603
604        // Walk the top-level `nodes` array of the SourceUnit
605        if let Some(nodes) = ast_node.get("nodes").and_then(|v| v.as_array()) {
606            for node in nodes {
607                walk_and_extract(node, path, &mut decl_index, &mut id_to_path);
608            }
609        }
610    }
611
612    Some(ExtractedDecls {
613        decl_index,
614        node_id_to_source_path: id_to_path,
615    })
616}
617
618/// Recursively walk a JSON node, extracting declaration nodes.
619fn walk_and_extract(
620    node: &serde_json::Value,
621    source_path: &str,
622    decl_index: &mut HashMap<NodeID, DeclNode>,
623    id_to_path: &mut HashMap<NodeID, String>,
624) {
625    let obj = match node.as_object() {
626        Some(o) => o,
627        None => return,
628    };
629
630    let node_type = match obj.get("nodeType").and_then(|v| v.as_str()) {
631        Some(nt) => nt,
632        None => return,
633    };
634
635    let node_id = obj.get("id").and_then(|v| v.as_i64());
636
637    // Record id → path for all nodes that have an id
638    if let Some(id) = node_id {
639        id_to_path.insert(id, source_path.to_string());
640    }
641
642    // Check if this is a declaration node type
643    if DECL_NODE_TYPES.contains(&node_type)
644        && let Some(id) = node_id
645    {
646        // Build a filtered Value from the borrowed node, copying only
647        // the fields needed for deserialization (skips body, modifiers, value, etc.).
648        // This avoids cloning the entire node (previously ~117 MB of transient churn).
649        let node_value = if node_type == "ContractDefinition" {
650            build_filtered_contract(obj)
651        } else {
652            build_filtered_decl(obj)
653        };
654
655        // Deserialize the filtered node into the typed struct
656        if let Some(decl) = deserialize_decl_node(node_type, node_value) {
657            decl_index.insert(id, decl);
658        }
659    }
660
661    // Recurse into children — ContractDefinition has `nodes`, SourceUnit has `nodes`.
662    // We also need to recurse into `parameters`, `returnParameters`, and `members`
663    // to find nested VariableDeclaration nodes (function params, return params,
664    // error/event params, struct members).
665    if let Some(children) = obj.get("nodes").and_then(|v| v.as_array()) {
666        for child in children {
667            walk_and_extract(child, source_path, decl_index, id_to_path);
668        }
669    }
670
671    // Walk into ParameterList.parameters arrays to capture individual
672    // VariableDeclaration nodes for params/returns
673    for param_key in &["parameters", "returnParameters"] {
674        if let Some(param_list) = obj.get(*param_key).and_then(|v| v.as_object()) {
675            // ParameterList has id and a `parameters` array
676            if let Some(pl_id) = param_list.get("id").and_then(|v| v.as_i64()) {
677                id_to_path.insert(pl_id, source_path.to_string());
678            }
679            if let Some(params) = param_list.get("parameters").and_then(|v| v.as_array()) {
680                for param in params {
681                    walk_and_extract(param, source_path, decl_index, id_to_path);
682                }
683            }
684        }
685    }
686
687    // Walk into struct `members` to capture member VariableDeclarations
688    if let Some(members) = obj.get("members").and_then(|v| v.as_array()) {
689        for member in members {
690            walk_and_extract(member, source_path, decl_index, id_to_path);
691        }
692    }
693}
694
695/// Build a filtered Value from a borrowed declaration node, cloning only the
696/// fields needed for deserialization. Heavy fields (body, modifiers, value, etc.)
697/// are never copied. This replaces the old clone-then-strip pattern that caused
698/// ~117 MB of transient allocation churn.
699fn build_filtered_decl(obj: &serde_json::Map<String, serde_json::Value>) -> serde_json::Value {
700    let mut filtered = serde_json::Map::with_capacity(obj.len());
701    for (key, value) in obj {
702        if !STRIP_FIELDS.contains(&key.as_str()) {
703            filtered.insert(key.clone(), value.clone());
704        }
705    }
706    serde_json::Value::Object(filtered)
707}
708
709/// Build a filtered Value from a borrowed ContractDefinition node.
710///
711/// Preserves the `nodes` array (needed for `resolve_inheritdoc_typed`) but
712/// filters heavy fields from each child within it. Contract-level heavy fields
713/// are also skipped.
714fn build_filtered_contract(obj: &serde_json::Map<String, serde_json::Value>) -> serde_json::Value {
715    let mut filtered = serde_json::Map::with_capacity(obj.len());
716    for (key, value) in obj {
717        if STRIP_FIELDS.contains(&key.as_str()) {
718            continue;
719        }
720        if key == "nodes" {
721            // Filter heavy fields from each child node in the `nodes` array
722            if let Some(arr) = value.as_array() {
723                let filtered_nodes: Vec<serde_json::Value> = arr
724                    .iter()
725                    .map(|child| {
726                        if let Some(child_obj) = child.as_object() {
727                            let mut filtered_child =
728                                serde_json::Map::with_capacity(child_obj.len());
729                            for (ck, cv) in child_obj {
730                                if !STRIP_CHILD_FIELDS.contains(&ck.as_str()) {
731                                    filtered_child.insert(ck.clone(), cv.clone());
732                                }
733                            }
734                            serde_json::Value::Object(filtered_child)
735                        } else {
736                            child.clone()
737                        }
738                    })
739                    .collect();
740                filtered.insert(key.clone(), serde_json::Value::Array(filtered_nodes));
741            } else {
742                filtered.insert(key.clone(), value.clone());
743            }
744        } else {
745            filtered.insert(key.clone(), value.clone());
746        }
747    }
748    serde_json::Value::Object(filtered)
749}
750
751/// Deserialize a stripped JSON Value into a `DeclNode` based on `nodeType`.
752fn deserialize_decl_node(node_type: &str, value: serde_json::Value) -> Option<DeclNode> {
753    match node_type {
754        "FunctionDefinition" => serde_json::from_value::<FunctionDefinition>(value)
755            .ok()
756            .map(DeclNode::FunctionDefinition),
757        "VariableDeclaration" => serde_json::from_value::<VariableDeclaration>(value)
758            .ok()
759            .map(DeclNode::VariableDeclaration),
760        "ContractDefinition" => serde_json::from_value::<ContractDefinition>(value)
761            .ok()
762            .map(DeclNode::ContractDefinition),
763        "EventDefinition" => serde_json::from_value::<EventDefinition>(value)
764            .ok()
765            .map(DeclNode::EventDefinition),
766        "ErrorDefinition" => serde_json::from_value::<ErrorDefinition>(value)
767            .ok()
768            .map(DeclNode::ErrorDefinition),
769        "StructDefinition" => serde_json::from_value::<StructDefinition>(value)
770            .ok()
771            .map(DeclNode::StructDefinition),
772        "EnumDefinition" => serde_json::from_value::<EnumDefinition>(value)
773            .ok()
774            .map(DeclNode::EnumDefinition),
775        "ModifierDefinition" => serde_json::from_value::<ModifierDefinition>(value)
776            .ok()
777            .map(DeclNode::ModifierDefinition),
778        "UserDefinedValueTypeDefinition" => {
779            serde_json::from_value::<UserDefinedValueTypeDefinition>(value)
780                .ok()
781                .map(DeclNode::UserDefinedValueTypeDefinition)
782        }
783        _ => None,
784    }
785}
786
787// ── Core types ─────────────────────────────────────────────────────────────
788
789/// AST node ID, matching solc's signed 64-bit integer.
790pub type NodeID = i64;
791
792/// Type description attached to expressions and some declarations.
793///
794/// Contains the compiler's resolved type information as strings.
795#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
796#[serde(rename_all = "camelCase")]
797pub struct TypeDescriptions {
798    /// Human-readable type string, e.g. `"uint256"`, `"mapping(address => uint256)"`.
799    pub type_string: Option<String>,
800    /// Machine-readable type identifier, e.g. `"t_uint256"`, `"t_mapping$_t_address_$_t_uint256_$"`.
801    pub type_identifier: Option<String>,
802}
803
804/// Structured documentation (NatSpec) attached to declarations.
805///
806/// In the AST JSON this appears either as a string or as a node with `id`, `src`, `text`.
807#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
808#[serde(untagged)]
809pub enum Documentation {
810    /// Simple string (older solc versions or some contexts).
811    String(String),
812    /// Structured documentation node.
813    Structured(StructuredDocumentation),
814}
815
816/// A `StructuredDocumentation` AST node.
817#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
818#[serde(rename_all = "camelCase")]
819pub struct StructuredDocumentation {
820    pub id: NodeID,
821    pub src: String,
822    pub text: String,
823}
824
825/// External reference inside an `InlineAssembly` node.
826///
827/// Maps a Yul identifier to the Solidity declaration it refers to.
828#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
829#[serde(rename_all = "camelCase")]
830pub struct ExternalReference {
831    pub declaration: NodeID,
832    #[serde(default)]
833    pub is_offset: bool,
834    #[serde(default)]
835    pub is_slot: bool,
836    pub src: String,
837    #[serde(default)]
838    pub suffix: Option<String>,
839    pub value_size: Option<i64>,
840}
841
842/// An entry in a `UsingForDirective`'s `functionList`.
843///
844/// Either a plain `function` reference or a `definition` + `operator` pair
845/// for user-defined operators.
846#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
847#[serde(rename_all = "camelCase")]
848pub struct UsingForFunction {
849    /// Plain library function reference (when no operator).
850    pub function: Option<IdentifierPath>,
851    /// Function definition for a user-defined operator.
852    pub definition: Option<IdentifierPath>,
853    /// The operator being overloaded (e.g. `"+"`).
854    pub operator: Option<String>,
855}
856
857/// An `IdentifierPath` AST node — a dotted name like `IERC20` or `MyLib.add`.
858#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
859#[serde(rename_all = "camelCase")]
860pub struct IdentifierPath {
861    pub id: NodeID,
862    pub name: String,
863    #[serde(default)]
864    pub name_locations: Vec<String>,
865    pub referenced_declaration: Option<NodeID>,
866    pub src: String,
867}
868
869// ── Tests ──────────────────────────────────────────────────────────────────
870
871#[cfg(test)]
872mod tests {
873    /// Top-level output from `solc --standard-json` after normalization.
874    ///
875    /// Only used in tests to deserialize full fixture files.
876    #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
877    #[serde(rename_all = "camelCase")]
878    pub struct SolcOutput {
879        #[serde(default)]
880        pub sources: std::collections::HashMap<String, SourceEntry>,
881        #[serde(default)]
882        pub source_id_to_path: std::collections::HashMap<String, String>,
883    }
884
885    /// A single source file entry in the solc output.
886    #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
887    pub struct SourceEntry {
888        pub id: i64,
889        pub ast: super::SourceUnit,
890    }
891    use super::*;
892    use std::path::Path;
893
894    /// Load and deserialize the entire poolmanager.json fixture.
895    fn load_fixture() -> SolcOutput {
896        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
897        let json = std::fs::read_to_string(&path)
898            .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
899        serde_json::from_str(&json)
900            .unwrap_or_else(|e| panic!("failed to deserialize poolmanager.json: {e}"))
901    }
902
903    #[test]
904    fn deserialize_poolmanager() {
905        let output = load_fixture();
906
907        // The fixture has 45 source files.
908        assert_eq!(
909            output.sources.len(),
910            45,
911            "expected 45 source files in poolmanager.json"
912        );
913
914        // Every source entry should have a valid AST with a non-empty src.
915        for (path, entry) in &output.sources {
916            assert!(entry.ast.id > 0, "bad AST id for {path}");
917            assert!(!entry.ast.src.is_empty(), "empty src for {path}");
918        }
919    }
920
921    #[test]
922    fn deserialize_all_source_units() {
923        let output = load_fixture();
924
925        // Verify that we can access nodes in every source unit
926        // and that the top-level contract count matches expectations.
927        let mut contract_count = 0;
928        for (_path, entry) in output.sources {
929            for node in &entry.ast.nodes {
930                if matches!(node, SourceUnitNode::ContractDefinition(_)) {
931                    contract_count += 1;
932                }
933            }
934        }
935        assert_eq!(
936            contract_count, 43,
937            "expected 43 top-level ContractDefinitions"
938        );
939    }
940
941    // ── Visitor tests ──────────────────────────────────────────────────
942
943    /// A visitor that counts specific node types.
944    struct NodeCounter {
945        functions: usize,
946        contracts: usize,
947        events: usize,
948        errors: usize,
949        variables: usize,
950        total_nodes: usize,
951    }
952
953    impl NodeCounter {
954        fn new() -> Self {
955            Self {
956                functions: 0,
957                contracts: 0,
958                events: 0,
959                errors: 0,
960                variables: 0,
961                total_nodes: 0,
962            }
963        }
964    }
965
966    impl AstVisitor for NodeCounter {
967        fn visit_node(&mut self, _id: NodeID, _src: &str) -> bool {
968            self.total_nodes += 1;
969            true
970        }
971
972        fn visit_function_definition(&mut self, node: &FunctionDefinition) -> bool {
973            self.functions += 1;
974            self.visit_node(node.id, &node.src)
975        }
976
977        fn visit_contract_definition(&mut self, node: &ContractDefinition) -> bool {
978            self.contracts += 1;
979            self.visit_node(node.id, &node.src)
980        }
981
982        fn visit_event_definition(&mut self, node: &EventDefinition) -> bool {
983            self.events += 1;
984            self.visit_node(node.id, &node.src)
985        }
986
987        fn visit_error_definition(&mut self, node: &ErrorDefinition) -> bool {
988            self.errors += 1;
989            self.visit_node(node.id, &node.src)
990        }
991
992        fn visit_variable_declaration(&mut self, node: &VariableDeclaration) -> bool {
993            self.variables += 1;
994            self.visit_node(node.id, &node.src)
995        }
996    }
997
998    #[test]
999    fn visitor_counts_nodes() {
1000        let output = load_fixture();
1001        let mut counter = NodeCounter::new();
1002
1003        for (_path, entry) in output.sources {
1004            entry.ast.accept(&mut counter);
1005        }
1006
1007        assert_eq!(counter.contracts, 43, "expected 43 ContractDefinitions");
1008        assert_eq!(counter.functions, 215, "expected 215 FunctionDefinitions");
1009        assert_eq!(counter.events, 12, "expected 12 EventDefinitions");
1010        assert_eq!(counter.errors, 42, "expected 42 ErrorDefinitions");
1011        assert!(
1012            counter.variables > 0,
1013            "expected at least some VariableDeclarations"
1014        );
1015        assert!(
1016            counter.total_nodes > 0,
1017            "expected total_nodes to be non-zero"
1018        );
1019
1020        // Sanity: total should be much larger than the sum of specific types.
1021        let specific_sum = counter.contracts + counter.functions + counter.events + counter.errors;
1022        assert!(
1023            counter.total_nodes > specific_sum,
1024            "total_nodes ({}) should be > sum of specific types ({specific_sum})",
1025            counter.total_nodes
1026        );
1027    }
1028
1029    #[test]
1030    fn cached_build_populates_decl_index() {
1031        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
1032        let json = std::fs::read_to_string(&path).unwrap();
1033        let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
1034
1035        let build = crate::goto::CachedBuild::new(raw, 0, None);
1036
1037        assert!(
1038            !build.decl_index.is_empty(),
1039            "decl_index should be populated"
1040        );
1041
1042        // Count by variant
1043        let mut funcs = 0;
1044        let mut vars = 0;
1045        let mut contracts = 0;
1046        let mut events = 0;
1047        let mut errors = 0;
1048        for decl in build.decl_index.values() {
1049            match decl {
1050                DeclNode::FunctionDefinition(_) => funcs += 1,
1051                DeclNode::VariableDeclaration(_) => vars += 1,
1052                DeclNode::ContractDefinition(_) => contracts += 1,
1053                DeclNode::EventDefinition(_) => events += 1,
1054                DeclNode::ErrorDefinition(_) => errors += 1,
1055                _ => {}
1056            }
1057        }
1058
1059        assert_eq!(
1060            contracts, 43,
1061            "expected 43 ContractDefinitions in decl_index"
1062        );
1063        assert_eq!(funcs, 215, "expected 215 FunctionDefinitions in decl_index");
1064        assert_eq!(events, 12, "expected 12 EventDefinitions in decl_index");
1065        assert_eq!(errors, 42, "expected 42 ErrorDefinitions in decl_index");
1066        assert!(vars > 0, "expected VariableDeclarations in decl_index");
1067    }
1068
1069    /// Verify that `node_id_to_source_path` covers all declaration nodes.
1070    #[test]
1071    fn node_id_to_source_path_covers_decl_index() {
1072        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
1073        let json = std::fs::read_to_string(&path).unwrap();
1074        let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
1075
1076        let build = crate::goto::CachedBuild::new(raw, 0, None);
1077
1078        assert!(
1079            !build.node_id_to_source_path.is_empty(),
1080            "node_id_to_source_path should be populated"
1081        );
1082
1083        // Every contract, function, event, error at contract level should have a path
1084        let mut missing = Vec::new();
1085        for (id, decl) in &build.decl_index {
1086            // Skip parameter VariableDeclarations — they are nested deeper than
1087            // the 2-level walk in our path builder
1088            if matches!(decl, DeclNode::VariableDeclaration(v) if v.state_variable != Some(true)) {
1089                continue;
1090            }
1091            if !build.node_id_to_source_path.contains_key(id) {
1092                missing.push(format!(
1093                    "id={id} name={:?} type={}",
1094                    decl.name(),
1095                    decl.node_type()
1096                ));
1097            }
1098        }
1099
1100        assert!(
1101            missing.is_empty(),
1102            "Declarations without source path ({}):\n{}",
1103            missing.len(),
1104            missing.join("\n"),
1105        );
1106    }
1107
1108    /// Verify that `baseFunctions` survives stripping and is populated on DeclNode.
1109    #[test]
1110    fn base_functions_populated_on_decl_nodes() {
1111        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
1112        let json = std::fs::read_to_string(&path).unwrap();
1113        let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
1114
1115        let build = crate::goto::CachedBuild::new(raw, 0, None);
1116
1117        // PoolManager.swap (616) should have baseFunctions: [2036] (IPoolManager.swap)
1118        let swap_decl = build
1119            .decl_index
1120            .get(&crate::types::NodeId(616))
1121            .expect("PoolManager.swap (id 616) should exist in decl_index");
1122        assert_eq!(swap_decl.name(), "swap");
1123        let base = swap_decl
1124            .base_functions()
1125            .expect("swap should have baseFunctions");
1126        assert_eq!(base, &[2036], "swap baseFunctions should be [2036]");
1127
1128        // IPoolManager.swap (2036) should have no baseFunctions (it's the interface root)
1129        let iswap_decl = build
1130            .decl_index
1131            .get(&crate::types::NodeId(2036))
1132            .expect("IPoolManager.swap (id 2036) should exist in decl_index");
1133        assert_eq!(iswap_decl.name(), "swap");
1134        assert!(
1135            iswap_decl.base_functions().is_none()
1136                || iswap_decl.base_functions().unwrap().is_empty(),
1137            "IPoolManager.swap should have no baseFunctions"
1138        );
1139
1140        // Count total functions with baseFunctions set
1141        let with_base: Vec<_> = build
1142            .decl_index
1143            .values()
1144            .filter(|d| d.base_functions().is_some_and(|b| !b.is_empty()))
1145            .collect();
1146        assert_eq!(
1147            with_base.len(),
1148            32,
1149            "expected 32 declarations with baseFunctions/baseModifiers in poolmanager.json"
1150        );
1151    }
1152
1153    /// Verify that `base_function_implementation` bidirectional index is populated.
1154    #[test]
1155    fn base_function_implementation_index_populated() {
1156        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
1157        let json = std::fs::read_to_string(&path).unwrap();
1158        let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
1159
1160        let build = crate::goto::CachedBuild::new(raw, 0, None);
1161
1162        // Forward: PoolManager.swap (616) → [IPoolManager.swap (2036)]
1163        let forward = build
1164            .base_function_implementation
1165            .get(&crate::types::NodeId(616))
1166            .expect("616 should be in base_function_implementation");
1167        assert!(
1168            forward.contains(&crate::types::NodeId(2036)),
1169            "616 → 2036 mapping should exist"
1170        );
1171
1172        // Reverse: IPoolManager.swap (2036) → [PoolManager.swap (616)]
1173        let reverse = build
1174            .base_function_implementation
1175            .get(&crate::types::NodeId(2036))
1176            .expect("2036 should be in base_function_implementation");
1177        assert!(
1178            reverse.contains(&crate::types::NodeId(616)),
1179            "2036 → 616 mapping should exist"
1180        );
1181
1182        // The index should not be empty
1183        assert!(
1184            !build.base_function_implementation.is_empty(),
1185            "base_function_implementation should be populated"
1186        );
1187
1188        // PoolManager.initialize (330) → [IPoolManager.initialize (2003)]
1189        let init_forward = build
1190            .base_function_implementation
1191            .get(&crate::types::NodeId(330))
1192            .expect("330 should be in base_function_implementation");
1193        assert!(
1194            init_forward.contains(&crate::types::NodeId(2003)),
1195            "330 → 2003 mapping should exist"
1196        );
1197    }
1198
1199    /// Verify that `base_function_implementation` is also populated on warm-loaded builds
1200    /// (via `from_reference_index`), since `NodeInfo` now persists `base_functions`.
1201    #[test]
1202    fn base_function_implementation_survives_warm_load() {
1203        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("poolmanager.json");
1204        let json = std::fs::read_to_string(&path).unwrap();
1205        let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
1206
1207        // Step 1: Build fresh — this populates base_functions on NodeInfo.
1208        let fresh_build = crate::goto::CachedBuild::new(raw, 0, None);
1209
1210        // Step 2: Simulate warm load by extracting nodes and passing to from_reference_index.
1211        let warm_build = crate::goto::CachedBuild::from_reference_index(
1212            fresh_build.nodes.clone(),
1213            fresh_build.path_to_abs.clone(),
1214            fresh_build.external_refs.clone(),
1215            fresh_build.id_to_path_map.clone(),
1216            0,
1217            None,
1218        );
1219
1220        // The warm-loaded build should have the same base_function_implementation as fresh.
1221        assert!(
1222            !warm_build.base_function_implementation.is_empty(),
1223            "base_function_implementation should be populated on warm-loaded build"
1224        );
1225
1226        // Forward: PoolManager.swap (616) → [IPoolManager.swap (2036)]
1227        let forward = warm_build
1228            .base_function_implementation
1229            .get(&crate::types::NodeId(616))
1230            .expect("616 should be in warm-loaded base_function_implementation");
1231        assert!(
1232            forward.contains(&crate::types::NodeId(2036)),
1233            "616 → 2036 mapping should exist in warm-loaded build"
1234        );
1235
1236        // Reverse: IPoolManager.swap (2036) → [PoolManager.swap (616)]
1237        let reverse = warm_build
1238            .base_function_implementation
1239            .get(&crate::types::NodeId(2036))
1240            .expect("2036 should be in warm-loaded base_function_implementation");
1241        assert!(
1242            reverse.contains(&crate::types::NodeId(616)),
1243            "2036 → 616 mapping should exist in warm-loaded build"
1244        );
1245
1246        // PoolManager.unlock (183) → [IPoolManager.unlock]
1247        let unlock_forward = warm_build
1248            .base_function_implementation
1249            .get(&crate::types::NodeId(183));
1250        assert!(
1251            unlock_forward.is_some(),
1252            "unlock (183) should be in warm-loaded base_function_implementation"
1253        );
1254    }
1255}