use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub const MAX_GRAPH_DEPTH: usize = 100;
pub const MAX_IMPORT_DEPTH: usize = 10;
pub type VisitedSet = HashSet<(PathBuf, String)>;
#[derive(Debug, Clone)]
pub struct CycleDetector {
visited: VisitedSet,
cycle_detected: bool,
depth: usize,
}
impl CycleDetector {
pub fn new() -> Self {
Self {
visited: HashSet::new(),
cycle_detected: false,
depth: 0,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
visited: HashSet::with_capacity(capacity),
cycle_detected: false,
depth: 0,
}
}
pub fn visit(&mut self, file: &Path, func: &str) -> bool {
let key = (file.to_path_buf(), func.to_string());
if !self.visited.insert(key) {
self.cycle_detected = true;
true
} else {
false
}
}
pub fn visit_file(&mut self, file: &Path) -> bool {
self.visit(file, "")
}
pub fn is_cycle_detected(&self) -> bool {
self.cycle_detected
}
pub fn visited_count(&self) -> usize {
self.visited.len()
}
pub fn was_visited(&self, file: &Path, func: &str) -> bool {
let key = (file.to_path_buf(), func.to_string());
self.visited.contains(&key)
}
pub fn visited_files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = self.visited.iter().map(|(path, _)| path.clone()).collect();
files.sort();
files.dedup();
files
}
pub fn reset(&mut self) {
self.visited.clear();
self.cycle_detected = false;
self.depth = 0;
}
pub fn enter_depth(&mut self) -> bool {
if self.depth >= MAX_GRAPH_DEPTH {
return false;
}
self.depth += 1;
true
}
pub fn exit_depth(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
pub fn current_depth(&self) -> usize {
self.depth
}
}
impl Default for CycleDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TraversalResult<T> {
pub results: Vec<T>,
pub complete: bool,
pub nodes_visited: usize,
pub cycle_detected: bool,
pub max_depth_reached: usize,
}
impl<T> TraversalResult<T> {
pub fn new() -> Self {
Self {
results: Vec::new(),
complete: true,
nodes_visited: 0,
cycle_detected: false,
max_depth_reached: 0,
}
}
pub fn mark_cycle(&mut self) {
self.cycle_detected = true;
self.complete = false;
}
pub fn mark_depth_limit(&mut self, depth: usize) {
self.complete = false;
if depth > self.max_depth_reached {
self.max_depth_reached = depth;
}
}
pub fn add(&mut self, item: T) {
self.results.push(item);
self.nodes_visited += 1;
}
}
impl<T> Default for TraversalResult<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cycle_detector_new() {
let detector = CycleDetector::new();
assert!(!detector.is_cycle_detected());
assert_eq!(detector.visited_count(), 0);
}
#[test]
fn test_cycle_detector_visit() {
let mut detector = CycleDetector::new();
assert!(!detector.visit(Path::new("file.py"), "func"));
assert_eq!(detector.visited_count(), 1);
assert!(detector.visit(Path::new("file.py"), "func"));
assert!(detector.is_cycle_detected());
assert!(!detector.visit(Path::new("other.py"), "func"));
assert_eq!(detector.visited_count(), 2);
assert!(!detector.visit(Path::new("file.py"), "other_func"));
assert_eq!(detector.visited_count(), 3);
}
#[test]
fn test_cycle_detector_was_visited() {
let mut detector = CycleDetector::new();
assert!(!detector.was_visited(Path::new("file.py"), "func"));
detector.visit(Path::new("file.py"), "func");
assert!(detector.was_visited(Path::new("file.py"), "func"));
assert!(!detector.was_visited(Path::new("file.py"), "other"));
}
#[test]
fn test_cycle_detector_reset() {
let mut detector = CycleDetector::new();
detector.visit(Path::new("file.py"), "func");
detector.visit(Path::new("file.py"), "func");
assert!(detector.is_cycle_detected());
assert_eq!(detector.visited_count(), 1);
detector.reset();
assert!(!detector.is_cycle_detected());
assert_eq!(detector.visited_count(), 0);
}
#[test]
fn test_cycle_detector_depth() {
let mut detector = CycleDetector::new();
assert_eq!(detector.current_depth(), 0);
assert!(detector.enter_depth());
assert_eq!(detector.current_depth(), 1);
detector.exit_depth();
assert_eq!(detector.current_depth(), 0);
for _ in 0..MAX_GRAPH_DEPTH {
assert!(detector.enter_depth());
}
assert!(!detector.enter_depth()); }
#[test]
fn test_cycle_detector_visited_files() {
let mut detector = CycleDetector::new();
detector.visit(Path::new("a.py"), "func1");
detector.visit(Path::new("a.py"), "func2");
detector.visit(Path::new("b.py"), "func1");
let files = detector.visited_files();
assert_eq!(files.len(), 2);
assert!(files.contains(&PathBuf::from("a.py")));
assert!(files.contains(&PathBuf::from("b.py")));
}
#[test]
fn test_traversal_result() {
let mut result: TraversalResult<String> = TraversalResult::new();
assert!(result.complete);
assert_eq!(result.nodes_visited, 0);
result.add("node1".to_string());
assert_eq!(result.nodes_visited, 1);
assert_eq!(result.results.len(), 1);
result.mark_cycle();
assert!(!result.complete);
assert!(result.cycle_detected);
}
}