use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::core::JSON_SCHEMA_DEPS_INDEX;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImportKind {
Use,
Mod,
From,
Bare,
NamedFrom,
StarFrom,
Static,
Alias,
Glob,
}
impl ImportKind {
pub fn label(self) -> &'static str {
match self {
Self::Use => "use",
Self::Mod => "mod",
Self::From => "from",
Self::Bare => "import",
Self::NamedFrom => "named",
Self::StarFrom => "star",
Self::Static => "static",
Self::Alias => "alias",
Self::Glob => "glob",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepEdge {
pub target: PathBuf,
pub kind: ImportKind,
pub line: u32,
#[serde(default)]
pub local_name: Option<String>,
#[serde(default)]
pub raw_path: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GraphStats {
pub file_count: usize,
pub edge_count: usize,
pub external_count: usize,
pub build_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepGraph {
pub schema: String,
pub forward: HashMap<PathBuf, Vec<DepEdge>>,
pub external: HashMap<PathBuf, Vec<String>>,
pub root: PathBuf,
pub built_at: SystemTime,
pub stats: GraphStats,
}
impl DepGraph {
pub fn empty(root: PathBuf) -> Self {
Self {
schema: JSON_SCHEMA_DEPS_INDEX.to_string(),
forward: HashMap::new(),
external: HashMap::new(),
root,
built_at: SystemTime::now(),
stats: GraphStats::default(),
}
}
pub fn reverse_adjacency(&self) -> HashMap<PathBuf, Vec<PathBuf>> {
let mut rev: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
for (src, edges) in &self.forward {
for edge in edges {
rev.entry(edge.target.clone())
.or_default()
.push(src.clone());
}
}
for v in rev.values_mut() {
v.sort();
v.dedup();
}
rev
}
pub fn sorted_edges(&self) -> Vec<(PathBuf, PathBuf, ImportKind)> {
let mut all = Vec::new();
for (src, edges) in &self.forward {
for e in edges {
all.push((src.clone(), e.target.clone(), e.kind));
}
}
all.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
all
}
#[allow(dead_code)]
pub fn grouped(&self) -> BTreeMap<PathBuf, Vec<DepEdge>> {
let mut out = BTreeMap::new();
for (k, v) in &self.forward {
let mut sorted = v.clone();
sorted.sort_by(|a, b| a.target.cmp(&b.target));
out.insert(k.clone(), sorted);
}
out
}
pub fn rel(&self, p: &Path) -> String {
match p.strip_prefix(&self.root) {
Ok(r) => r
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/"),
Err(_) => p.display().to_string(),
}
}
pub fn in_scope(&self, p: &Path, scope: &str) -> bool {
if scope.is_empty() {
return true;
}
let rel = self.rel(p);
if rel == scope {
return true;
}
if rel.len() <= scope.len() {
return false;
}
rel.starts_with(scope) && rel.as_bytes()[scope.len()] == b'/'
}
pub fn subgraph_for_scope(&self, scope: &str) -> DepGraph {
if scope.is_empty() {
return self.clone();
}
let mut forward: HashMap<PathBuf, Vec<DepEdge>> = HashMap::new();
for (src, edges) in &self.forward {
if !self.in_scope(src, scope) {
continue;
}
let kept: Vec<DepEdge> = edges
.iter()
.filter(|e| self.in_scope(&e.target, scope))
.cloned()
.collect();
forward.insert(src.clone(), kept);
}
let external: HashMap<PathBuf, Vec<String>> = self
.external
.iter()
.filter(|(p, _)| self.in_scope(p, scope))
.map(|(p, v)| (p.clone(), v.clone()))
.collect();
let edge_count: usize = forward.values().map(|v| v.len()).sum();
let external_count: usize = external.values().map(|v| v.len()).sum();
DepGraph {
schema: self.schema.clone(),
forward,
external,
root: self.root.clone(),
built_at: self.built_at,
stats: GraphStats {
file_count: self.stats.file_count,
edge_count,
external_count,
build_ms: self.stats.build_ms,
},
}
}
}
pub fn dedup_edges(graph: &mut DepGraph) {
for edges in graph.forward.values_mut() {
let mut seen = std::collections::HashSet::new();
edges.retain(|e| seen.insert(e.target.clone()));
}
}