use super::types::{CodeNode, GraphError, GraphStats};
use crate::Project;
use ryo_analysis::{AnalysisContext, SymbolId, SymbolKind, Visibility};
use std::path::Path;
use std::time::{Duration, Instant};
pub struct GraphApi {
ctx: AnalysisContext,
build_time: Duration,
}
impl GraphApi {
pub fn from_path(path: &Path) -> Result<Self, GraphError> {
let start = Instant::now();
let project = Project::load(path).map_err(|e| GraphError::LoadFailed(e.to_string()))?;
Self::from_project(&project).map(|mut api| {
api.build_time = start.elapsed();
api
})
}
pub fn from_project(project: &Project) -> Result<Self, GraphError> {
let start = Instant::now();
let ctx = AnalysisContext::from_workspace_root_parallel(project.workspace_root())
.map_err(|e| GraphError::LoadFailed(format!("Failed to create context: {}", e)))?;
Ok(Self {
ctx,
build_time: start.elapsed(),
})
}
pub fn from_context(ctx: AnalysisContext, build_time: Duration) -> Self {
Self { ctx, build_time }
}
pub fn build_time(&self) -> Duration {
self.build_time
}
pub fn stats(&self) -> GraphStats {
let registry = &self.ctx.registry;
let mut functions = 0;
let mut structs = 0;
let mut enums = 0;
let mut traits = 0;
let mut impls = 0;
for (id, _) in registry.iter() {
if let Some(kind) = registry.kind(id) {
match kind {
SymbolKind::Function => functions += 1,
SymbolKind::Struct => structs += 1,
SymbolKind::Enum => enums += 1,
SymbolKind::Trait => traits += 1,
SymbolKind::Impl => impls += 1,
_ => {}
}
}
}
GraphStats {
files: self.ctx.files.len(),
nodes: self.ctx.code_graph.node_count(),
functions,
structs,
enums,
traits,
impls,
edges: self.ctx.code_graph.edge_count(),
}
}
pub fn context(&self) -> &AnalysisContext {
&self.ctx
}
pub(crate) fn query_nodes(&self, kind: SymbolKind) -> Vec<CodeNode> {
self.ctx
.registry
.iter()
.filter(|(id, _)| self.ctx.registry.kind(*id) == Some(kind))
.filter_map(|(id, _)| self.symbol_to_node(id))
.collect()
}
fn symbol_to_node(&self, id: SymbolId) -> Option<CodeNode> {
let path = self.ctx.registry.resolve(id)?;
let kind = self.ctx.registry.kind(id)?;
let file = self
.ctx
.registry
.span(id)
.map(|s| s.file.to_absolute())
.unwrap_or_default();
let is_public = self
.ctx
.registry
.visibility(id)
.map(|v| *v == Visibility::Public)
.unwrap_or(false);
let is_async = self
.ctx
.detail_store
.function(id)
.map(|f| f.is_async)
.unwrap_or(false);
Some(CodeNode {
name: path.name().to_string(),
file,
kind: kind.into(),
is_public,
is_async,
symbol_id: id,
})
}
}
#[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 greet(name: &str) -> String {
format!("Hello, {}!", name)
}
pub struct User {
pub name: String,
}
pub enum Status {
Active,
Inactive,
}
pub trait Greetable {
fn greet(&self) -> String;
}
"#,
)
.unwrap();
dir
}
#[test]
fn test_from_path() {
let dir = create_test_project();
let result = GraphApi::from_path(dir.path());
assert!(result.is_ok());
}
#[test]
fn test_stats() {
let dir = create_test_project();
let api = GraphApi::from_path(dir.path()).unwrap();
let stats = api.stats();
assert_eq!(stats.files, 1);
assert!(stats.nodes > 0);
}
#[test]
fn test_build_time() {
let dir = create_test_project();
let api = GraphApi::from_path(dir.path()).unwrap();
assert!(api.build_time().as_nanos() > 0);
}
}