1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use crate::parser::{EdgeDef, EdgeKind, NodeDef, NodeKind};
5
6pub fn is_test_path(path: &str) -> bool {
8 let lower = path.to_lowercase();
9 lower.contains("/test/")
10 || lower.contains("/tests/")
11 || lower.contains("/__tests__/")
12 || lower.contains("/spec/")
13 || lower.ends_with(".test.ts")
14 || lower.ends_with(".spec.ts")
15 || lower.ends_with(".test.js")
16 || lower.ends_with(".spec.js")
17 || lower.ends_with(".test.tsx")
18 || lower.ends_with(".spec.tsx")
19 || lower.ends_with("_test.py")
20 || lower.ends_with("_test.rs")
21}
22
23pub fn resolve(
24 nodes: &[NodeDef],
25 edges: &[EdgeDef],
26 _repo_root: &Path,
27) -> anyhow::Result<Vec<EdgeDef>> {
28 let mut resolved_edges: Vec<EdgeDef> = Vec::new();
29
30 let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
32
33 let mut export_index: HashMap<String, Vec<String>> = HashMap::new();
35 for node in nodes {
36 export_index
37 .entry(node.name.clone())
38 .or_default()
39 .push(node.id.clone());
40 }
41
42 let mut file_paths: HashSet<String> = HashSet::new();
44 for node in nodes {
45 file_paths.insert(node.path.clone());
46 }
47
48 let known_file_ids: HashSet<String> =
50 file_paths.iter().map(|p| format!("file:{}", p)).collect();
51
52 let test_paths: HashSet<&str> = nodes
54 .iter()
55 .filter(|n| is_test_path(&n.path))
56 .map(|n| n.path.as_str())
57 .collect();
58
59 let test_node_ids: HashSet<&str> = nodes
61 .iter()
62 .filter(|n| is_test_path(&n.path))
63 .map(|n| n.id.as_str())
64 .collect();
65 let _ = test_paths; for edge in edges {
69 match edge.kind {
70 EdgeKind::Imports => {
71 let dst_is_valid =
74 node_ids.contains(edge.dst.as_str()) || known_file_ids.contains(&edge.dst);
75
76 if dst_is_valid {
77 resolved_edges.push(EdgeDef {
78 src: edge.src.clone(),
79 dst: edge.dst.clone(),
80 kind: EdgeKind::Imports,
81 ..Default::default()
82 });
83 } else {
84 let import_target = edge.dst.trim_start_matches("file:");
86 let mut found = false;
87 for ext in &[".ts", ".tsx", ".js", ".jsx", ".py", ".rs"] {
88 let alt = format!("file:{}{}", import_target, ext);
89 if known_file_ids.contains(&alt) {
90 resolved_edges.push(EdgeDef {
91 src: edge.src.clone(),
92 dst: alt,
93 kind: EdgeKind::Imports,
94 ..Default::default()
95 });
96 found = true;
97 break;
98 }
99 }
100 if !found {
102 for index in &["/index.js", "/index.ts", "/index.jsx", "/index.tsx"] {
103 let alt = format!("file:{}{}", import_target, index);
104 if known_file_ids.contains(&alt) {
105 resolved_edges.push(EdgeDef {
106 src: edge.src.clone(),
107 dst: alt,
108 kind: EdgeKind::Imports,
109 ..Default::default()
110 });
111 found = true;
112 break;
113 }
114 }
115 }
116 if !found {
117 resolved_edges.push(edge.clone());
119 }
120 }
121 }
122 EdgeKind::Exports => {
123 if node_ids.contains(edge.dst.as_str()) {
125 resolved_edges.push(edge.clone());
126 } else {
127 tracing::debug!("Unresolved export edge: {} -> {}", edge.src, edge.dst);
129 resolved_edges.push(edge.clone());
130 }
131 }
132 EdgeKind::Calls | EdgeKind::Inherits => {
133 let src_is_test = test_node_ids.contains(edge.src.as_str());
135
136 if node_ids.contains(edge.dst.as_str()) {
138 let dst_is_test = test_node_ids.contains(edge.dst.as_str());
139 let kind = if src_is_test && !dst_is_test {
140 EdgeKind::Tests
141 } else {
142 edge.kind.clone()
143 };
144 resolved_edges.push(EdgeDef {
145 kind,
146 ..edge.clone()
147 });
148 } else if let Some(targets) = export_index.get(&edge.dst) {
149 for target_id in targets {
151 let dst_is_test = test_node_ids.contains(target_id.as_str());
152 let kind = if src_is_test && !dst_is_test {
153 EdgeKind::Tests
154 } else {
155 EdgeKind::Calls
156 };
157 resolved_edges.push(EdgeDef {
158 src: edge.src.clone(),
159 dst: target_id.clone(),
160 kind,
161 confidence: 0.8,
162 ..Default::default()
163 });
164 }
165 } else {
166 resolved_edges.push(edge.clone());
168 }
169 }
170 _ => {
171 resolved_edges.push(edge.clone());
173 }
174 }
175 }
176
177 Ok(resolved_edges)
178}
179
180pub fn create_file_nodes(
181 file_paths: &HashSet<String>,
182 language: &HashMap<String, &str>,
183) -> Vec<NodeDef> {
184 let mut nodes = Vec::new();
185
186 for path in file_paths {
187 let id = format!("file:{}", path);
188 let _lang = language.get(path.as_str()).copied().unwrap_or("unknown");
189
190 nodes.push(NodeDef {
191 id,
192 kind: NodeKind::File,
193 name: path.clone(),
194 path: path.clone(),
195 line_start: 1,
196 line_end: 1,
197 ..Default::default()
198 });
199 }
200
201 nodes
202}
203
204pub fn build_language_map(nodes: &[NodeDef]) -> HashMap<String, &'static str> {
205 let mut map = HashMap::new();
206 for node in nodes {
207 let lang = match node.id.split(':').next().unwrap_or("") {
208 "fn" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
209 "fn" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
210 "fn" if node.path.ends_with(".py") => "python",
211 "fn" if node.path.ends_with(".rs") => "rust",
212 "cls" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
213 "cls" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
214 "cls" if node.path.ends_with(".py") => "python",
215 "cls" if node.path.ends_with(".rs") => "rust",
216 "file" if node.path.ends_with(".ts") || node.path.ends_with(".tsx") => "typescript",
217 "file" if node.path.ends_with(".js") || node.path.ends_with(".jsx") => "javascript",
218 "file" if node.path.ends_with(".py") => "python",
219 "file" if node.path.ends_with(".rs") => "rust",
220 _ => "unknown",
221 };
222 map.entry(node.path.clone()).or_insert(lang);
224 }
225 map
226}