use super::extractors::{
GoDependencyExtractor, PythonDependencyExtractor, RustDependencyExtractor,
TypeScriptDependencyExtractor, go::ExtractionError as GoExtractionError,
python::ExtractionError as PyExtractionError, rust::ExtractionError as RustExtractionError,
typescript::ExtractionError as TsExtractionError,
};
use super::graph::DependencyGraph;
use super::storage::{StorageBackend, StorageError};
use super::types::AnalysisDefFingerprint;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
Rust,
TypeScript,
JavaScript,
Python,
Go,
}
pub struct LanguageDetector;
impl LanguageDetector {
pub fn detect_language(path: &Path) -> Option<Language> {
path.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| match ext.to_lowercase().as_str() {
"rs" => Some(Language::Rust),
"ts" | "tsx" => Some(Language::TypeScript),
"js" | "jsx" => Some(Language::JavaScript),
"py" => Some(Language::Python),
"go" => Some(Language::Go),
_ => None,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
#[error("Unsupported language for file: {0}")]
UnsupportedLanguage(PathBuf),
#[error("IO error reading {file}: {error}")]
IoError {
file: PathBuf,
error: std::io::Error,
},
#[error("Extraction failed for {file}: {error}")]
ExtractionFailed { file: PathBuf, error: String },
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Rust extraction error: {0}")]
RustExtraction(#[from] RustExtractionError),
#[error("TypeScript extraction error: {0}")]
TypeScriptExtraction(#[from] TsExtractionError),
#[error("Python extraction error: {0}")]
PythonExtraction(#[from] PyExtractionError),
#[error("Go extraction error: {0}")]
GoExtraction(#[from] GoExtractionError),
}
pub struct DependencyGraphBuilder {
graph: DependencyGraph,
storage: Box<dyn StorageBackend>,
rust_extractor: RustDependencyExtractor,
typescript_extractor: TypeScriptDependencyExtractor,
python_extractor: PythonDependencyExtractor,
go_extractor: GoDependencyExtractor,
}
impl DependencyGraphBuilder {
pub fn new(storage: Box<dyn StorageBackend>) -> Self {
Self {
graph: DependencyGraph::new(),
storage,
rust_extractor: RustDependencyExtractor::new(),
typescript_extractor: TypeScriptDependencyExtractor::new(),
python_extractor: PythonDependencyExtractor::new(),
go_extractor: GoDependencyExtractor::new(None), }
}
pub fn graph(&self) -> &DependencyGraph {
&self.graph
}
pub async fn extract_file(&mut self, file_path: &Path) -> Result<(), BuildError> {
let language = LanguageDetector::detect_language(file_path)
.ok_or_else(|| BuildError::UnsupportedLanguage(file_path.to_path_buf()))?;
debug!(
"Extracting dependencies from {:?} ({:?})",
file_path, language
);
let content = tokio::fs::read(file_path)
.await
.map_err(|error| BuildError::IoError {
file: file_path.to_path_buf(),
error,
})?;
let source = String::from_utf8_lossy(&content);
let fingerprint = AnalysisDefFingerprint::new(&content);
self.graph
.nodes
.insert(file_path.to_path_buf(), fingerprint);
let edges = match language {
Language::Rust => self
.rust_extractor
.extract_dependency_edges(&source, file_path)?,
Language::TypeScript | Language::JavaScript => self
.typescript_extractor
.extract_dependency_edges(&source, file_path)?,
Language::Python => self
.python_extractor
.extract_dependency_edges(&source, file_path)?,
Language::Go => self
.go_extractor
.extract_dependency_edges(&source, file_path)?,
};
for edge in edges {
self.graph.add_edge(edge);
}
Ok(())
}
pub async fn extract_files(&mut self, files: &[PathBuf]) -> Result<(), BuildError> {
let mut last_error = None;
let mut success_count = 0;
for file in files {
match self.extract_file(file).await {
Ok(_) => success_count += 1,
Err(e) => {
warn!("Failed to extract {}: {}", file.display(), e);
last_error = Some(e);
}
}
}
debug!(
"Batch extraction: {}/{} files succeeded",
success_count,
files.len()
);
if let Some(err) = last_error {
if success_count == 0 {
return Err(err);
}
warn!(
"Batch extraction: {}/{} files failed",
files.len() - success_count,
files.len()
);
}
Ok(())
}
pub async fn persist(&self) -> Result<(), BuildError> {
debug!(
"Persisting graph: {} nodes, {} edges",
self.graph.node_count(),
self.graph.edge_count()
);
self.storage.save_full_graph(&self.graph).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::storage::InMemoryStorage;
#[test]
fn test_language_detection() {
assert_eq!(
LanguageDetector::detect_language(Path::new("file.rs")),
Some(Language::Rust)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.ts")),
Some(Language::TypeScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.tsx")),
Some(Language::TypeScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.js")),
Some(Language::JavaScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.jsx")),
Some(Language::JavaScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.py")),
Some(Language::Python)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.go")),
Some(Language::Go)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.java")),
None
);
assert_eq!(
LanguageDetector::detect_language(Path::new("FILE.RS")),
Some(Language::Rust)
);
}
#[test]
fn test_builder_creation() {
let storage = Box::new(InMemoryStorage::new());
let builder = DependencyGraphBuilder::new(storage);
assert_eq!(builder.graph().node_count(), 0);
assert_eq!(builder.graph().edge_count(), 0);
}
}