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