Skip to main content

lean_ctx/core/
graph_provider.rs

1use super::graph_index::ProjectIndex;
2#[cfg(feature = "property-graph")]
3use super::property_graph::CodeGraph;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum GraphProviderSource {
7    PropertyGraph,
8    GraphIndex,
9}
10
11pub enum GraphProvider {
12    #[cfg(feature = "property-graph")]
13    PropertyGraph(CodeGraph),
14    GraphIndex(ProjectIndex),
15}
16
17pub struct OpenGraphProvider {
18    pub source: GraphProviderSource,
19    pub provider: GraphProvider,
20}
21
22impl GraphProvider {
23    pub fn node_count(&self) -> Option<usize> {
24        match self {
25            #[cfg(feature = "property-graph")]
26            GraphProvider::PropertyGraph(g) => g.node_count().ok(),
27            GraphProvider::GraphIndex(i) => Some(i.file_count()),
28        }
29    }
30
31    pub fn edge_count(&self) -> Option<usize> {
32        match self {
33            #[cfg(feature = "property-graph")]
34            GraphProvider::PropertyGraph(g) => g.edge_count().ok(),
35            GraphProvider::GraphIndex(i) => Some(i.edge_count()),
36        }
37    }
38
39    pub fn dependencies(&self, file_path: &str) -> Vec<String> {
40        match self {
41            #[cfg(feature = "property-graph")]
42            GraphProvider::PropertyGraph(g) => g.dependencies(file_path).unwrap_or_default(),
43            GraphProvider::GraphIndex(i) => i
44                .edges
45                .iter()
46                .filter(|e| e.kind == "import" && e.from == file_path)
47                .map(|e| e.to.clone())
48                .collect(),
49        }
50    }
51
52    pub fn dependents(&self, file_path: &str) -> Vec<String> {
53        match self {
54            #[cfg(feature = "property-graph")]
55            GraphProvider::PropertyGraph(g) => g.dependents(file_path).unwrap_or_default(),
56            GraphProvider::GraphIndex(i) => i
57                .edges
58                .iter()
59                .filter(|e| e.kind == "import" && e.to == file_path)
60                .map(|e| e.from.clone())
61                .collect(),
62        }
63    }
64
65    pub fn related(&self, file_path: &str, depth: usize) -> Vec<String> {
66        match self {
67            #[cfg(feature = "property-graph")]
68            GraphProvider::PropertyGraph(g) => g
69                .impact_analysis(file_path, depth)
70                .map(|r| r.affected_files)
71                .unwrap_or_default(),
72            GraphProvider::GraphIndex(i) => i.get_related(file_path, depth),
73        }
74    }
75}
76
77pub fn open_best_effort(project_root: &str) -> Option<OpenGraphProvider> {
78    #[cfg(feature = "property-graph")]
79    {
80        let root = std::path::Path::new(project_root);
81        if let Ok(pg) = CodeGraph::open(root) {
82            if let Ok(n) = pg.node_count() {
83                if n > 0 {
84                    return Some(OpenGraphProvider {
85                        source: GraphProviderSource::PropertyGraph,
86                        provider: GraphProvider::PropertyGraph(pg),
87                    });
88                }
89            }
90        }
91    }
92
93    if let Some(idx) = super::index_orchestrator::try_load_graph_index(project_root) {
94        if !idx.files.is_empty() {
95            return Some(OpenGraphProvider {
96                source: GraphProviderSource::GraphIndex,
97                provider: GraphProvider::GraphIndex(idx),
98            });
99        }
100    }
101
102    None
103}
104
105pub fn open_or_build(project_root: &str) -> Option<OpenGraphProvider> {
106    if let Some(p) = open_best_effort(project_root) {
107        return Some(p);
108    }
109    let idx = super::graph_index::load_or_build(project_root);
110    if idx.files.is_empty() {
111        return None;
112    }
113    Some(OpenGraphProvider {
114        source: GraphProviderSource::GraphIndex,
115        provider: GraphProvider::GraphIndex(idx),
116    })
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn best_effort_prefers_graph_index_when_property_graph_empty() {
125        let _lock = crate::core::data_dir::test_env_lock();
126        let tmp = tempfile::tempdir().expect("tempdir");
127        let data = tmp.path().join("data");
128        std::fs::create_dir_all(&data).expect("mkdir data");
129        std::env::set_var("NEBU_CTX_DATA_DIR", data.to_string_lossy().to_string());
130
131        let project_root = tmp.path().join("proj");
132        std::fs::create_dir_all(&project_root).expect("mkdir proj");
133        let root = project_root.to_string_lossy().to_string();
134
135        let mut idx = ProjectIndex::new(&root);
136        idx.files.insert(
137            "src/main.rs".to_string(),
138            super::super::graph_index::FileEntry {
139                path: "src/main.rs".to_string(),
140                hash: "h".to_string(),
141                language: "rs".to_string(),
142                line_count: 1,
143                token_count: 1,
144                exports: vec![],
145                summary: "".to_string(),
146            },
147        );
148        idx.save().expect("save index");
149
150        let open = open_best_effort(&root).expect("open");
151        assert_eq!(open.source, GraphProviderSource::GraphIndex);
152
153        std::env::remove_var("NEBU_CTX_DATA_DIR");
154    }
155
156    #[test]
157    fn best_effort_none_when_no_graphs() {
158        let _lock = crate::core::data_dir::test_env_lock();
159        let tmp = tempfile::tempdir().expect("tempdir");
160        let data = tmp.path().join("data");
161        std::fs::create_dir_all(&data).expect("mkdir data");
162        std::env::set_var("NEBU_CTX_DATA_DIR", data.to_string_lossy().to_string());
163
164        let project_root = tmp.path().join("proj");
165        std::fs::create_dir_all(&project_root).expect("mkdir proj");
166        let root = project_root.to_string_lossy().to_string();
167
168        let open = open_best_effort(&root);
169        assert!(open.is_none());
170
171        std::env::remove_var("NEBU_CTX_DATA_DIR");
172    }
173}