use crate::{config::ParserConfig, errors::ParserError, metrics::ParserMetrics};
use codegraph::{CodeGraph, NodeId};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileInfo {
pub file_path: PathBuf,
pub file_id: NodeId,
pub functions: Vec<NodeId>,
pub classes: Vec<NodeId>,
pub traits: Vec<NodeId>,
pub imports: Vec<NodeId>,
#[serde(with = "duration_serde")]
pub parse_time: Duration,
pub line_count: usize,
pub byte_count: usize,
}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.as_secs().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs: u64 = u64::deserialize(deserializer)?;
Ok(Duration::from_secs(secs))
}
}
impl FileInfo {
pub fn entity_count(&self) -> usize {
self.functions.len() + self.classes.len() + self.traits.len()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectInfo {
pub files: Vec<FileInfo>,
pub total_functions: usize,
pub total_classes: usize,
#[serde(with = "duration_serde")]
pub total_parse_time: Duration,
pub failed_files: Vec<(PathBuf, String)>,
}
impl ProjectInfo {
pub fn total_files(&self) -> usize {
self.files.len() + self.failed_files.len()
}
pub fn success_rate(&self) -> f64 {
if self.total_files() == 0 {
0.0
} else {
self.files.len() as f64 / self.total_files() as f64
}
}
pub fn avg_parse_time(&self) -> Duration {
if self.files.is_empty() {
Duration::ZERO
} else {
self.total_parse_time / self.files.len() as u32
}
}
}
pub trait CodeParser: Send + Sync {
fn language(&self) -> &str;
fn file_extensions(&self) -> &[&str];
fn parse_file(&self, path: &Path, graph: &mut CodeGraph) -> Result<FileInfo, ParserError>;
fn parse_source(
&self,
source: &str,
file_path: &Path,
graph: &mut CodeGraph,
) -> Result<FileInfo, ParserError>;
fn parse_files(
&self,
paths: &[PathBuf],
graph: &mut CodeGraph,
) -> Result<ProjectInfo, ParserError> {
let mut files = Vec::new();
let mut failed_files = Vec::new();
let mut total_functions = 0;
let mut total_classes = 0;
let mut total_parse_time = Duration::ZERO;
for path in paths {
match self.parse_file(path, graph) {
Ok(info) => {
total_functions += info.functions.len();
total_classes += info.classes.len();
total_parse_time += info.parse_time;
files.push(info);
}
Err(e) => {
failed_files.push((path.clone(), e.to_string()));
}
}
}
Ok(ProjectInfo {
files,
total_functions,
total_classes,
total_parse_time,
failed_files,
})
}
fn parse_directory(
&self,
dir: &Path,
graph: &mut CodeGraph,
) -> Result<ProjectInfo, ParserError> {
let paths = self.discover_files(dir)?;
self.parse_files(&paths, graph)
}
fn discover_files(&self, dir: &Path) -> Result<Vec<PathBuf>, ParserError> {
use std::fs;
let mut files = Vec::new();
let extensions = self.file_extensions();
fn walk_dir(
dir: &Path,
extensions: &[&str],
files: &mut Vec<PathBuf>,
) -> Result<(), ParserError> {
if !dir.is_dir() {
return Ok(());
}
for entry in
fs::read_dir(dir).map_err(|e| ParserError::IoError(dir.to_path_buf(), e))?
{
let entry = entry.map_err(|e| ParserError::IoError(dir.to_path_buf(), e))?;
let path = entry.path();
if path.is_dir() {
walk_dir(&path, extensions, files)?;
} else if let Some(ext) = path.extension() {
let ext_str = format!(".{}", ext.to_string_lossy());
if extensions.contains(&ext_str.as_str()) {
files.push(path);
}
}
}
Ok(())
}
walk_dir(dir, extensions, &mut files)?;
Ok(files)
}
fn can_parse(&self, path: &Path) -> bool {
if let Some(ext) = path.extension() {
let ext_str = format!(".{}", ext.to_string_lossy());
self.file_extensions().contains(&ext_str.as_str())
} else {
false
}
}
fn config(&self) -> &ParserConfig;
fn metrics(&self) -> ParserMetrics;
fn reset_metrics(&mut self);
}