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 let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
15
16 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 let mut file_paths: HashSet<String> = HashSet::new();
27 for node in nodes {
28 file_paths.insert(node.path.clone());
29 }
30
31 let known_file_ids: HashSet<String> = file_paths
33 .iter()
34 .map(|p| format!("file:{}", p))
35 .collect();
36
37 for edge in edges {
39 match edge.kind {
40 EdgeKind::Imports => {
41 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 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 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 resolved_edges.push(edge.clone());
89 }
90 }
91 }
92 EdgeKind::Exports => {
93 if node_ids.contains(edge.dst.as_str()) {
95 resolved_edges.push(edge.clone());
96 } else {
97 tracing::debug!("Unresolved export edge: {} -> {}", edge.src, edge.dst);
99 resolved_edges.push(edge.clone());
100 }
101 }
102 EdgeKind::Calls | EdgeKind::Inherits => {
103 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 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 resolved_edges.push(edge.clone());
120 }
121 }
122 _ => {
123 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 map.entry(node.path.clone()).or_insert(lang);
181 }
182 map
183}