Skip to main content

lean_ctx/core/
graph_provider.rs

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