//! Deterministic dependency graph generator for the swactor crate. //! //! Parses all `.rs` source files using `syn`, extracts type definitions, //! imports, and cross-module dependencies, then outputs `deps.dot` and //! `deps.html` files. use std::collections::HashMap; use std::fmt::Write as FmtWrite; use std::fs; use std::path::{Path, PathBuf}; // ─── Data structures ───────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] enum TypeKind { Struct, Trait, Enum, } #[derive(Debug, Clone)] struct TypeInfo { name: String, kind: TypeKind, fields: Vec<(String, String)>, // (field_name, type_description) } #[derive(Debug)] struct ModuleInfo { name: String, feature_gate: Option, types: Vec, /// local_name → (source_module, original_name) imports: HashMap, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[allow(dead_code)] enum EdgeKind { Field, TraitImpl, TraitObject, } #[derive(Debug, Clone)] struct Edge { from_module: String, from_type: String, to_module: String, to_type: String, kind: EdgeKind, label: String, } // ─── Module colors ─────────────────────────────────────────────────────────── fn module_colors(module: &str) -> (&'static str, &'static str, &'static str) { // Returns (cluster_fill, cluster_border, node_fill) match module { "error" => ("#f0f0f0", "#888", "#e8f5e9"), "config" => ("#f0f0f0", "#888", "#e8f5e9"), "channel" => ("#f0f0f0", "#888", "#fff9c4"), "actor" => ("#e3f2fd", "#1565c0", "#bbdefb"), "address_map" => ("#f3e5f5", "#7b1fa2", "#e1bee7"), "runtime" => ("#fce4ec", "#c62828", "#ffcdd2"), "worker" => ("#fff3e0", "#e65100", "#ffe0b2"), "python" => ("#f5f5f5", "#999", "#d7ccc8"), _ => ("#f0f0f0", "#888", "#e0e0e0"), } } fn module_edge_color(module: &str) -> &'static str { match module { "error" | "config" | "channel" => "#666", "actor" => "#1565c0", "address_map" => "#7b1fa2", "runtime" => "#c62828", "worker" => "#e65100", "python" => "#999", _ => "#666", } } // ─── Phase 1: Module discovery ─────────────────────────────────────────────── fn discover_modules(src_dir: &Path) -> Vec<(String, Option, PathBuf)> { let lib_path = src_dir.join("lib.rs"); let content = fs::read_to_string(&lib_path).expect("Failed to read src/lib.rs"); let syntax = syn::parse_file(&content).expect("Failed to parse src/lib.rs"); let mut modules = Vec::new(); let mut i = 0; let items: Vec<&syn::Item> = syntax.items.iter().collect(); while i < items.len() { // Check for #[cfg(feature = "...")] on the next item let feature_gate = if let syn::Item::Mod(item_mod) = items[i] { extract_feature_gate(&item_mod.attrs) } else { None }; if let syn::Item::Mod(item_mod) = items[i] { let mod_name = item_mod.ident.to_string(); let mod_path = src_dir.join(format!("{}.rs", mod_name)); if mod_path.exists() { modules.push((mod_name, feature_gate, mod_path)); } } i += 1; } modules } fn extract_feature_gate(attrs: &[syn::Attribute]) -> Option { for attr in attrs { if attr.path().is_ident("cfg") { let tokens = attr.meta.require_list().ok()?.tokens.to_string(); // Parse: feature = "python" if let Some(pos) = tokens.find("feature") { let rest = &tokens[pos..]; if let Some(start) = rest.find('"') { let rest = &rest[start + 1..]; if let Some(end) = rest.find('"') { return Some(rest[..end].to_string()); } } } } } None } // ─── Phase 2: Parse & index ────────────────────────────────────────────────── fn parse_module(name: &str, path: &Path) -> ModuleInfo { let content = fs::read_to_string(path) .unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e)); let syntax = syn::parse_file(&content) .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path.display(), e)); let mut types = Vec::new(); for item in &syntax.items { match item { syn::Item::Struct(s) => { let fields = extract_struct_fields(s); types.push(TypeInfo { name: s.ident.to_string(), kind: TypeKind::Struct, fields, }); } syn::Item::Trait(t) => { let fields = extract_trait_items(t); types.push(TypeInfo { name: t.ident.to_string(), kind: TypeKind::Trait, fields, }); } syn::Item::Enum(e) => { let fields = extract_enum_variants(e); types.push(TypeInfo { name: e.ident.to_string(), kind: TypeKind::Enum, fields, }); } _ => {} } } ModuleInfo { name: name.to_string(), feature_gate: None, // filled in later types, imports: HashMap::new(), // filled in phase 3 } } fn extract_struct_fields(s: &syn::ItemStruct) -> Vec<(String, String)> { let mut fields = Vec::new(); match &s.fields { syn::Fields::Named(named) => { for f in &named.named { if let Some(ident) = &f.ident { let ty = type_to_short_string(&f.ty); fields.push((ident.to_string(), ty)); } } } syn::Fields::Unnamed(unnamed) => { for (i, f) in unnamed.unnamed.iter().enumerate() { let ty = type_to_short_string(&f.ty); fields.push((format!("{}", i), ty)); } } syn::Fields::Unit => {} } fields } fn extract_trait_items(t: &syn::ItemTrait) -> Vec<(String, String)> { let mut items = Vec::new(); // Extract associated types for item in &t.items { if let syn::TraitItem::Type(assoc) = item { let bounds: Vec = assoc.bounds.iter().map(|b| quote_to_string(b)).collect(); items.push((assoc.ident.to_string(), bounds.join(" + "))); } } // Extract method signatures (just name + simplified sig) for item in &t.items { if let syn::TraitItem::Fn(method) = item { let sig = method_sig_short(&method.sig); items.push((method.sig.ident.to_string(), sig)); } } items } fn extract_enum_variants(e: &syn::ItemEnum) -> Vec<(String, String)> { let mut variants = Vec::new(); for v in &e.variants { let fields_desc = match &v.fields { syn::Fields::Named(named) => { let parts: Vec = named .named .iter() .filter_map(|f| { f.ident .as_ref() .map(|id| format!("{}: {}", id, type_to_short_string(&f.ty))) }) .collect(); format!("{{ {} }}", parts.join(", ")) } syn::Fields::Unnamed(unnamed) => { let parts: Vec = unnamed .unnamed .iter() .map(|f| type_to_short_string(&f.ty)) .collect(); format!("({})", parts.join(", ")) } syn::Fields::Unit => String::new(), }; variants.push((v.ident.to_string(), fields_desc)); } variants } fn method_sig_short(sig: &syn::Signature) -> String { let params: Vec = sig .inputs .iter() .filter_map(|arg| match arg { syn::FnArg::Receiver(_) => Some("&self".to_string()), syn::FnArg::Typed(pat) => Some(type_to_short_string(&pat.ty)), }) .collect(); let ret = match &sig.output { syn::ReturnType::Default => String::new(), syn::ReturnType::Type(_, ty) => format!(" → {}", type_to_short_string(ty)), }; format!("({}){}", params.join(", "), ret) } fn type_to_short_string(ty: &syn::Type) -> String { // Produce a compact but readable type representation match ty { syn::Type::Path(tp) => { let segments: Vec = tp .path .segments .iter() .map(|seg| { let name = seg.ident.to_string(); match &seg.arguments { syn::PathArguments::None => name, syn::PathArguments::AngleBracketed(args) => { let inner: Vec = args .args .iter() .map(|a| match a { syn::GenericArgument::Type(t) => type_to_short_string(t), syn::GenericArgument::Lifetime(lt) => { format!("'{}", lt.ident) } _ => quote_to_string(a), }) .collect(); format!("{}<{}>", name, inner.join(", ")) } syn::PathArguments::Parenthesized(args) => { let inner: Vec = args.inputs.iter().map(type_to_short_string).collect(); format!("{}({})", name, inner.join(", ")) } } }) .collect(); segments.join("::") } syn::Type::Reference(r) => { let lt = r .lifetime .as_ref() .map(|l| format!("&'{} ", l.ident)) .unwrap_or_else(|| "&".to_string()); let mutability = if r.mutability.is_some() { "mut " } else { "" }; format!("{}{}{}", lt, mutability, type_to_short_string(&r.elem)) } syn::Type::TraitObject(to) => { let bounds: Vec = to.bounds.iter().map(|b| quote_to_string(b)).collect(); format!("dyn {}", bounds.join(" + ")) } syn::Type::Tuple(t) => { let inner: Vec = t.elems.iter().map(type_to_short_string).collect(); format!("({})", inner.join(", ")) } syn::Type::Slice(s) => { format!("[{}]", type_to_short_string(&s.elem)) } syn::Type::Array(a) => { format!("[{}; ..]", type_to_short_string(&a.elem)) } _ => quote_to_string(ty), } } fn quote_to_string(t: &T) -> String { t.to_token_stream().to_string() } // ─── Phase 3: Import resolution ────────────────────────────────────────────── fn resolve_imports(modules: &mut [ModuleInfo], src_dir: &Path) { // Build type_name → module_name lookup from all modules let mut type_to_module: HashMap = HashMap::new(); for module in modules.iter() { for ty in &module.types { type_to_module.insert(ty.name.clone(), module.name.clone()); } } // For each module, parse its use items and resolve imports for module in modules.iter_mut() { let path = src_dir.join(format!("{}.rs", module.name)); let content = fs::read_to_string(&path).unwrap(); let syntax = syn::parse_file(&content).unwrap(); for item in &syntax.items { if let syn::Item::Use(use_item) = item { collect_use_imports(&use_item.tree, &[], &mut module.imports); } } } } fn collect_use_imports( tree: &syn::UseTree, prefix: &[String], imports: &mut HashMap, ) { match tree { syn::UseTree::Path(p) => { let mut new_prefix = prefix.to_vec(); new_prefix.push(p.ident.to_string()); collect_use_imports(&p.tree, &new_prefix, imports); } syn::UseTree::Name(n) => { let name = n.ident.to_string(); if let Some(module) = extract_crate_module(prefix) { imports.insert(name.clone(), (module, name)); } } syn::UseTree::Rename(r) => { let original = r.ident.to_string(); let alias = r.rename.to_string(); if let Some(module) = extract_crate_module(prefix) { imports.insert(alias, (module, original)); } } syn::UseTree::Glob(_) => { // `use crate::foo::*` — we skip glob imports } syn::UseTree::Group(g) => { for tree in &g.items { collect_use_imports(tree, prefix, imports); } } } } /// Given a use path prefix like ["crate", "actor"], return the module name "actor". /// Returns None for non-crate paths (std, external crates). fn extract_crate_module(prefix: &[String]) -> Option { if prefix.first().map(|s| s.as_str()) == Some("crate") { prefix.get(1).cloned() } else { None } } // ─── Phase 4: Dependency extraction ────────────────────────────────────────── fn extract_edges(modules: &[ModuleInfo], src_dir: &Path) -> Vec { let mut edges = Vec::new(); // Build type_name → module_name lookup let mut type_to_module: HashMap = HashMap::new(); for module in modules { for ty in &module.types { type_to_module.insert(ty.name.clone(), module.name.clone()); } } for module in modules { // Parse file again for impl blocks let path = src_dir.join(format!("{}.rs", module.name)); let content = fs::read_to_string(&path).unwrap(); let syntax = syn::parse_file(&content).unwrap(); // Extract edges from struct/trait/enum fields for ty in &module.types { for (_field_name, field_type) in &ty.fields { let referenced = extract_type_names_from_string(field_type); for ref_name in &referenced { if ref_name == &ty.name { continue; // skip self-references } if let Some(target_module) = resolve_type(ref_name, module, &type_to_module) { edges.push(Edge { from_module: module.name.clone(), from_type: ty.name.clone(), to_module: target_module.clone(), to_type: ref_name.clone(), kind: EdgeKind::Field, label: _field_name.clone(), }); } } } } // Extract edges from impl blocks for item in &syntax.items { if let syn::Item::Impl(impl_block) = item { let self_type = extract_base_type_name(&impl_block.self_ty); if self_type.is_none() { continue; } let self_type = self_type.unwrap(); let self_module = type_to_module.get(&self_type).cloned(); // Skip generic/blanket impls (self type is a type parameter, not a known type) if self_module.is_none() { continue; } // Trait impl: `impl Trait for Type` if let Some((_, trait_path, _)) = &impl_block.trait_ { let trait_name = path_to_name(trait_path); if trait_name == self_type { // skip self-impl (e.g. blanket impls) } else if is_std_type(&trait_name) { // skip std trait impls (Send, Sync, Clone, etc.) } else if let Some(target_module) = resolve_type(&trait_name, module, &type_to_module) { // Attribute to the module where the self type lives let from_mod = self_module.clone().unwrap_or(module.name.clone()); edges.push(Edge { from_module: from_mod, from_type: self_type.clone(), to_module: target_module, to_type: trait_name.clone(), kind: EdgeKind::TraitImpl, label: "impl".to_string(), }); } } // Only process method signatures for types belonging to this module if self_module.as_deref() != Some(&module.name) { continue; } // Method signatures — extract types from params/return types for impl_item in &impl_block.items { if let syn::ImplItem::Fn(method) = impl_item { let sig_types = extract_types_from_sig(&method.sig); for ref_name in &sig_types { if ref_name == &self_type { continue; } if let Some(target_module) = resolve_type(ref_name, module, &type_to_module) { let label = format!( "{}() param", method.sig.ident ); edges.push(Edge { from_module: module.name.clone(), from_type: self_type.clone(), to_module: target_module, to_type: ref_name.clone(), kind: EdgeKind::Field, label, }); } } } } } } // Extract edges from trait definitions (method params referencing other types) for item in &syntax.items { if let syn::Item::Trait(trait_def) = item { let trait_name = trait_def.ident.to_string(); if type_to_module.get(&trait_name) != Some(&module.name) { continue; } for trait_item in &trait_def.items { if let syn::TraitItem::Fn(method) = trait_item { let sig_types = extract_types_from_sig(&method.sig); for ref_name in &sig_types { if ref_name == &trait_name { continue; } if let Some(target_module) = resolve_type(ref_name, module, &type_to_module) { let label = format!( "{}() param", method.sig.ident ); edges.push(Edge { from_module: module.name.clone(), from_type: trait_name.clone(), to_module: target_module, to_type: ref_name.clone(), kind: EdgeKind::Field, label, }); } } } } } } } // Deduplicate edges dedup_edges(&mut edges); edges } fn dedup_edges(edges: &mut Vec) { let mut seen = std::collections::HashSet::new(); edges.retain(|e| { let key = ( e.from_module.clone(), e.from_type.clone(), e.to_module.clone(), e.to_type.clone(), e.kind.clone(), ); seen.insert(key) }); } /// Extract all type names referenced in a method signature fn extract_types_from_sig(sig: &syn::Signature) -> Vec { let mut types = Vec::new(); for arg in &sig.inputs { match arg { syn::FnArg::Typed(pat_type) => { collect_type_names(&pat_type.ty, &mut types); } _ => {} } } if let syn::ReturnType::Type(_, ty) = &sig.output { collect_type_names(ty, &mut types); } types } /// Recursively collect type names from a syn::Type fn collect_type_names(ty: &syn::Type, names: &mut Vec) { match ty { syn::Type::Path(tp) => { for seg in &tp.path.segments { let name = seg.ident.to_string(); // Skip standard library / primitive wrappers if !is_std_wrapper(&name) && !is_primitive(&name) { names.push(name.clone()); } if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { for arg in &args.args { if let syn::GenericArgument::Type(inner) = arg { collect_type_names(inner, names); } } } } } syn::Type::Reference(r) => { collect_type_names(&r.elem, names); } syn::Type::TraitObject(to) => { for bound in &to.bounds { if let syn::TypeParamBound::Trait(t) = bound { if let Some(seg) = t.path.segments.last() { let name = seg.ident.to_string(); if !is_std_type(&name) { names.push(name); } } } } } syn::Type::Tuple(t) => { for elem in &t.elems { collect_type_names(elem, names); } } syn::Type::Slice(s) => { collect_type_names(&s.elem, names); } syn::Type::Paren(p) => { collect_type_names(&p.elem, names); } _ => {} } } /// Given a short type name and a module's import map, resolve to the source module. fn resolve_type( name: &str, module: &ModuleInfo, type_to_module: &HashMap, ) -> Option { // Check import map first if let Some((src_module, _original)) = module.imports.get(name) { // Verify the type actually exists in that module if type_to_module.contains_key(name) { return Some(src_module.clone()); } // The import pointed to a module, but the type name from the import // might be the original name if type_to_module.contains_key(_original) { return Some(src_module.clone()); } } // Check if type is defined in any module type_to_module.get(name).cloned() } fn extract_base_type_name(ty: &syn::Type) -> Option { match ty { syn::Type::Path(tp) => { tp.path.segments.last().map(|s| s.ident.to_string()) } _ => None, } } fn path_to_name(path: &syn::Path) -> String { path.segments .last() .map(|s| s.ident.to_string()) .unwrap_or_default() } fn extract_type_names_from_string(type_str: &str) -> Vec { // Extract PascalCase type names from a type string let mut names = Vec::new(); let mut current = String::new(); for ch in type_str.chars() { if ch.is_alphanumeric() || ch == '_' { current.push(ch); } else { if !current.is_empty() { if is_pascal_case(¤t) && !is_std_wrapper(¤t) && !is_primitive(¤t) && !is_std_type(¤t) { names.push(current.clone()); } current.clear(); } } } if !current.is_empty() && is_pascal_case(¤t) && !is_std_wrapper(¤t) && !is_primitive(¤t) && !is_std_type(¤t) { names.push(current); } names } fn is_pascal_case(s: &str) -> bool { s.len() > 1 && s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) } fn is_std_wrapper(name: &str) -> bool { matches!( name, "Arc" | "Box" | "Option" | "Vec" | "HashMap" | "HashSet" | "RwLock" | "Mutex" | "RefCell" | "Cell" | "Rc" | "Result" | "VecDeque" | "BTreeMap" | "BTreeSet" | "AtomicBool" | "AtomicUsize" | "AtomicI64" | "JoinHandle" | "Ordering" ) } fn is_primitive(name: &str) -> bool { matches!( name, "bool" | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "f32" | "f64" | "str" | "String" | "Self" ) } fn is_std_type(name: &str) -> bool { matches!( name, "Any" | "Send" | "Sync" | "Sized" | "Clone" | "Copy" | "Debug" | "Display" | "Default" | "Hash" | "Eq" | "PartialEq" | "Ord" | "PartialOrd" | "From" | "Into" | "AsRef" | "Iterator" | "IntoIterator" | "ToString" | "Hasher" | "PyObject" | "PyResult" | "PyErr" | "PyModule" | "Python" | "Bound" | "PyAny" | "ArrayQueue" | "SegQueue" ) } // ─── Phase 5: DOT output ───────────────────────────────────────────────────── fn generate_dot(modules: &[ModuleInfo], edges: &[Edge]) -> String { let mut out = String::new(); writeln!(out, "digraph swactor {{").unwrap(); writeln!(out, " rankdir=LR;").unwrap(); writeln!(out, " fontname=\"Helvetica\";").unwrap(); writeln!(out, " fontsize=14;").unwrap(); writeln!( out, " node [fontname=\"Helvetica\", fontsize=11, style=filled, shape=record];" ) .unwrap(); writeln!(out, " edge [fontname=\"Helvetica\", fontsize=9];").unwrap(); writeln!(out, " label=\"swactor — internal dependency DAG\";").unwrap(); writeln!(out, " labelloc=t;").unwrap(); writeln!(out, " compound=true;").unwrap(); writeln!(out, " newrank=true;").unwrap(); writeln!(out, " splines=ortho;").unwrap(); writeln!(out).unwrap(); // Define module ordering for consistent output let module_order = [ "error", "config", "channel", "actor", "address_map", "runtime", "worker", "python", ]; // Emit subgraph clusters for mod_name in &module_order { if let Some(module) = modules.iter().find(|m| m.name == *mod_name) { emit_cluster(&mut out, module); } } // Emit intra-module edges (within same cluster) writeln!(out).unwrap(); writeln!( out, " // ═══════════════════════════════════════════════════════════════════" ) .unwrap(); writeln!( out, " // INTRA-MODULE EDGES (within same cluster)" ) .unwrap(); writeln!( out, " // ═══════════════════════════════════════════════════════════════════" ) .unwrap(); writeln!(out).unwrap(); for edge in edges.iter().filter(|e| e.from_module == e.to_module) { emit_edge(&mut out, edge, true); } // Emit cross-module edges writeln!(out).unwrap(); writeln!( out, " // ═══════════════════════════════════════════════════════════════════" ) .unwrap(); writeln!( out, " // CROSS-MODULE EDGES (the real dependency DAG)" ) .unwrap(); writeln!( out, " // ═══════════════════════════════════════════════════════════════════" ) .unwrap(); // Group cross-module edges by (from_module, to_module) let mut grouped: HashMap<(String, String), Vec<&Edge>> = HashMap::new(); for edge in edges.iter().filter(|e| e.from_module != e.to_module) { grouped .entry((edge.from_module.clone(), edge.to_module.clone())) .or_default() .push(edge); } // Sort groups by module order for deterministic output let mut group_keys: Vec<(String, String)> = grouped.keys().cloned().collect(); group_keys.sort_by(|a, b| { let ai = module_order .iter() .position(|m| *m == a.0) .unwrap_or(99); let bi = module_order .iter() .position(|m| *m == b.0) .unwrap_or(99); let aj = module_order .iter() .position(|m| *m == a.1) .unwrap_or(99); let bj = module_order .iter() .position(|m| *m == b.1) .unwrap_or(99); (ai, aj).cmp(&(bi, bj)) }); for key in &group_keys { let edges_group = &grouped[key]; writeln!(out).unwrap(); writeln!( out, " // --- {} depends on {} ---", key.0, key.1 ) .unwrap(); for edge in edges_group { emit_edge(&mut out, edge, false); } } writeln!(out, "}}").unwrap(); out } fn emit_cluster(out: &mut String, module: &ModuleInfo) { let (cluster_fill, cluster_border, node_fill) = module_colors(&module.name); let style = if module.feature_gate.is_some() { "rounded,dashed,filled" } else { "rounded,filled" }; let label = if module.feature_gate.is_some() { format!("{} (feature-gated)", module.name) } else { module.name.clone() }; writeln!( out, " subgraph cluster_{} {{", module.name ) .unwrap(); writeln!(out, " label=\"{}\";", label).unwrap(); writeln!( out, " style=\"{}\"; fillcolor=\"{}\"; color=\"{}\";", style, cluster_fill, cluster_border ) .unwrap(); for ty in &module.types { let prefix = match ty.kind { TypeKind::Trait => "«trait» ", TypeKind::Enum => "«enum» ", TypeKind::Struct => "", }; let fields_str = if ty.fields.is_empty() { String::new() } else { let field_lines: Vec = ty .fields .iter() .map(|(name, ty_desc)| { if ty_desc.is_empty() { escape_dot(name) } else if ty.kind == TypeKind::Trait { // For traits, show method signatures format!("{}({})", escape_dot(name), escape_dot(ty_desc)) } else if ty.kind == TypeKind::Enum { // For enum variants, show variant name and fields if ty_desc.is_empty() { escape_dot(name) } else { format!("{} {}", escape_dot(name), escape_dot(ty_desc)) } } else { format!("{}: {}", escape_dot(name), escape_dot(ty_desc)) } }) .collect(); format!("|{}", field_lines.join("\\n")) }; writeln!( out, " {} [label=\"{{{}{}{}}}\", fillcolor=\"{}\"];", ty.name, prefix, ty.name, fields_str, node_fill ) .unwrap(); } writeln!(out, " }}").unwrap(); } fn emit_edge(out: &mut String, edge: &Edge, intra: bool) { let color = if intra { "#666" } else { module_edge_color(&edge.from_module) }; let (style, penwidth) = match edge.kind { EdgeKind::TraitImpl => { if intra { ("dotted", "1") } else { ("dotted", "1.5") } } EdgeKind::TraitObject => { if intra { ("dashed", "1") } else { ("dashed", "1.5") } } EdgeKind::Field => { if intra { ("dashed", "1") } else { ("solid", "1.5") } } }; let label_escaped = escape_dot(&edge.label); writeln!( out, " {} -> {} [label=\"{}\", style={}, color=\"{}\", penwidth={}];", edge.from_type, edge.to_type, label_escaped, style, color, penwidth ) .unwrap(); } fn escape_dot(s: &str) -> String { s.replace('\\', "\\\\") .replace('"', "\\\"") .replace('<', "\\<") .replace('>', "\\>") .replace('{', "\\{") .replace('}', "\\}") .replace('|', "\\|") } // ─── Phase 6: HTML output ──────────────────────────────────────────────────── fn generate_html(dot_source: &str) -> String { // Escape the DOT source for embedding in a JS template literal let dot_escaped = dot_source .replace('\\', "\\\\") .replace('`', "\\`") .replace("${", "\\${"); format!( r##" swactor dependency DAG
swactor dep graph   scroll to zoom · drag to pan · click node to focus
Loading Graphviz…
"## ) } // ─── Main ──────────────────────────────────────────────────────────────────── fn main() { let args: Vec = std::env::args().collect(); let mut src_dir = PathBuf::from("src"); let mut output_prefix = String::from("deps"); let mut i = 1; while i < args.len() { match args[i].as_str() { "--src-dir" => { i += 1; src_dir = PathBuf::from(&args[i]); } "--output" => { i += 1; output_prefix = args[i].clone(); } "--help" | "-h" => { eprintln!("Usage: depgraph [--src-dir src/] [--output deps]"); eprintln!(" --src-dir DIR Source directory (default: src/)"); eprintln!(" --output PREFIX Output prefix (default: deps)"); eprintln!(" Produces PREFIX.dot and PREFIX.html"); std::process::exit(0); } other => { eprintln!("Unknown argument: {}", other); std::process::exit(1); } } i += 1; } eprintln!("Scanning source directory: {}", src_dir.display()); // Phase 1: Module discovery let module_defs = discover_modules(&src_dir); eprintln!( "Found {} modules: {}", module_defs.len(), module_defs .iter() .map(|(n, _, _)| n.as_str()) .collect::>() .join(", ") ); // Phase 2: Parse & index let mut modules: Vec = module_defs .iter() .map(|(name, feature, path)| { let mut m = parse_module(name, path); m.feature_gate = feature.clone(); m }) .collect(); for m in &modules { eprintln!( " {} — {} types: {}", m.name, m.types.len(), m.types .iter() .map(|t| t.name.as_str()) .collect::>() .join(", ") ); } // Phase 3: Import resolution resolve_imports(&mut modules, &src_dir); // Phase 4: Dependency extraction let edges = extract_edges(&modules, &src_dir); eprintln!("Found {} dependency edges", edges.len()); let cross_module = edges .iter() .filter(|e| e.from_module != e.to_module) .count(); let intra_module = edges.len() - cross_module; eprintln!( " {} cross-module, {} intra-module", cross_module, intra_module ); // Phase 5: DOT output let dot = generate_dot(&modules, &edges); let dot_path = format!("{}.dot", output_prefix); fs::write(&dot_path, &dot).expect("Failed to write .dot file"); eprintln!("Wrote {}", dot_path); // Phase 6: HTML output let html = generate_html(&dot); let html_path = format!("{}.html", output_prefix); fs::write(&html_path, &html).expect("Failed to write .html file"); eprintln!("Wrote {}", html_path); eprintln!("Done!"); }