use std::collections::{HashMap, HashSet};
use super::file_table::FileId;
#[derive(Debug, Default, Clone)]
pub struct DepGraph {
forward: HashMap<FileId, HashSet<FileId>>,
reverse: HashMap<FileId, HashSet<FileId>>,
unresolved: HashMap<FileId, Vec<String>>,
}
impl DepGraph {
pub fn new() -> Self {
Self::default()
}
pub fn set_edges(&mut self, file: FileId, resolved: HashSet<FileId>, unresolved: Vec<String>) {
if let Some(old) = self.forward.remove(&file) {
for target in &old {
if *target == file {
continue;
}
if let Some(set) = self.reverse.get_mut(target) {
set.remove(&file);
if set.is_empty() {
self.reverse.remove(target);
}
}
}
}
for target in &resolved {
if *target == file {
continue;
}
self.reverse.entry(*target).or_default().insert(file);
}
self.forward.insert(file, resolved);
if unresolved.is_empty() {
self.unresolved.remove(&file);
} else {
self.unresolved.insert(file, unresolved);
}
}
pub fn remove_file(&mut self, file: FileId) {
if let Some(old) = self.forward.remove(&file) {
for target in old {
if target == file {
continue;
}
if let Some(set) = self.reverse.get_mut(&target) {
set.remove(&file);
if set.is_empty() {
self.reverse.remove(&target);
}
}
}
}
self.unresolved.remove(&file);
if let Some(rev) = self.reverse.remove(&file) {
for importer in rev {
if let Some(forward) = self.forward.get_mut(&importer) {
forward.remove(&file);
}
}
}
}
pub fn imports_of(&self, file: FileId) -> Vec<FileId> {
self.forward
.get(&file)
.map(|set| set.iter().copied().collect())
.unwrap_or_default()
}
pub fn importers_of(&self, file: FileId) -> Vec<FileId> {
self.reverse
.get(&file)
.map(|set| set.iter().copied().collect())
.unwrap_or_default()
}
pub fn unresolved_imports(&self, file: FileId) -> &[String] {
self.unresolved.get(&file).map(Vec::as_slice).unwrap_or(&[])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set_of(ids: &[FileId]) -> HashSet<FileId> {
ids.iter().copied().collect()
}
#[test]
fn set_edges_populates_both_sides() {
let mut g = DepGraph::new();
g.set_edges(1, set_of(&[2, 3]), vec![]);
let mut imports = g.imports_of(1);
imports.sort();
assert_eq!(imports, vec![2, 3]);
assert_eq!(g.importers_of(2), vec![1]);
assert_eq!(g.importers_of(3), vec![1]);
}
#[test]
fn re_setting_edges_drops_stale_reverse_edges() {
let mut g = DepGraph::new();
g.set_edges(1, set_of(&[2]), vec![]);
g.set_edges(1, set_of(&[3]), vec![]);
assert!(g.importers_of(2).is_empty());
assert_eq!(g.importers_of(3), vec![1]);
}
#[test]
fn remove_file_cleans_up_both_directions() {
let mut g = DepGraph::new();
g.set_edges(1, set_of(&[2]), vec!["unmatched".into()]);
g.set_edges(3, set_of(&[1]), vec![]);
g.remove_file(1);
assert!(g.imports_of(1).is_empty());
assert!(g.importers_of(2).is_empty());
assert!(g.imports_of(3).is_empty());
}
#[test]
fn unresolved_imports_round_trip() {
let mut g = DepGraph::new();
g.set_edges(1, HashSet::new(), vec!["weird::path".into()]);
assert_eq!(g.unresolved_imports(1), &["weird::path".to_string()]);
g.set_edges(1, HashSet::new(), vec![]);
assert!(g.unresolved_imports(1).is_empty());
}
}