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