pub mod python;
mod rust;
mod typescript;
mod go;
mod java;
mod csharp;
mod c;
mod cpp;
use anyhow::Result;
use std::path::Path;
pub fn parse_file(path: &Path) -> Result<ParseResult> {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"py" | "pyi" => python::parse(path),
"ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" => typescript::parse(path),
"rs" => rust::parse(path),
"go" => go::parse(path),
"java" => java::parse(path),
"cs" => csharp::parse(path),
"kt" | "kts" => Ok(ParseResult::default()),
"c" | "h" => c::parse(path),
"cpp" | "cc" | "cxx" | "c++" | "hpp" | "hh" | "hxx" | "h++" => cpp::parse(path),
_ => Ok(ParseResult::default()),
}
}
pub fn language_for_extension(ext: &str) -> Option<&'static str> {
match ext {
"py" | "pyi" => Some("Python"),
"ts" | "tsx" => Some("TypeScript"),
"js" | "jsx" | "mjs" | "cjs" => Some("JavaScript"),
"rs" => Some("Rust"),
"go" => Some("Go"),
"java" => Some("Java"),
"cs" => Some("C#"),
"kt" | "kts" => Some("Kotlin"),
"c" | "h" => Some("C"),
"cpp" | "cc" | "cxx" | "c++" | "hpp" | "hh" | "hxx" | "h++" => Some("C++"),
_ => None,
}
}
pub fn is_supported_extension(ext: &str) -> bool {
language_for_extension(ext).is_some()
}
pub fn supported_extensions() -> &'static [&'static str] {
&[
"py", "pyi", "ts", "tsx", "js", "jsx", "mjs", "cjs", "rs", "go", "java", "cs", "kt", "kts", "c", "h", "cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx", "h++", ]
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ImportInfo {
pub path: String,
pub is_type_only: bool,
}
impl ImportInfo {
pub fn runtime(path: impl Into<String>) -> Self {
Self { path: path.into(), is_type_only: false }
}
pub fn type_only(path: impl Into<String>) -> Self {
Self { path: path.into(), is_type_only: true }
}
}
#[derive(Debug, Default, Clone)]
pub struct ParseResult {
pub functions: Vec<crate::models::Function>,
pub classes: Vec<crate::models::Class>,
pub imports: Vec<ImportInfo>,
pub calls: Vec<(String, String)>,
}
impl ParseResult {
pub fn new() -> Self {
Self::default()
}
pub fn merge(&mut self, other: ParseResult) {
self.functions.extend(other.functions);
self.classes.extend(other.classes);
self.imports.extend(other.imports);
self.calls.extend(other.calls);
}
pub fn is_empty(&self) -> bool {
self.functions.is_empty()
&& self.classes.is_empty()
&& self.imports.is_empty()
&& self.calls.is_empty()
}
pub fn entity_count(&self) -> usize {
self.functions.len() + self.classes.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_python_file() {
let path = PathBuf::from("test.py");
let _ = parse_file(&path);
}
#[test]
fn test_unknown_extension_returns_empty() {
let path = PathBuf::from("test.unknown");
let result = parse_file(&path).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_parse_result_merge() {
use crate::models::{Class, Function};
let mut result1 = ParseResult {
functions: vec![Function {
name: "func1".to_string(),
qualified_name: "test::func1:1".to_string(),
file_path: PathBuf::from("test.py"),
line_start: 1,
line_end: 5,
parameters: vec![],
return_type: None,
is_async: false,
complexity: None,
}],
classes: vec![],
imports: vec![ImportInfo::runtime("os")],
calls: vec![],
};
let result2 = ParseResult {
functions: vec![Function {
name: "func2".to_string(),
qualified_name: "test::func2:10".to_string(),
file_path: PathBuf::from("test.py"),
line_start: 10,
line_end: 15,
parameters: vec![],
return_type: None,
is_async: true,
complexity: None,
}],
classes: vec![Class {
name: "MyClass".to_string(),
qualified_name: "test::MyClass:20".to_string(),
file_path: PathBuf::from("test.py"),
line_start: 20,
line_end: 30,
methods: vec![],
bases: vec![],
}],
imports: vec![ImportInfo::runtime("sys")],
calls: vec![("test::func1:1".to_string(), "func2".to_string())],
};
result1.merge(result2);
assert_eq!(result1.functions.len(), 2);
assert_eq!(result1.classes.len(), 1);
assert_eq!(result1.imports.len(), 2);
assert_eq!(result1.calls.len(), 1);
assert_eq!(result1.entity_count(), 3);
}
#[test]
fn test_language_for_extension() {
assert_eq!(language_for_extension("py"), Some("Python"));
assert_eq!(language_for_extension("ts"), Some("TypeScript"));
assert_eq!(language_for_extension("rs"), Some("Rust"));
assert_eq!(language_for_extension("go"), Some("Go"));
assert_eq!(language_for_extension("java"), Some("Java"));
assert_eq!(language_for_extension("cs"), Some("C#"));
assert_eq!(language_for_extension("kt"), Some("Kotlin"));
assert_eq!(language_for_extension("c"), Some("C"));
assert_eq!(language_for_extension("cpp"), Some("C++"));
assert_eq!(language_for_extension("unknown"), None);
}
#[test]
fn test_supported_extensions() {
let exts = supported_extensions();
assert!(exts.contains(&"py"));
assert!(exts.contains(&"ts"));
assert!(exts.contains(&"rs"));
assert!(exts.contains(&"go"));
assert!(exts.contains(&"java"));
assert!(exts.contains(&"cs"));
assert!(exts.contains(&"kt"));
assert!(exts.contains(&"c"));
assert!(exts.contains(&"cpp"));
}
}