1use crate::project::Project;
10use ryo_analysis::{
11 BorrowKind, DataFlowBuilderWorkspace, DataFlowGraphV2, ImHashMap, LockGranularityAnalyzerV2,
12 LockStatsV2, LockSuggestion, SymbolPath, SymbolRegistry, VarId, WorkspaceFilePath,
13};
14use std::path::Path;
15use std::sync::Arc;
16
17pub struct DataFlowServiceV2 {
23 registry: SymbolRegistry,
24 graph: DataFlowGraphV2,
25}
26
27impl DataFlowServiceV2 {
28 pub fn from_project(project: &Project) -> Self {
30 let resolver = project.path_resolver();
32 let im_files: ImHashMap<WorkspaceFilePath, Arc<ryo_source::PureFile>> = project
33 .files()
34 .iter()
35 .filter_map(|(path, file)| {
36 let wfp = resolver.resolve(path).ok()?;
37 Some((wfp, Arc::new(file.clone())))
38 })
39 .collect();
40
41 let crate_name = project
43 .metadata()
44 .members()
45 .next()
46 .map(|m| m.module_name.clone())
47 .unwrap_or_else(|| "unknown".to_string());
48
49 let registry = SymbolRegistry::new();
50 let graph = DataFlowBuilderWorkspace::new(®istry, &im_files, &crate_name).build();
51 Self { registry, graph }
52 }
53
54 pub fn from_path(path: &Path) -> Result<Self, DataFlowErrorV2> {
56 let project = Project::load(path).map_err(|e| DataFlowErrorV2::Project(e.to_string()))?;
57 Ok(Self::from_project(&project))
58 }
59
60 pub fn stats(&self) -> DataFlowStatsV2 {
62 DataFlowStatsV2 {
63 var_count: self.graph.var_count(),
64 flow_count: self.graph.flow_count(),
65 }
66 }
67
68 pub fn graph(&self) -> &DataFlowGraphV2 {
70 &self.graph
71 }
72
73 pub fn registry(&self) -> &SymbolRegistry {
75 &self.registry
76 }
77
78 pub fn vars_in_function(&self, path: &str) -> Vec<VarInfoV2> {
89 let symbol_path = match SymbolPath::parse(path) {
90 Ok(p) => p,
91 Err(_) => return vec![],
92 };
93
94 let Some(symbol_id) = self.registry.lookup(&symbol_path) else {
95 return vec![];
96 };
97
98 self.graph
99 .vars_in_symbol(symbol_id)
100 .iter()
101 .filter_map(|&var_id| self.var_info(var_id))
102 .collect()
103 }
104
105 pub fn var_info(&self, var_id: VarId) -> Option<VarInfoV2> {
107 let data = self.graph.var(var_id)?;
108 let symbol_id = self.graph.var_to_symbol(var_id)?;
109 let path = self.registry.resolve(symbol_id)?;
110 Some(VarInfoV2 {
111 id: var_id,
112 path: path.to_string(),
113 name: path.name().to_string(),
114 kind: format!("{:?}", data.kind),
115 line: data.line,
116 })
117 }
118
119 pub fn impact(&self, var_id: VarId) -> Vec<VarInfoV2> {
121 self.graph
122 .impact(var_id)
123 .into_iter()
124 .filter_map(|id| self.var_info(id))
125 .collect()
126 }
127
128 pub fn provenance(&self, var_id: VarId) -> Vec<VarInfoV2> {
130 self.graph
131 .provenance(var_id)
132 .into_iter()
133 .filter_map(|id| self.var_info(id))
134 .collect()
135 }
136
137 pub fn find_var(&self, path: &str) -> Option<VarInfoV2> {
144 let symbol_path = SymbolPath::parse(path).ok()?;
145 let symbol_id = self.registry.lookup(&symbol_path)?;
146 let var_id = self.graph.symbol_to_var(symbol_id)?;
147 self.var_info(var_id)
148 }
149
150 pub fn all_vars(&self) -> Vec<VarInfoV2> {
152 self.graph
153 .iter_vars()
154 .filter_map(|(var_id, _)| self.var_info(var_id))
155 .collect()
156 }
157
158 pub fn all_flows(&self) -> Vec<FlowInfoV2> {
160 self.graph
161 .iter_flows()
162 .filter_map(|(_flow_id, data, edge)| {
163 let from = self.var_info(edge.from)?;
164 let to = self.var_info(edge.to)?;
165 Some(FlowInfoV2 {
166 from,
167 to,
168 kind: format!("{:?}", data.kind),
169 line: data.line,
170 })
171 })
172 .collect()
173 }
174
175 pub fn find_by_name(&self, name: &str) -> Vec<VarInfoV2> {
183 self.graph
184 .iter_vars()
185 .filter_map(|(var_id, _data)| {
186 let symbol_id = self.graph.var_to_symbol(var_id)?;
187 let path = self.registry.resolve(symbol_id)?;
188 if path.name() == name || path.to_string().contains(name) {
189 self.var_info(var_id)
190 } else {
191 None
192 }
193 })
194 .collect()
195 }
196
197 pub fn find_sources(&self) -> Vec<VarInfoV2> {
199 self.graph
200 .iter_vars()
201 .filter_map(|(var_id, _)| {
202 if self.graph.incoming(var_id).is_empty() {
203 self.var_info(var_id)
204 } else {
205 None
206 }
207 })
208 .collect()
209 }
210
211 pub fn find_sinks(&self) -> Vec<VarInfoV2> {
213 self.graph
214 .iter_vars()
215 .filter_map(|(var_id, _)| {
216 if self.graph.outgoing(var_id).is_empty() {
217 self.var_info(var_id)
218 } else {
219 None
220 }
221 })
222 .collect()
223 }
224
225 pub fn impact_by_name(&self, name: &str) -> Vec<VarInfoV2> {
227 self.find_by_name(name)
228 .into_iter()
229 .flat_map(|var| self.impact(var.id))
230 .collect()
231 }
232
233 pub fn provenance_by_name(&self, name: &str) -> Vec<VarInfoV2> {
235 self.find_by_name(name)
236 .into_iter()
237 .flat_map(|var| self.provenance(var.id))
238 .collect()
239 }
240
241 pub fn borrow_check(&self, name: &str, at_line: u32) -> BorrowCheckResultV2 {
257 let vars = self.find_by_name(name);
258 if vars.is_empty() {
259 return BorrowCheckResultV2 {
260 variable: name.to_string(),
261 line: at_line,
262 conflicts: vec![],
263 };
264 }
265
266 let tracker = self.graph.borrow_tracker();
267 let mut all_conflicts = Vec::new();
268
269 for var in &vars {
270 let conflicts = tracker.conflicts(var.id, BorrowKind::Mutable, at_line);
271 for conflict in &conflicts {
272 all_conflicts.push(format!("`{}`: {}", name, conflict));
273 }
274 }
275
276 BorrowCheckResultV2 {
277 variable: name.to_string(),
278 line: at_line,
279 conflicts: all_conflicts,
280 }
281 }
282
283 pub fn lock_analysis(&self) -> LockAnalysisResultV2 {
291 let analyzer = LockGranularityAnalyzerV2::new(self.graph.lock_tracker());
292 let suggestions = analyzer.analyze();
293 let stats = analyzer.stats();
294
295 LockAnalysisResultV2 { stats, suggestions }
296 }
297}
298
299#[derive(Debug, thiserror::Error)]
305pub enum DataFlowErrorV2 {
306 #[error("Project error: {0}")]
307 Project(String),
308
309 #[error("Analysis error: {0}")]
310 Analysis(String),
311
312 #[error("Symbol not found: {0}")]
313 SymbolNotFound(String),
314}
315
316#[derive(Debug, Clone)]
318pub struct DataFlowStatsV2 {
319 pub var_count: usize,
320 pub flow_count: usize,
321}
322
323#[derive(Debug, Clone)]
325pub struct VarInfoV2 {
326 pub id: VarId,
328 pub path: String,
330 pub name: String,
332 pub kind: String,
334 pub line: u32,
336}
337
338#[derive(Debug, Clone)]
340pub struct FlowInfoV2 {
341 pub from: VarInfoV2,
343 pub to: VarInfoV2,
345 pub kind: String,
347 pub line: u32,
349}
350
351#[derive(Debug, Clone)]
353pub struct BorrowCheckResultV2 {
354 pub variable: String,
356 pub line: u32,
358 pub conflicts: Vec<String>,
360}
361
362impl BorrowCheckResultV2 {
363 pub fn is_ok(&self) -> bool {
365 self.conflicts.is_empty()
366 }
367
368 pub fn has_conflicts(&self) -> bool {
370 !self.conflicts.is_empty()
371 }
372}
373
374#[derive(Debug, Clone)]
376pub struct LockAnalysisResultV2 {
377 pub stats: LockStatsV2,
379 pub suggestions: Vec<LockSuggestion>,
381}
382
383impl LockAnalysisResultV2 {
384 pub fn has_suggestions(&self) -> bool {
386 !self.suggestions.is_empty()
387 }
388}
389
390#[cfg(test)]
395mod tests {
396 use super::*;
397 use std::fs;
398 use tempfile::tempdir;
399
400 fn create_test_project() -> tempfile::TempDir {
401 let dir = tempdir().unwrap();
402 let src = dir.path().join("src");
403 fs::create_dir(&src).unwrap();
404
405 fs::write(
406 dir.path().join("Cargo.toml"),
407 r#"[package]
408name = "test-project"
409version = "0.1.0"
410edition = "2021"
411"#,
412 )
413 .unwrap();
414
415 fs::write(
416 src.join("lib.rs"),
417 r#"
418pub fn process(input: i32) {
419 let x = input;
420 let y = x + 1;
421 let z = y * 2;
422}
423
424impl Config {
425 pub fn update(&mut self, value: i32) {
426 self.field = value;
427 }
428}
429"#,
430 )
431 .unwrap();
432
433 dir
434 }
435
436 #[test]
437 fn test_from_path() {
438 let dir = create_test_project();
439 let result = DataFlowServiceV2::from_path(dir.path());
440 assert!(result.is_ok());
441 }
442
443 #[test]
444 fn test_stats() {
445 let dir = create_test_project();
446 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
447 let stats = service.stats();
448 let _ = (stats.var_count, stats.flow_count);
451 }
452
453 #[test]
454 fn test_all_vars() {
455 let dir = create_test_project();
456 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
457 let vars = service.all_vars();
458 let _ = vars;
461 }
462
463 #[test]
464 fn test_find_by_name() {
465 let dir = create_test_project();
466 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
467
468 let vars = service.find_by_name("input");
469 let _ = vars;
472 }
473
474 #[test]
475 fn test_find_sources_sinks() {
476 let dir = create_test_project();
477 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
478
479 let sources = service.find_sources();
480 let sinks = service.find_sinks();
481
482 let _ = (sources, sinks);
485 }
486
487 #[test]
488 fn test_impact_provenance() {
489 let dir = create_test_project();
490 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
491
492 let vars = service.find_by_name("input");
493 if let Some(var) = vars.first() {
494 let impact = service.impact(var.id);
495 assert!(!impact.is_empty() || service.stats().flow_count == 0);
497 }
498 }
499
500 #[test]
501 fn test_all_flows() {
502 let dir = create_test_project();
503 let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
504
505 let flows = service.all_flows();
506 let _ = flows;
509 }
510}