use crate::project::Project;
use ryo_analysis::{
BorrowKind, DataFlowBuilderWorkspace, DataFlowGraphV2, ImHashMap, LockGranularityAnalyzerV2,
LockStatsV2, LockSuggestion, SymbolPath, SymbolRegistry, VarId, WorkspaceFilePath,
};
use std::path::Path;
use std::sync::Arc;
pub struct DataFlowServiceV2 {
registry: SymbolRegistry,
graph: DataFlowGraphV2,
}
impl DataFlowServiceV2 {
pub fn from_project(project: &Project) -> Self {
let resolver = project.path_resolver();
let im_files: ImHashMap<WorkspaceFilePath, Arc<ryo_source::PureFile>> = project
.files()
.iter()
.filter_map(|(path, file)| {
let wfp = resolver.resolve(path).ok()?;
Some((wfp, Arc::new(file.clone())))
})
.collect();
let crate_name = project
.metadata()
.members()
.next()
.map(|m| m.module_name.clone())
.unwrap_or_else(|| "unknown".to_string());
let registry = SymbolRegistry::new();
let graph = DataFlowBuilderWorkspace::new(®istry, &im_files, &crate_name).build();
Self { registry, graph }
}
pub fn from_path(path: &Path) -> Result<Self, DataFlowErrorV2> {
let project = Project::load(path).map_err(|e| DataFlowErrorV2::Project(e.to_string()))?;
Ok(Self::from_project(&project))
}
pub fn stats(&self) -> DataFlowStatsV2 {
DataFlowStatsV2 {
var_count: self.graph.var_count(),
flow_count: self.graph.flow_count(),
}
}
pub fn graph(&self) -> &DataFlowGraphV2 {
&self.graph
}
pub fn registry(&self) -> &SymbolRegistry {
&self.registry
}
pub fn vars_in_function(&self, path: &str) -> Vec<VarInfoV2> {
let symbol_path = match SymbolPath::parse(path) {
Ok(p) => p,
Err(_) => return vec![],
};
let Some(symbol_id) = self.registry.lookup(&symbol_path) else {
return vec![];
};
self.graph
.vars_in_symbol(symbol_id)
.iter()
.filter_map(|&var_id| self.var_info(var_id))
.collect()
}
pub fn var_info(&self, var_id: VarId) -> Option<VarInfoV2> {
let data = self.graph.var(var_id)?;
let symbol_id = self.graph.var_to_symbol(var_id)?;
let path = self.registry.resolve(symbol_id)?;
Some(VarInfoV2 {
id: var_id,
path: path.to_string(),
name: path.name().to_string(),
kind: format!("{:?}", data.kind),
line: data.line,
})
}
pub fn impact(&self, var_id: VarId) -> Vec<VarInfoV2> {
self.graph
.impact(var_id)
.into_iter()
.filter_map(|id| self.var_info(id))
.collect()
}
pub fn provenance(&self, var_id: VarId) -> Vec<VarInfoV2> {
self.graph
.provenance(var_id)
.into_iter()
.filter_map(|id| self.var_info(id))
.collect()
}
pub fn find_var(&self, path: &str) -> Option<VarInfoV2> {
let symbol_path = SymbolPath::parse(path).ok()?;
let symbol_id = self.registry.lookup(&symbol_path)?;
let var_id = self.graph.symbol_to_var(symbol_id)?;
self.var_info(var_id)
}
pub fn all_vars(&self) -> Vec<VarInfoV2> {
self.graph
.iter_vars()
.filter_map(|(var_id, _)| self.var_info(var_id))
.collect()
}
pub fn all_flows(&self) -> Vec<FlowInfoV2> {
self.graph
.iter_flows()
.filter_map(|(_flow_id, data, edge)| {
let from = self.var_info(edge.from)?;
let to = self.var_info(edge.to)?;
Some(FlowInfoV2 {
from,
to,
kind: format!("{:?}", data.kind),
line: data.line,
})
})
.collect()
}
pub fn find_by_name(&self, name: &str) -> Vec<VarInfoV2> {
self.graph
.iter_vars()
.filter_map(|(var_id, _data)| {
let symbol_id = self.graph.var_to_symbol(var_id)?;
let path = self.registry.resolve(symbol_id)?;
if path.name() == name || path.to_string().contains(name) {
self.var_info(var_id)
} else {
None
}
})
.collect()
}
pub fn find_sources(&self) -> Vec<VarInfoV2> {
self.graph
.iter_vars()
.filter_map(|(var_id, _)| {
if self.graph.incoming(var_id).is_empty() {
self.var_info(var_id)
} else {
None
}
})
.collect()
}
pub fn find_sinks(&self) -> Vec<VarInfoV2> {
self.graph
.iter_vars()
.filter_map(|(var_id, _)| {
if self.graph.outgoing(var_id).is_empty() {
self.var_info(var_id)
} else {
None
}
})
.collect()
}
pub fn impact_by_name(&self, name: &str) -> Vec<VarInfoV2> {
self.find_by_name(name)
.into_iter()
.flat_map(|var| self.impact(var.id))
.collect()
}
pub fn provenance_by_name(&self, name: &str) -> Vec<VarInfoV2> {
self.find_by_name(name)
.into_iter()
.flat_map(|var| self.provenance(var.id))
.collect()
}
pub fn borrow_check(&self, name: &str, at_line: u32) -> BorrowCheckResultV2 {
let vars = self.find_by_name(name);
if vars.is_empty() {
return BorrowCheckResultV2 {
variable: name.to_string(),
line: at_line,
conflicts: vec![],
};
}
let tracker = self.graph.borrow_tracker();
let mut all_conflicts = Vec::new();
for var in &vars {
let conflicts = tracker.conflicts(var.id, BorrowKind::Mutable, at_line);
for conflict in &conflicts {
all_conflicts.push(format!("`{}`: {}", name, conflict));
}
}
BorrowCheckResultV2 {
variable: name.to_string(),
line: at_line,
conflicts: all_conflicts,
}
}
pub fn lock_analysis(&self) -> LockAnalysisResultV2 {
let analyzer = LockGranularityAnalyzerV2::new(self.graph.lock_tracker());
let suggestions = analyzer.analyze();
let stats = analyzer.stats();
LockAnalysisResultV2 { stats, suggestions }
}
}
#[derive(Debug, thiserror::Error)]
pub enum DataFlowErrorV2 {
#[error("Project error: {0}")]
Project(String),
#[error("Analysis error: {0}")]
Analysis(String),
#[error("Symbol not found: {0}")]
SymbolNotFound(String),
}
#[derive(Debug, Clone)]
pub struct DataFlowStatsV2 {
pub var_count: usize,
pub flow_count: usize,
}
#[derive(Debug, Clone)]
pub struct VarInfoV2 {
pub id: VarId,
pub path: String,
pub name: String,
pub kind: String,
pub line: u32,
}
#[derive(Debug, Clone)]
pub struct FlowInfoV2 {
pub from: VarInfoV2,
pub to: VarInfoV2,
pub kind: String,
pub line: u32,
}
#[derive(Debug, Clone)]
pub struct BorrowCheckResultV2 {
pub variable: String,
pub line: u32,
pub conflicts: Vec<String>,
}
impl BorrowCheckResultV2 {
pub fn is_ok(&self) -> bool {
self.conflicts.is_empty()
}
pub fn has_conflicts(&self) -> bool {
!self.conflicts.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct LockAnalysisResultV2 {
pub stats: LockStatsV2,
pub suggestions: Vec<LockSuggestion>,
}
impl LockAnalysisResultV2 {
pub fn has_suggestions(&self) -> bool {
!self.suggestions.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn create_test_project() -> tempfile::TempDir {
let dir = tempdir().unwrap();
let src = dir.path().join("src");
fs::create_dir(&src).unwrap();
fs::write(
dir.path().join("Cargo.toml"),
r#"[package]
name = "test-project"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
fs::write(
src.join("lib.rs"),
r#"
pub fn process(input: i32) {
let x = input;
let y = x + 1;
let z = y * 2;
}
impl Config {
pub fn update(&mut self, value: i32) {
self.field = value;
}
}
"#,
)
.unwrap();
dir
}
#[test]
fn test_from_path() {
let dir = create_test_project();
let result = DataFlowServiceV2::from_path(dir.path());
assert!(result.is_ok());
}
#[test]
fn test_stats() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let stats = service.stats();
let _ = (stats.var_count, stats.flow_count);
}
#[test]
fn test_all_vars() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let vars = service.all_vars();
let _ = vars;
}
#[test]
fn test_find_by_name() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let vars = service.find_by_name("input");
let _ = vars;
}
#[test]
fn test_find_sources_sinks() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let sources = service.find_sources();
let sinks = service.find_sinks();
let _ = (sources, sinks);
}
#[test]
fn test_impact_provenance() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let vars = service.find_by_name("input");
if let Some(var) = vars.first() {
let impact = service.impact(var.id);
assert!(!impact.is_empty() || service.stats().flow_count == 0);
}
}
#[test]
fn test_all_flows() {
let dir = create_test_project();
let service = DataFlowServiceV2::from_path(dir.path()).unwrap();
let flows = service.all_flows();
let _ = flows;
}
}