Skip to main content

cgx_engine/
resolver.rs

1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use crate::parser::{EdgeDef, EdgeKind, NodeDef, NodeKind};
5
6pub fn resolve(
7    nodes: &[NodeDef],
8    edges: &[EdgeDef],
9    _repo_root: &Path,
10) -> anyhow::Result<Vec<EdgeDef>> {
11    let mut resolved_edges: Vec<EdgeDef> = Vec::new();
12
13    // Create a set of all known node IDs for validation
14    let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
15
16    // Build export index: name -> Vec<node_id>
17    let mut export_index: HashMap<String, Vec<String>> = HashMap::new();
18    for node in nodes {
19        export_index
20            .entry(node.name.clone())
21            .or_default()
22            .push(node.id.clone());
23    }
24
25    // Build file node set from actual files
26    let mut file_paths: HashSet<String> = HashSet::new();
27    for node in nodes {
28        file_paths.insert(node.path.clone());
29    }
30
31    // Create file node IDs we know about
32    let known_file_ids: HashSet<String> =
33        file_paths.iter().map(|p| format!("file:{}", p)).collect();
34
35    // Process all edges
36    for edge in edges {
37        match edge.kind {
38            EdgeKind::Imports => {
39                // src = file:<current_file>, dst = file:<imported_file>
40                // Check if dst is a valid file ID, or try to resolve it
41                let dst_is_valid =
42                    node_ids.contains(edge.dst.as_str()) || known_file_ids.contains(&edge.dst);
43
44                if dst_is_valid {
45                    resolved_edges.push(EdgeDef {
46                        src: edge.src.clone(),
47                        dst: edge.dst.clone(),
48                        kind: EdgeKind::Imports,
49                        ..Default::default()
50                    });
51                } else {
52                    // Try with different extensions
53                    let import_target = edge.dst.trim_start_matches("file:");
54                    let mut found = false;
55                    for ext in &[".ts", ".tsx", ".js", ".jsx", ".py", ".rs"] {
56                        let alt = format!("file:{}{}", import_target, ext);
57                        if known_file_ids.contains(&alt) {
58                            resolved_edges.push(EdgeDef {
59                                src: edge.src.clone(),
60                                dst: alt,
61                                kind: EdgeKind::Imports,
62                                ..Default::default()
63                            });
64                            found = true;
65                            break;
66                        }
67                    }
68                    // Try directory index files (Node.js resolution)
69                    if !found {
70                        for index in &["/index.js", "/index.ts", "/index.jsx", "/index.tsx"] {
71                            let alt = format!("file:{}{}", import_target, index);
72                            if known_file_ids.contains(&alt) {
73                                resolved_edges.push(EdgeDef {
74                                    src: edge.src.clone(),
75                                    dst: alt,
76                                    kind: EdgeKind::Imports,
77                                    ..Default::default()
78                                });
79                                found = true;
80                                break;
81                            }
82                        }
83                    }
84                    if !found {
85                        // Include unresolvable imports too (they may still be useful)
86                        resolved_edges.push(edge.clone());
87                    }
88                }
89            }
90            EdgeKind::Exports => {
91                // src = file:<path>, dst = fn:path:name or cls:path:name
92                if node_ids.contains(edge.dst.as_str()) {
93                    resolved_edges.push(edge.clone());
94                } else {
95                    // Keep the edge but log a warning — may be resolved in a later pass
96                    tracing::debug!("Unresolved export edge: {} -> {}", edge.src, edge.dst);
97                    resolved_edges.push(edge.clone());
98                }
99            }
100            EdgeKind::Calls | EdgeKind::Inherits => {
101                // If dst is a valid node ID, keep it. Otherwise try to resolve by name.
102                if node_ids.contains(edge.dst.as_str()) {
103                    resolved_edges.push(edge.clone());
104                } else if let Some(targets) = export_index.get(&edge.dst) {
105                    // Found matching names - create CALLS edges with lower confidence
106                    for target_id in targets {
107                        resolved_edges.push(EdgeDef {
108                            src: edge.src.clone(),
109                            dst: target_id.clone(),
110                            kind: EdgeKind::Calls,
111                            confidence: 0.8,
112                            ..Default::default()
113                        });
114                    }
115                } else {
116                    // Keep the edge even if unresolved (maybe a future phase can handle)
117                    resolved_edges.push(edge.clone());
118                }
119            }
120            _ => {
121                // CoChanges, Owns, DependsOn — pass through unchanged
122                resolved_edges.push(edge.clone());
123            }
124        }
125    }
126
127    Ok(resolved_edges)
128}
129
130pub fn create_file_nodes(
131    file_paths: &HashSet<String>,
132    language: &HashMap<String, &str>,
133) -> Vec<NodeDef> {
134    let mut nodes = Vec::new();
135
136    for path in file_paths {
137        let id = format!("file:{}", path);
138        let _lang = language.get(path.as_str()).copied().unwrap_or("unknown");
139
140        nodes.push(NodeDef {
141            id,
142            kind: NodeKind::File,
143            name: path.clone(),
144            path: path.clone(),
145            line_start: 1,
146            line_end: 1,
147            ..Default::default()
148        });
149    }
150
151    nodes
152}
153
154pub fn build_language_map(nodes: &[NodeDef]) -> HashMap<String, &'static str> {
155    let mut map = HashMap::new();
156    for node in nodes {
157        let lang = match node.id.split(':').next().unwrap_or("") {
158            "fn" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
159            "fn" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
160            "fn" if node.path.ends_with(".py") => "python",
161            "fn" if node.path.ends_with(".rs") => "rust",
162            "cls" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
163            "cls" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
164            "cls" if node.path.ends_with(".py") => "python",
165            "cls" if node.path.ends_with(".rs") => "rust",
166            "file" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
167            "file" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
168            "file" if node.path.ends_with(".py") => "python",
169            "file" if node.path.ends_with(".rs") => "rust",
170            _ => "unknown",
171        };
172        // Only insert if not already present (function/class nodes take priority)
173        map.entry(node.path.clone()).or_insert(lang);
174    }
175    map
176}