Skip to main content

sdivi_graph/
dependency_graph.rs

1//! [`DependencyGraph`] construction from raw edges or `FeatureRecord` slices.
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use petgraph::graph::NodeIndex;
7use petgraph::Graph;
8use thiserror::Error;
9#[cfg(feature = "pipeline-records")]
10use tracing::debug;
11
12/// Errors that can occur during graph construction.
13#[derive(Debug, Error)]
14pub enum GraphError {
15    /// No source files were provided to the builder.
16    #[error("no source records provided")]
17    Empty,
18}
19
20/// Directed dependency graph with one node per source file.
21///
22/// Nodes carry the file path (relative to the repository root).
23/// Edges are directed from the importing file to the imported file.
24///
25/// # Examples
26///
27/// ```rust
28/// use sdivi_graph::dependency_graph::{DependencyGraph, build_dependency_graph_from_edges};
29///
30/// let dg = build_dependency_graph_from_edges(
31///     &["src/lib.rs".to_string(), "src/models.rs".to_string()],
32///     &[(0, 1)],
33/// );
34/// assert_eq!(dg.node_count(), 2);
35/// assert_eq!(dg.edge_count(), 1);
36/// ```
37#[derive(Debug, Clone)]
38pub struct DependencyGraph {
39    pub(crate) graph: Graph<PathBuf, ()>,
40    pub(crate) path_to_node: BTreeMap<PathBuf, NodeIndex>,
41}
42
43impl DependencyGraph {
44    /// Number of nodes (source files) in the graph.
45    pub fn node_count(&self) -> usize {
46        self.graph.node_count()
47    }
48
49    /// Number of directed edges in the graph.
50    pub fn edge_count(&self) -> usize {
51        self.graph.edge_count()
52    }
53
54    /// Returns the file path for node index `idx`.
55    pub fn node_path(&self, idx: usize) -> Option<&Path> {
56        let ni = NodeIndex::new(idx);
57        self.graph.node_weight(ni).map(|p| p.as_path())
58    }
59
60    /// Returns the node index for `path`, or `None` if not present.
61    pub fn node_for_path(&self, path: &Path) -> Option<usize> {
62        self.path_to_node.get(path).map(|ni| ni.index())
63    }
64
65    /// Returns all directed edges as `(from_idx, to_idx)` pairs.
66    pub fn edges_as_pairs(&self) -> Vec<(usize, usize)> {
67        self.graph
68            .raw_edges()
69            .iter()
70            .map(|e| (e.source().index(), e.target().index()))
71            .collect()
72    }
73
74    /// Returns the neighbors of node `idx` (nodes that `idx` imports from).
75    pub fn neighbors(&self, idx: usize) -> Vec<usize> {
76        let ni = NodeIndex::new(idx);
77        self.graph.neighbors(ni).map(|n| n.index()).collect()
78    }
79}
80
81/// Builds a [`DependencyGraph`] from node paths and raw `(from, to)` edge pairs.
82///
83/// Each entry in `node_paths` becomes one node; edges reference nodes by
84/// 0-based index into `node_paths`.  Out-of-range edge indices and self-loops
85/// are silently dropped.  Duplicate edges are deduplicated.
86///
87/// This constructor is always available (no `pipeline-records` feature needed).
88///
89/// # Examples
90///
91/// ```rust
92/// use sdivi_graph::dependency_graph::build_dependency_graph_from_edges;
93///
94/// let dg = build_dependency_graph_from_edges(
95///     &["src/lib.rs".to_string(), "src/models.rs".to_string()],
96///     &[(0, 1)],
97/// );
98/// assert_eq!(dg.node_count(), 2);
99/// assert_eq!(dg.edge_count(), 1);
100/// ```
101pub fn build_dependency_graph_from_edges(
102    node_paths: &[String],
103    edges: &[(usize, usize)],
104) -> DependencyGraph {
105    let mut graph: Graph<PathBuf, ()> = Graph::new();
106    let mut path_to_node: BTreeMap<PathBuf, NodeIndex> = BTreeMap::new();
107
108    for path_str in node_paths {
109        let path = PathBuf::from(path_str);
110        let ni = graph.add_node(path.clone());
111        path_to_node.insert(path, ni);
112    }
113
114    let node_count = node_paths.len();
115    for &(from, to) in edges {
116        if from >= node_count || to >= node_count || from == to {
117            continue;
118        }
119        let from_ni = NodeIndex::new(from);
120        let to_ni = NodeIndex::new(to);
121        if !graph.contains_edge(from_ni, to_ni) {
122            graph.add_edge(from_ni, to_ni, ());
123        }
124    }
125
126    DependencyGraph {
127        graph,
128        path_to_node,
129    }
130}
131
132/// Builds a [`DependencyGraph`] from a slice of `FeatureRecord`s.
133///
134/// Import strings are resolved against the set of known file paths; unresolvable
135/// imports (external packages, missing files) are dropped at `DEBUG` level.
136/// Go module-path imports are not resolved because the module prefix requires
137/// explicit supply — use [`build_dependency_graph_with_go_module`] from the
138/// pipeline layer when `go.mod` is available.
139///
140/// # Examples
141///
142/// ```rust
143/// use sdivi_graph::dependency_graph::build_dependency_graph;
144/// use sdivi_parsing::feature_record::FeatureRecord;
145/// use std::path::PathBuf;
146///
147/// let records: Vec<FeatureRecord> = vec![];
148/// let dg = build_dependency_graph(&records);
149/// assert_eq!(dg.node_count(), 0);
150/// ```
151#[cfg(feature = "pipeline-records")]
152pub fn build_dependency_graph(
153    records: &[sdivi_parsing::feature_record::FeatureRecord],
154) -> DependencyGraph {
155    build_dependency_graph_with_tsconfig(records, None, None)
156}
157
158/// Builds a [`DependencyGraph`] with an explicit Go module prefix for resolution.
159///
160/// `go_module` should be the module path from `go.mod` (e.g.
161/// `"github.com/myorg/myapp"`). Pass `None` to treat all Go imports as
162/// external (same as [`build_dependency_graph`]).
163///
164/// The pipeline layer reads `go.mod` from the repository root and calls this
165/// function to enable internal Go package edges.
166///
167/// # Examples
168///
169/// ```rust
170/// use sdivi_graph::dependency_graph::build_dependency_graph_with_go_module;
171/// use sdivi_parsing::feature_record::FeatureRecord;
172///
173/// let records: Vec<FeatureRecord> = vec![];
174/// let dg = build_dependency_graph_with_go_module(&records, Some("example.com/myapp"));
175/// assert_eq!(dg.node_count(), 0);
176/// ```
177#[cfg(feature = "pipeline-records")]
178pub fn build_dependency_graph_with_go_module(
179    records: &[sdivi_parsing::feature_record::FeatureRecord],
180    go_module: Option<&str>,
181) -> DependencyGraph {
182    build_dependency_graph_with_tsconfig(records, go_module, None)
183}
184
185/// Builds a [`DependencyGraph`] with Go module prefix and tsconfig path aliases.
186///
187/// Combines M26 Go-module resolution with M27 tsconfig path-alias resolution.
188/// `tsconfig` is parsed by the pipeline from `tsconfig.json` / `jsconfig.json`
189/// at the repository root and passed here; `None` disables alias resolution.
190///
191/// # Examples
192///
193/// ```rust
194/// use sdivi_graph::dependency_graph::build_dependency_graph_with_tsconfig;
195/// use sdivi_parsing::feature_record::FeatureRecord;
196///
197/// let records: Vec<FeatureRecord> = vec![];
198/// let dg = build_dependency_graph_with_tsconfig(&records, None, None);
199/// assert_eq!(dg.node_count(), 0);
200/// ```
201#[cfg(feature = "pipeline-records")]
202pub fn build_dependency_graph_with_tsconfig(
203    records: &[sdivi_parsing::feature_record::FeatureRecord],
204    go_module: Option<&str>,
205    tsconfig: Option<&crate::tsconfig::TsConfigPaths>,
206) -> DependencyGraph {
207    use crate::resolve::{build_stem_map, compute_java_roots, resolve_imports};
208
209    let mut graph: Graph<PathBuf, ()> = Graph::new();
210    let mut path_to_node: BTreeMap<PathBuf, NodeIndex> = BTreeMap::new();
211
212    for record in records {
213        let ni = graph.add_node(record.path.clone());
214        path_to_node.insert(record.path.clone(), ni);
215    }
216
217    let stem_map = build_stem_map(&path_to_node);
218    let java_roots = compute_java_roots(&path_to_node);
219
220    for record in records {
221        let from_ni = path_to_node[&record.path];
222        for import in &record.imports {
223            let targets = resolve_imports(
224                import,
225                &record.path,
226                &record.language,
227                &stem_map,
228                &path_to_node,
229                go_module,
230                &java_roots,
231                tsconfig,
232            );
233            if targets.is_empty() {
234                debug!(%import, path = ?record.path, "unresolved import dropped");
235            }
236            for to_ni in targets {
237                if to_ni != from_ni && !graph.contains_edge(from_ni, to_ni) {
238                    graph.add_edge(from_ni, to_ni, ());
239                }
240            }
241        }
242    }
243
244    DependencyGraph {
245        graph,
246        path_to_node,
247    }
248}