tldr_cli/commands/remaining/
graph_utils.rs1use std::collections::HashSet;
15use std::path::{Path, PathBuf};
16
17pub const MAX_GRAPH_DEPTH: usize = 100;
23
24pub const MAX_IMPORT_DEPTH: usize = 10;
26
27pub type VisitedSet = HashSet<(PathBuf, String)>;
33
34#[derive(Debug, Clone)]
54pub struct CycleDetector {
55 visited: VisitedSet,
57 cycle_detected: bool,
59 depth: usize,
61}
62
63impl CycleDetector {
64 pub fn new() -> Self {
66 Self {
67 visited: HashSet::new(),
68 cycle_detected: false,
69 depth: 0,
70 }
71 }
72
73 pub fn with_capacity(capacity: usize) -> Self {
75 Self {
76 visited: HashSet::with_capacity(capacity),
77 cycle_detected: false,
78 depth: 0,
79 }
80 }
81
82 pub fn visit(&mut self, file: &Path, func: &str) -> bool {
87 let key = (file.to_path_buf(), func.to_string());
88 if !self.visited.insert(key) {
89 self.cycle_detected = true;
90 true
91 } else {
92 false
93 }
94 }
95
96 pub fn visit_file(&mut self, file: &Path) -> bool {
98 self.visit(file, "")
99 }
100
101 pub fn is_cycle_detected(&self) -> bool {
103 self.cycle_detected
104 }
105
106 pub fn visited_count(&self) -> usize {
108 self.visited.len()
109 }
110
111 pub fn was_visited(&self, file: &Path, func: &str) -> bool {
113 let key = (file.to_path_buf(), func.to_string());
114 self.visited.contains(&key)
115 }
116
117 pub fn visited_files(&self) -> Vec<PathBuf> {
119 let mut files: Vec<PathBuf> = self.visited.iter().map(|(path, _)| path.clone()).collect();
120 files.sort();
121 files.dedup();
122 files
123 }
124
125 pub fn reset(&mut self) {
127 self.visited.clear();
128 self.cycle_detected = false;
129 self.depth = 0;
130 }
131
132 pub fn enter_depth(&mut self) -> bool {
134 if self.depth >= MAX_GRAPH_DEPTH {
135 return false;
136 }
137 self.depth += 1;
138 true
139 }
140
141 pub fn exit_depth(&mut self) {
143 self.depth = self.depth.saturating_sub(1);
144 }
145
146 pub fn current_depth(&self) -> usize {
148 self.depth
149 }
150}
151
152impl Default for CycleDetector {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[derive(Debug, Clone)]
164pub struct TraversalResult<T> {
165 pub results: Vec<T>,
167 pub complete: bool,
169 pub nodes_visited: usize,
171 pub cycle_detected: bool,
173 pub max_depth_reached: usize,
175}
176
177impl<T> TraversalResult<T> {
178 pub fn new() -> Self {
180 Self {
181 results: Vec::new(),
182 complete: true,
183 nodes_visited: 0,
184 cycle_detected: false,
185 max_depth_reached: 0,
186 }
187 }
188
189 pub fn mark_cycle(&mut self) {
191 self.cycle_detected = true;
192 self.complete = false;
193 }
194
195 pub fn mark_depth_limit(&mut self, depth: usize) {
197 self.complete = false;
198 if depth > self.max_depth_reached {
199 self.max_depth_reached = depth;
200 }
201 }
202
203 pub fn add(&mut self, item: T) {
205 self.results.push(item);
206 self.nodes_visited += 1;
207 }
208}
209
210impl<T> Default for TraversalResult<T> {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_cycle_detector_new() {
226 let detector = CycleDetector::new();
227 assert!(!detector.is_cycle_detected());
228 assert_eq!(detector.visited_count(), 0);
229 }
230
231 #[test]
232 fn test_cycle_detector_visit() {
233 let mut detector = CycleDetector::new();
234
235 assert!(!detector.visit(Path::new("file.py"), "func"));
237 assert_eq!(detector.visited_count(), 1);
238
239 assert!(detector.visit(Path::new("file.py"), "func"));
241 assert!(detector.is_cycle_detected());
242
243 assert!(!detector.visit(Path::new("other.py"), "func"));
245 assert_eq!(detector.visited_count(), 2);
246
247 assert!(!detector.visit(Path::new("file.py"), "other_func"));
249 assert_eq!(detector.visited_count(), 3);
250 }
251
252 #[test]
253 fn test_cycle_detector_was_visited() {
254 let mut detector = CycleDetector::new();
255
256 assert!(!detector.was_visited(Path::new("file.py"), "func"));
257
258 detector.visit(Path::new("file.py"), "func");
259
260 assert!(detector.was_visited(Path::new("file.py"), "func"));
261 assert!(!detector.was_visited(Path::new("file.py"), "other"));
262 }
263
264 #[test]
265 fn test_cycle_detector_reset() {
266 let mut detector = CycleDetector::new();
267
268 detector.visit(Path::new("file.py"), "func");
269 detector.visit(Path::new("file.py"), "func"); assert!(detector.is_cycle_detected());
272 assert_eq!(detector.visited_count(), 1);
273
274 detector.reset();
275
276 assert!(!detector.is_cycle_detected());
277 assert_eq!(detector.visited_count(), 0);
278 }
279
280 #[test]
281 fn test_cycle_detector_depth() {
282 let mut detector = CycleDetector::new();
283
284 assert_eq!(detector.current_depth(), 0);
285
286 assert!(detector.enter_depth());
287 assert_eq!(detector.current_depth(), 1);
288
289 detector.exit_depth();
290 assert_eq!(detector.current_depth(), 0);
291
292 for _ in 0..MAX_GRAPH_DEPTH {
294 assert!(detector.enter_depth());
295 }
296 assert!(!detector.enter_depth()); }
298
299 #[test]
300 fn test_cycle_detector_visited_files() {
301 let mut detector = CycleDetector::new();
302
303 detector.visit(Path::new("a.py"), "func1");
304 detector.visit(Path::new("a.py"), "func2");
305 detector.visit(Path::new("b.py"), "func1");
306
307 let files = detector.visited_files();
308 assert_eq!(files.len(), 2);
309 assert!(files.contains(&PathBuf::from("a.py")));
310 assert!(files.contains(&PathBuf::from("b.py")));
311 }
312
313 #[test]
314 fn test_traversal_result() {
315 let mut result: TraversalResult<String> = TraversalResult::new();
316
317 assert!(result.complete);
318 assert_eq!(result.nodes_visited, 0);
319
320 result.add("node1".to_string());
321 assert_eq!(result.nodes_visited, 1);
322 assert_eq!(result.results.len(), 1);
323
324 result.mark_cycle();
325 assert!(!result.complete);
326 assert!(result.cycle_detected);
327 }
328}