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