use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct DetectedStack {
pub language: Language,
pub framework: Option<String>,
pub test_runner: Option<String>,
pub type_checker: Option<String>,
pub linter: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Language {
Rust,
TypeScript,
JavaScript,
Python,
Go,
#[default]
Unknown,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Language::Rust => write!(f, "Rust"),
Language::TypeScript => write!(f, "TypeScript"),
Language::JavaScript => write!(f, "JavaScript"),
Language::Python => write!(f, "Python"),
Language::Go => write!(f, "Go"),
Language::Unknown => write!(f, "Unknown"),
}
}
}
impl DetectedStack {
pub fn to_summary(&self) -> String {
let mut parts = vec![format!("Language: {}", self.language)];
if let Some(ref fw) = self.framework {
parts.push(format!("Framework: {}", fw));
}
if let Some(ref tr) = self.test_runner {
parts.push(format!("Test Runner: {}", tr));
}
if let Some(ref tc) = self.type_checker {
parts.push(format!("Type Checker: {}", tc));
}
if let Some(ref l) = self.linter {
parts.push(format!("Linter: {}", l));
}
parts.join("\n")
}
}
pub fn detect_stack(project_dir: &Path) -> DetectedStack {
if project_dir.join("Cargo.toml").exists() {
return detect_rust_stack(project_dir);
}
if project_dir.join("package.json").exists() {
return detect_node_stack(project_dir);
}
if project_dir.join("pyproject.toml").exists() || project_dir.join("setup.py").exists() {
return detect_python_stack(project_dir);
}
if project_dir.join("go.mod").exists() {
return detect_go_stack(project_dir);
}
DetectedStack::default()
}
fn detect_rust_stack(_dir: &Path) -> DetectedStack {
DetectedStack {
language: Language::Rust,
framework: None, test_runner: Some("cargo test".to_string()),
type_checker: Some("rustc".to_string()),
linter: Some("clippy".to_string()),
}
}
fn detect_node_stack(dir: &Path) -> DetectedStack {
let pkg_path = dir.join("package.json");
let mut stack = DetectedStack {
language: Language::JavaScript,
framework: None,
test_runner: None,
type_checker: None,
linter: None,
};
if let Ok(content) = std::fs::read_to_string(&pkg_path) {
if let Ok(pkg) = serde_json::from_str::<serde_json::Value>(&content) {
if has_dep(&pkg, "devDependencies", "typescript") {
stack.language = Language::TypeScript;
stack.type_checker = Some("tsc".to_string());
}
if has_dep(&pkg, "devDependencies", "vitest") {
stack.test_runner = Some("vitest".to_string());
} else if has_dep(&pkg, "devDependencies", "jest") {
stack.test_runner = Some("jest".to_string());
} else if has_dep(&pkg, "devDependencies", "mocha") {
stack.test_runner = Some("mocha".to_string());
}
if has_dep(&pkg, "devDependencies", "eslint") {
stack.linter = Some("eslint".to_string());
} else if has_dep(&pkg, "devDependencies", "biome") {
stack.linter = Some("biome".to_string());
}
if has_dep(&pkg, "dependencies", "next") {
stack.framework = Some("Next.js".to_string());
} else if has_dep(&pkg, "dependencies", "react") {
stack.framework = Some("React".to_string());
} else if has_dep(&pkg, "dependencies", "vue") {
stack.framework = Some("Vue".to_string());
} else if has_dep(&pkg, "dependencies", "express") {
stack.framework = Some("Express".to_string());
}
}
}
stack
}
fn has_dep(pkg: &serde_json::Value, dep_type: &str, name: &str) -> bool {
pkg.get(dep_type).and_then(|deps| deps.get(name)).is_some()
}
fn detect_python_stack(dir: &Path) -> DetectedStack {
let mut stack = DetectedStack {
language: Language::Python,
framework: None,
test_runner: Some("pytest".to_string()), type_checker: None,
linter: None,
};
let pyproject_path = dir.join("pyproject.toml");
if let Ok(content) = std::fs::read_to_string(pyproject_path) {
let content_lower = content.to_lowercase();
if content_lower.contains("mypy") {
stack.type_checker = Some("mypy".to_string());
}
if content_lower.contains("ruff") {
stack.linter = Some("ruff".to_string());
} else if content_lower.contains("flake8") {
stack.linter = Some("flake8".to_string());
} else if content_lower.contains("pylint") {
stack.linter = Some("pylint".to_string());
}
if content_lower.contains("django") {
stack.framework = Some("Django".to_string());
} else if content_lower.contains("fastapi") {
stack.framework = Some("FastAPI".to_string());
} else if content_lower.contains("flask") {
stack.framework = Some("Flask".to_string());
}
}
stack
}
fn detect_go_stack(_dir: &Path) -> DetectedStack {
DetectedStack {
language: Language::Go,
framework: None,
test_runner: Some("go test".to_string()),
type_checker: Some("go build".to_string()), linter: Some("golangci-lint".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_rust_stack() {
let dir = TempDir::new().expect("temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]").expect("write");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::Rust);
assert_eq!(stack.test_runner, Some("cargo test".to_string()));
assert_eq!(stack.type_checker, Some("rustc".to_string()));
assert_eq!(stack.linter, Some("clippy".to_string()));
}
#[test]
fn test_detect_node_typescript_stack() {
let dir = TempDir::new().expect("temp dir");
let pkg_json = r#"{
"name": "test",
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^1.0.0",
"eslint": "^8.0.0"
},
"dependencies": {
"react": "^18.0.0"
}
}"#;
fs::write(dir.path().join("package.json"), pkg_json).expect("write");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::TypeScript);
assert_eq!(stack.test_runner, Some("vitest".to_string()));
assert_eq!(stack.type_checker, Some("tsc".to_string()));
assert_eq!(stack.linter, Some("eslint".to_string()));
assert_eq!(stack.framework, Some("React".to_string()));
}
#[test]
fn test_detect_node_javascript_stack() {
let dir = TempDir::new().expect("temp dir");
let pkg_json = r#"{
"name": "test",
"devDependencies": {
"jest": "^29.0.0"
},
"dependencies": {
"express": "^4.0.0"
}
}"#;
fs::write(dir.path().join("package.json"), pkg_json).expect("write");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::JavaScript);
assert_eq!(stack.test_runner, Some("jest".to_string()));
assert_eq!(stack.framework, Some("Express".to_string()));
}
#[test]
fn test_detect_python_stack() {
let dir = TempDir::new().expect("temp dir");
let pyproject = r#"
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
strict = true
[tool.ruff]
line-length = 88
[project]
dependencies = ["fastapi"]
"#;
fs::write(dir.path().join("pyproject.toml"), pyproject).expect("write");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::Python);
assert_eq!(stack.test_runner, Some("pytest".to_string()));
assert_eq!(stack.type_checker, Some("mypy".to_string()));
assert_eq!(stack.linter, Some("ruff".to_string()));
assert_eq!(stack.framework, Some("FastAPI".to_string()));
}
#[test]
fn test_detect_go_stack() {
let dir = TempDir::new().expect("temp dir");
fs::write(dir.path().join("go.mod"), "module example.com/test").expect("write");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::Go);
assert_eq!(stack.test_runner, Some("go test".to_string()));
assert_eq!(stack.linter, Some("golangci-lint".to_string()));
}
#[test]
fn test_detect_unknown_stack() {
let dir = TempDir::new().expect("temp dir");
let stack = detect_stack(dir.path());
assert_eq!(stack.language, Language::Unknown);
assert!(stack.test_runner.is_none());
}
#[test]
fn test_to_summary() {
let stack = DetectedStack {
language: Language::Rust,
framework: Some("Actix".to_string()),
test_runner: Some("cargo test".to_string()),
type_checker: Some("rustc".to_string()),
linter: Some("clippy".to_string()),
};
let summary = stack.to_summary();
assert!(summary.contains("Language: Rust"));
assert!(summary.contains("Framework: Actix"));
assert!(summary.contains("Test Runner: cargo test"));
assert!(summary.contains("Type Checker: rustc"));
assert!(summary.contains("Linter: clippy"));
}
#[test]
fn test_to_summary_minimal() {
let stack = DetectedStack::default();
let summary = stack.to_summary();
assert!(summary.contains("Language: Unknown"));
assert!(!summary.contains("Framework:"));
}
#[test]
fn test_language_display() {
assert_eq!(format!("{}", Language::Rust), "Rust");
assert_eq!(format!("{}", Language::TypeScript), "TypeScript");
assert_eq!(format!("{}", Language::JavaScript), "JavaScript");
assert_eq!(format!("{}", Language::Python), "Python");
assert_eq!(format!("{}", Language::Go), "Go");
assert_eq!(format!("{}", Language::Unknown), "Unknown");
}
}