1pub mod indexer;
2pub mod persist;
3pub mod resolve;
4
5use std::collections::hash_map::DefaultHasher;
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::hash::{Hash, Hasher};
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12pub type SymbolId = u64;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum SymbolKind {
18 Function,
19 Method,
20 Struct,
21 Class,
22 Trait,
23 Interface,
24 Enum,
25 Constant,
26 Variable,
27 Module,
28 Import,
29 TypeAlias,
30 Other(String),
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum Visibility {
36 Public,
37 Private,
38 Protected,
39 Internal,
40 Unknown,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SymbolNode {
46 pub id: SymbolId,
47 pub name: String,
48 pub kind: SymbolKind,
49 pub visibility: Visibility,
50 pub file: PathBuf,
51 pub start_line: usize,
52 pub end_line: usize,
53 pub signature: Option<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum EdgeKind {
60 Calls,
61 Imports,
62 Inherits,
63 Implements,
64 References,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Edge {
73 pub to: SymbolId,
74 pub kind: EdgeKind,
75 pub line: usize,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct CodeGraph {
81 pub nodes: HashMap<SymbolId, SymbolNode>,
82 pub edges_out: HashMap<SymbolId, Vec<Edge>>,
83 pub edges_in: HashMap<SymbolId, Vec<Edge>>,
84 pub file_symbols: HashMap<PathBuf, Vec<SymbolId>>,
85 pub file_mtimes: HashMap<PathBuf, u64>,
86}
87
88impl CodeGraph {
89 pub fn new() -> Self {
91 Self {
92 nodes: HashMap::new(),
93 edges_out: HashMap::new(),
94 edges_in: HashMap::new(),
95 file_symbols: HashMap::new(),
96 file_mtimes: HashMap::new(),
97 }
98 }
99
100 pub fn make_id(file: &PathBuf, name: &str, start_line: usize) -> SymbolId {
102 let mut hasher = DefaultHasher::new();
103 file.hash(&mut hasher);
104 name.hash(&mut hasher);
105 start_line.hash(&mut hasher);
106 hasher.finish()
107 }
108
109 pub fn add_symbol(&mut self, node: SymbolNode) {
111 let id = node.id;
112 let file = node.file.clone();
113 self.nodes.insert(id, node);
114 self.file_symbols.entry(file).or_default().push(id);
115 }
116
117 pub fn add_edge(&mut self, from: SymbolId, edge: Edge) {
119 let to = edge.to;
120 let kind = edge.kind.clone();
121 let line = edge.line;
122
123 self.edges_out.entry(from).or_default().push(edge);
124 self.edges_in.entry(to).or_default().push(Edge {
125 to: from,
126 kind,
127 line,
128 });
129 }
130
131 pub fn node(&self, id: SymbolId) -> Option<&SymbolNode> {
133 self.nodes.get(&id)
134 }
135
136 pub fn symbols_in_file(&self, file: &PathBuf) -> Option<&Vec<SymbolId>> {
138 self.file_symbols.get(file)
139 }
140
141 pub fn callees(&self, id: SymbolId) -> Option<&Vec<Edge>> {
143 self.edges_out.get(&id)
144 }
145
146 pub fn callers(&self, id: SymbolId) -> Option<&Vec<Edge>> {
148 self.edges_in.get(&id)
149 }
150
151 pub fn remove_file(&mut self, file: &PathBuf) {
153 let symbol_ids = match self.file_symbols.remove(file) {
154 Some(ids) => ids,
155 None => return,
156 };
157
158 for &id in &symbol_ids {
159 self.nodes.remove(&id);
160
161 if let Some(out_edges) = self.edges_out.remove(&id) {
163 for edge in &out_edges {
164 if let Some(in_list) = self.edges_in.get_mut(&edge.to) {
165 in_list.retain(|e| e.to != id);
166 if in_list.is_empty() {
167 self.edges_in.remove(&edge.to);
168 }
169 }
170 }
171 }
172
173 if let Some(in_edges) = self.edges_in.remove(&id) {
175 for edge in &in_edges {
176 if let Some(out_list) = self.edges_out.get_mut(&edge.to) {
177 out_list.retain(|e| e.to != id);
178 if out_list.is_empty() {
179 self.edges_out.remove(&edge.to);
180 }
181 }
182 }
183 }
184 }
185
186 self.file_mtimes.remove(file);
187 }
188
189 pub fn find_by_name(&self, name: &str) -> Vec<&SymbolNode> {
191 self.nodes.values().filter(|n| n.name == name).collect()
192 }
193
194 pub fn node_count(&self) -> usize {
196 self.nodes.len()
197 }
198
199 pub fn file_count(&self) -> usize {
201 self.file_symbols.len()
202 }
203
204 pub fn is_ready(&self) -> bool {
206 !self.nodes.is_empty()
207 }
208
209 pub fn trace_callers(&self, id: SymbolId, max_depth: usize) -> Vec<(SymbolId, usize)> {
211 let mut visited = HashSet::new();
212 let mut queue = VecDeque::new();
213 let mut result = Vec::new();
214 visited.insert(id);
215 queue.push_back((id, 0usize));
216 while let Some((current, depth)) = queue.pop_front() {
217 if depth >= max_depth {
218 continue;
219 }
220 if let Some(edges) = self.callers(current) {
221 for edge in edges {
222 if visited.insert(edge.to) {
223 result.push((edge.to, depth + 1));
224 queue.push_back((edge.to, depth + 1));
225 }
226 }
227 }
228 }
229 result
230 }
231
232 pub fn trace_callees(&self, id: SymbolId, max_depth: usize) -> Vec<(SymbolId, usize)> {
234 let mut visited = HashSet::new();
235 let mut queue = VecDeque::new();
236 let mut result = Vec::new();
237 visited.insert(id);
238 queue.push_back((id, 0usize));
239 while let Some((current, depth)) = queue.pop_front() {
240 if depth >= max_depth {
241 continue;
242 }
243 if let Some(edges) = self.callees(current) {
244 for edge in edges {
245 if visited.insert(edge.to) {
246 result.push((edge.to, depth + 1));
247 queue.push_back((edge.to, depth + 1));
248 }
249 }
250 }
251 }
252 result
253 }
254
255 pub fn shortest_path(&self, from: SymbolId, to: SymbolId) -> Option<Vec<SymbolId>> {
258 if from == to {
259 return Some(vec![from]);
260 }
261 let max_hops = 10;
262 let mut visited = HashSet::new();
263 let mut queue = VecDeque::new();
264 let mut parent: HashMap<SymbolId, SymbolId> = HashMap::new();
265 visited.insert(from);
266 queue.push_back((from, 0usize));
267 while let Some((current, depth)) = queue.pop_front() {
268 if depth >= max_hops {
269 continue;
270 }
271 if let Some(edges) = self.callees(current) {
272 for edge in edges {
273 if visited.insert(edge.to) {
274 parent.insert(edge.to, current);
275 if edge.to == to {
276 let mut path = vec![to];
278 let mut cur = to;
279 while let Some(&p) = parent.get(&cur) {
280 path.push(p);
281 cur = p;
282 }
283 path.reverse();
284 return Some(path);
285 }
286 queue.push_back((edge.to, depth + 1));
287 }
288 }
289 }
290 }
291 None
292 }
293
294 pub fn file_dependents(&self, file: &Path, max_depth: usize) -> Vec<PathBuf> {
296 let file_buf = file.to_path_buf();
297 let symbol_ids = match self.file_symbols.get(&file_buf) {
298 Some(ids) => ids.clone(),
299 None => return Vec::new(),
300 };
301 let mut dependent_files = HashSet::new();
302 for &sym_id in &symbol_ids {
303 for (caller_id, _depth) in self.trace_callers(sym_id, max_depth) {
304 if let Some(node) = self.node(caller_id) {
305 if node.file != file_buf {
306 dependent_files.insert(node.file.clone());
307 }
308 }
309 }
310 }
311 dependent_files.into_iter().collect()
312 }
313
314 pub fn file_dependency_summary(&self, filename: &str) -> Option<String> {
316 let (path, sym_ids) = self.file_symbols.iter().find(|(p, _)| {
317 p.file_name()
318 .map(|f| f.to_string_lossy() == filename)
319 .unwrap_or(false)
320 })?;
321 let path = path.clone();
322
323 let mut uses: Vec<String> = Vec::new();
324 for &sid in sym_ids.iter().take(10) {
325 if let Some(edges) = self.callees(sid) {
326 for edge in edges {
327 if let Some(node) = self.node(edge.to) {
328 if node.file != path {
329 let f = node
330 .file
331 .file_name()
332 .map(|n| n.to_string_lossy().to_string())
333 .unwrap_or_default();
334 if !uses.contains(&f) {
335 uses.push(f);
336 }
337 }
338 }
339 }
340 }
341 }
342
343 let deps = self.file_dependents(&path, 2);
344 let dep_names: Vec<String> = deps
345 .iter()
346 .filter_map(|p| p.file_name().map(|f| f.to_string_lossy().to_string()))
347 .collect();
348
349 if uses.is_empty() && dep_names.is_empty() {
350 return None;
351 }
352
353 let mut info = format!("[Graph: {}]", filename);
354 if !uses.is_empty() {
355 info.push_str(&format!(" uses: {}", uses.join(", ")));
356 }
357 if !dep_names.is_empty() {
358 info.push_str(&format!(" | used by: {}", dep_names.join(", ")));
359 }
360 Some(info)
361 }
362
363 pub fn call_chain_summary(&self, fn_name: &str) -> Option<String> {
367 let symbols = self.find_by_name(fn_name);
368 let sym = symbols
370 .iter()
371 .filter(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method))
372 .max_by_key(|s| self.callees(s.id).map(|e| e.len()).unwrap_or(0))?;
373
374 let callees = self.trace_callees(sym.id, 3);
375 if callees.is_empty() {
376 return None;
377 }
378
379 let mut chain = format!("[Call chain: {}()", fn_name);
380 for (callee_id, depth) in &callees {
381 if let Some(node) = self.node(*callee_id) {
382 let short_file = node
383 .file
384 .file_name()
385 .map(|f| f.to_string_lossy().to_string())
386 .unwrap_or_default();
387 let indent = " → ".repeat(*depth);
388 chain.push_str(&format!(
389 "\n {}{}() ({}:{})",
390 indent, node.name, short_file, node.start_line
391 ));
392 }
393 }
394 chain.push_str("]\n");
395 chain.push_str(
396 "[SCOPE: The issue is in ONE of these functions. \
397 Read ONLY these files — do not explore outside this chain.]",
398 );
399 Some(chain)
400 }
401}
402
403impl Default for CodeGraph {
404 fn default() -> Self {
405 Self::new()
406 }
407}