Skip to main content

agentic_codebase/semantic/
analyzer.rs

1//! Main semantic analyzer.
2//!
3//! Orchestrates the semantic analysis pipeline: build symbol table,
4//! resolve references, trace FFI, detect patterns, extract concepts,
5//! and build the final CodeGraph.
6
7use std::collections::HashMap;
8
9use crate::graph::CodeGraph;
10use crate::parse::{RawCodeUnit, ReferenceKind};
11use crate::types::{
12    AcbResult, CodeUnit, CodeUnitType, Edge, EdgeType, Language, DEFAULT_DIMENSION,
13};
14
15use super::concept_extractor::{ConceptExtractor, ExtractedConcept};
16use super::ffi_tracer::{FfiEdge, FfiTracer};
17use super::pattern_detector::{PatternDetector, PatternInstance};
18use super::resolver::{Resolution, ResolvedUnit, Resolver, SymbolTable};
19
20/// Options for the semantic analysis pass.
21#[derive(Debug, Clone)]
22pub struct AnalyzeOptions {
23    /// Detect design patterns.
24    pub detect_patterns: bool,
25    /// Extract high-level concepts.
26    pub extract_concepts: bool,
27    /// Trace FFI boundaries.
28    pub trace_ffi: bool,
29}
30
31impl Default for AnalyzeOptions {
32    fn default() -> Self {
33        Self {
34            detect_patterns: true,
35            extract_concepts: true,
36            trace_ffi: true,
37        }
38    }
39}
40
41/// The main semantic analysis orchestrator.
42pub struct SemanticAnalyzer {
43    resolver: Resolver,
44    ffi_tracer: FfiTracer,
45    pattern_detector: PatternDetector,
46    concept_extractor: ConceptExtractor,
47}
48
49impl SemanticAnalyzer {
50    /// Create a new semantic analyzer.
51    pub fn new() -> Self {
52        Self {
53            resolver: Resolver::new(),
54            ffi_tracer: FfiTracer::new(),
55            pattern_detector: PatternDetector::new(),
56            concept_extractor: ConceptExtractor::new(),
57        }
58    }
59
60    /// Analyze raw units and produce a CodeGraph.
61    pub fn analyze(
62        &self,
63        raw_units: Vec<RawCodeUnit>,
64        options: &AnalyzeOptions,
65    ) -> AcbResult<CodeGraph> {
66        // Phase 1: Build symbol table
67        let symbol_table = SymbolTable::build(&raw_units)?;
68
69        // Phase 2: Resolve references
70        let resolved = self.resolver.resolve_all(&raw_units, &symbol_table)?;
71
72        // Phase 3: Trace FFI boundaries
73        let ffi_edges = if options.trace_ffi {
74            self.ffi_tracer.trace(&resolved)?
75        } else {
76            Vec::new()
77        };
78
79        // Phase 4: Detect patterns
80        let patterns = if options.detect_patterns {
81            self.pattern_detector.detect(&resolved)?
82        } else {
83            Vec::new()
84        };
85
86        // Phase 5: Extract concepts
87        let concepts = if options.extract_concepts {
88            self.concept_extractor.extract(&resolved)?
89        } else {
90            Vec::new()
91        };
92
93        // Phase 6: Build final graph
94        self.build_graph(resolved, ffi_edges, patterns, concepts)
95    }
96
97    /// Build the CodeGraph from resolved units and analysis results.
98    fn build_graph(
99        &self,
100        resolved: Vec<ResolvedUnit>,
101        ffi_edges: Vec<FfiEdge>,
102        patterns: Vec<PatternInstance>,
103        _concepts: Vec<ExtractedConcept>,
104    ) -> AcbResult<CodeGraph> {
105        let mut graph = CodeGraph::new(DEFAULT_DIMENSION);
106
107        // Map from temp_id → graph_id
108        let mut id_map: HashMap<u64, u64> = HashMap::new();
109
110        // Add all units to the graph
111        for runit in &resolved {
112            let raw = &runit.unit;
113            let code_unit = CodeUnit::new(
114                raw.unit_type,
115                raw.language,
116                raw.name.clone(),
117                raw.qualified_name.clone(),
118                raw.file_path.clone(),
119                raw.span,
120            );
121            let mut cu = code_unit;
122            cu.signature = raw.signature.clone();
123            cu.doc_summary = raw.doc.clone();
124            cu.visibility = raw.visibility;
125            cu.complexity = raw.complexity;
126            cu.is_async = raw.is_async;
127            cu.is_generator = raw.is_generator;
128
129            let graph_id = graph.add_unit(cu);
130            id_map.insert(raw.temp_id, graph_id);
131        }
132
133        // Add edges from resolved references
134        for runit in &resolved {
135            let source_temp = runit.unit.temp_id;
136            let source_graph_id = match id_map.get(&source_temp) {
137                Some(&id) => id,
138                None => continue,
139            };
140
141            for ref_info in &runit.resolved_refs {
142                match &ref_info.resolution {
143                    Resolution::Local(target_temp) => {
144                        if let Some(&target_graph_id) = id_map.get(target_temp) {
145                            if source_graph_id == target_graph_id {
146                                continue; // skip self-edges
147                            }
148                            let edge_type = reference_to_edge_type(ref_info.raw.kind);
149                            let edge = Edge::new(source_graph_id, target_graph_id, edge_type);
150                            // Ignore duplicate edge errors
151                            let _ = graph.add_edge(edge);
152                        }
153                    }
154                    Resolution::Imported(imported) => {
155                        if let Some(&target_graph_id) = id_map.get(&imported.unit_id) {
156                            if source_graph_id == target_graph_id {
157                                continue;
158                            }
159                            let edge =
160                                Edge::new(source_graph_id, target_graph_id, EdgeType::Imports);
161                            let _ = graph.add_edge(edge);
162                        }
163                    }
164                    Resolution::External(_) | Resolution::Unresolved => {
165                        // No edges to external/unresolved
166                    }
167                }
168            }
169        }
170
171        // Add containment edges: module → its children (by file co-location)
172        self.add_containment_edges(&resolved, &id_map, &mut graph);
173
174        // Add FFI edges
175        for ffi_edge in ffi_edges {
176            if let Some(&source_id) = id_map.get(&ffi_edge.source_id) {
177                if let Some(target_temp) = ffi_edge.target_id {
178                    if let Some(&target_id) = id_map.get(&target_temp) {
179                        if source_id != target_id {
180                            let edge = Edge::new(source_id, target_id, EdgeType::FfiBinds);
181                            let _ = graph.add_edge(edge);
182                        }
183                    }
184                }
185            }
186        }
187
188        // Add pattern edges
189        for pattern in patterns {
190            if let Some(&primary_id) = id_map.get(&pattern.primary_unit) {
191                // Create a pattern node
192                let pattern_unit = CodeUnit::new(
193                    CodeUnitType::Pattern,
194                    Language::Unknown,
195                    pattern.pattern_name.clone(),
196                    format!("pattern::{}", pattern.pattern_name),
197                    std::path::PathBuf::new(),
198                    crate::types::Span::point(0, 0),
199                );
200                let pattern_graph_id = graph.add_unit(pattern_unit);
201                let edge = Edge::new(primary_id, pattern_graph_id, EdgeType::PatternOf);
202                let _ = graph.add_edge(edge);
203            }
204        }
205
206        Ok(graph)
207    }
208
209    /// Add containment edges: module → functions/classes/etc. in the same file.
210    fn add_containment_edges(
211        &self,
212        resolved: &[ResolvedUnit],
213        id_map: &HashMap<u64, u64>,
214        graph: &mut CodeGraph,
215    ) {
216        // Group units by file
217        let mut file_groups: HashMap<String, Vec<&ResolvedUnit>> = HashMap::new();
218        for runit in resolved {
219            let key = runit.unit.file_path.to_string_lossy().to_string();
220            file_groups.entry(key).or_default().push(runit);
221        }
222
223        for units_in_file in file_groups.values() {
224            // Find the module unit for this file
225            let module = units_in_file
226                .iter()
227                .find(|u| u.unit.unit_type == CodeUnitType::Module);
228
229            if let Some(module_unit) = module {
230                let module_graph_id = match id_map.get(&module_unit.unit.temp_id) {
231                    Some(&id) => id,
232                    None => continue,
233                };
234
235                // All other non-module, non-import units in this file are children
236                for other in units_in_file {
237                    if other.unit.temp_id == module_unit.unit.temp_id {
238                        continue;
239                    }
240                    if other.unit.unit_type == CodeUnitType::Import {
241                        continue;
242                    }
243                    if let Some(&other_id) = id_map.get(&other.unit.temp_id) {
244                        let edge = Edge::new(module_graph_id, other_id, EdgeType::Contains);
245                        let _ = graph.add_edge(edge);
246                    }
247                }
248            }
249        }
250    }
251}
252
253impl Default for SemanticAnalyzer {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259/// Convert a raw reference kind to an edge type.
260fn reference_to_edge_type(kind: ReferenceKind) -> EdgeType {
261    match kind {
262        ReferenceKind::Call => EdgeType::Calls,
263        ReferenceKind::Import => EdgeType::Imports,
264        ReferenceKind::TypeUse => EdgeType::UsesType,
265        ReferenceKind::Inherit => EdgeType::Inherits,
266        ReferenceKind::Implement => EdgeType::Implements,
267        ReferenceKind::Access => EdgeType::References,
268    }
269}