pub mod detector;
pub mod languages;
pub mod patterns;
pub mod reporter;
pub mod scanner;
pub use detector::{Analysis, LanguageDetector, LanguageStats};
pub use languages::{LanguageCategory, LanguageInfo, LanguageRegistry};
pub use reporter::Reporter;
pub use scanner::{FileInfo, Scanner};
pub use crate::cli::OutputFormat;
mod cli;
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::fs;
pub struct TestCase {
pub language: String,
pub extension: String,
pub content: String,
pub filename: String,
}
fn generate_sample_content(language: &str) -> String {
match language {
"Rust" => "fn main() {\n println!(\"Hello, world!\");\n}".to_string(),
"Python" => "def hello():\n print(\"Hello, world!\")\n\nif __name__ == \"__main__\":\n hello()".to_string(),
"JavaScript" => "function hello() {\n console.log(\"Hello, world!\");\n}\n\nhello();".to_string(),
"TypeScript" => "interface Greeting {\n message: string;\n}\n\nfunction hello(): void {\n console.log(\"Hello, world!\");\n}".to_string(),
"Go" => "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello, world!\")\n}".to_string(),
"Java" => "public class Hello {\n public static void main(String[] args) {\n System.out.println(\"Hello, world!\");\n }\n}".to_string(),
"C" => "#include <stdio.h>\n\nint main() {\n printf(\"Hello, world!\\n\");\n return 0;\n}".to_string(),
"C++" => "#include <iostream>\n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}".to_string(),
"C#" => "using System;\n\nnamespace HelloWorld {\n public class Program {\n public static void Main() {\n Console.WriteLine(\"Hello, world!\");\n }\n }\n}".to_string(),
"PHP" => "<?php\nfunction hello() {\n echo \"Hello, world!\";\n}\nhello();\n?>".to_string(),
"Ruby" => "def hello\n puts \"Hello, world!\"\nend\n\nhello".to_string(),
"Swift" => "import Foundation\n\nfunc hello() {\n print(\"Hello, world!\")\n}\n\nhello()".to_string(),
"Kotlin" => "fun main() {\n println(\"Hello, world!\")\n}".to_string(),
"HTML" => "<!DOCTYPE html>\n<html>\n<head>\n <title>Hello</title>\n</head>\n<body>\n <h1>Hello, world!</h1>\n</body>\n</html>".to_string(),
"CSS" => "body {\n margin: 0;\n padding: 0;\n font-family: Arial, sans-serif;\n}".to_string(),
"JSON" => "{\n \"message\": \"Hello, world!\",\n \"version\": \"1.0.0\",\n \"active\": true\n}".to_string(),
"YAML" => "---\nmessage: \"Hello, world!\"\nversion: 1.0.0\nactive: true".to_string(),
"XML" => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n <message>Hello, world!</message>\n</root>".to_string(),
"Markdown" => "# Hello World\n\nThis is a **markdown** document.\n\n```code\nSample code block\n```".to_string(),
"Shell" => "#!/bin/bash\necho \"Hello, world!\"\nif [ $? -eq 0 ]; then\n echo \"Success\"\nfi".to_string(),
_ => format!("// Sample {} code\nfunction hello() {{\n return \"Hello, world!\";\n}}", language),
}
}
#[test]
fn test_language_detection_by_extension() {
let detector = LanguageDetector::new().expect("Failed to create detector");
let test_cases = vec![
("Rust", "rs"),
("Python", "py"),
("JavaScript", "js"),
("TypeScript", "ts"),
("Go", "go"),
("Java", "java"),
("C", "c"),
("C++", "cpp"),
("C#", "cs"),
("PHP", "php"),
("Ruby", "rb"),
("Swift", "swift"),
("Kotlin", "kt"),
("HTML", "html"),
("CSS", "css"),
("JSON", "json"),
("YAML", "yml"),
("XML", "xml"), ("Markdown", "md"),
("Shell", "sh"),
];
for (language, extension) in test_cases {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let filename = format!("test.{}", extension);
let file_path = temp_dir.path().join(&filename);
let content = generate_sample_content(language);
fs::write(&file_path, &content)
.expect("Failed to write test file");
let file_info = FileInfo {
path: file_path.clone(),
size: content.len() as u64,
extension: Some(extension.to_string()),
is_binary: false,
};
let files = vec![file_info];
let analysis = detector.analyze_files(&files)
.expect("Failed to analyze files");
assert!(!analysis.stats.is_empty(),
"No language detected for {} file: {}",
language, filename);
let detected_lang = &analysis.stats[0].language;
assert_eq!(detected_lang, language,
"Expected {} but detected {} for file: {}",
language, detected_lang, filename);
}
}
#[test]
fn test_language_detection_by_content() {
let detector = LanguageDetector::new().expect("Failed to create detector");
let content_tests = vec![
("#!/usr/bin/env python3\nprint('hello')", "Python"),
("#!/bin/bash\necho hello", "Shell"),
("#!/usr/bin/env node\nconsole.log('hello')", "JavaScript"),
("<?php\necho 'Hello World';\n?>", "PHP"),
("#include <stdio.h>\nint main() { return 0; }", "C"),
];
let mut successful_detections = 0;
let total_tests = content_tests.len();
for (content, expected_lang) in content_tests {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("testfile");
fs::write(&file_path, content)
.expect("Failed to write test file");
let file_info = FileInfo {
path: file_path.clone(),
size: content.len() as u64,
extension: None, is_binary: false,
};
let files = vec![file_info];
let analysis = detector.analyze_files(&files)
.expect("Failed to analyze files");
if !analysis.stats.is_empty() {
let detected_lang = &analysis.stats[0].language;
if detected_lang == expected_lang {
successful_detections += 1;
} else {
println!("Content detection mismatch: expected {} but got {} for: {}",
expected_lang, detected_lang, content.lines().next().unwrap_or(content));
}
} else {
println!("No language detected for content: {}", content.lines().next().unwrap_or(content));
}
}
let success_rate = (successful_detections as f64 / total_tests as f64) * 100.0;
assert!(success_rate >= 80.0,
"Content-based detection success rate too low: {:.1}% ({}/{})",
success_rate, successful_detections, total_tests);
}
#[test]
fn test_special_files_detection() {
let detector = LanguageDetector::new().expect("Failed to create detector");
let special_files = vec![
("Dockerfile", "FROM ubuntu:20.04\nRUN apt-get update", "Dockerfile"),
("Makefile", "all:\n\techo 'Building...'\n\n.PHONY: all", "Makefile"),
("Rakefile", "task :default do\n puts 'Hello'\nend", "Ruby"),
("Gemfile", "source 'https://rubygems.org'\ngem 'rails'", "Ruby"),
];
for (filename, content, expected_lang) in special_files {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join(filename);
fs::write(&file_path, content)
.expect("Failed to write test file");
let file_info = FileInfo {
path: file_path.clone(),
size: content.len() as u64,
extension: None,
is_binary: false,
};
let files = vec![file_info];
let analysis = detector.analyze_files(&files)
.expect("Failed to analyze files");
assert!(!analysis.stats.is_empty(),
"No language detected for special file: {}", filename);
let detected_lang = &analysis.stats[0].language;
assert_eq!(detected_lang, expected_lang,
"Expected {} but detected {} for special file: {}",
expected_lang, detected_lang, filename);
}
}
#[test]
fn test_multiple_extensions_same_language() {
let detector = LanguageDetector::new().expect("Failed to create detector");
let multi_ext_tests = vec![
("Python", vec!["py", "pyw", "py3", "pyi"]),
("JavaScript", vec!["js", "mjs", "cjs", "es6"]),
("TypeScript", vec!["ts", "mts", "cts"]),
("C++", vec!["cpp", "cc", "cxx", "hpp"]),
];
for (language, extensions) in multi_ext_tests {
for ext in extensions {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let filename = format!("test.{}", ext);
let file_path = temp_dir.path().join(&filename);
let content = generate_sample_content(language);
fs::write(&file_path, &content)
.expect("Failed to write test file");
let file_info = FileInfo {
path: file_path.clone(),
size: content.len() as u64,
extension: Some(ext.to_string()),
is_binary: false,
};
let files = vec![file_info];
let analysis = detector.analyze_files(&files)
.expect("Failed to analyze files");
assert!(!analysis.stats.is_empty(),
"No language detected for {}.{}", language, ext);
let detected_lang = &analysis.stats[0].language;
assert_eq!(detected_lang, language,
"Expected {} but detected {} for extension: {}",
language, detected_lang, ext);
}
}
}
#[test]
fn test_analysis_statistics() {
let detector = LanguageDetector::new().expect("Failed to create detector");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let test_files = vec![
("test1.rs", "fn main() { println!(\"Hello\"); }", "Rust"),
("test2.py", "def hello():\n print('Hello')", "Python"),
("test3.js", "function hello() { console.log('Hello'); }", "JavaScript"),
("test4.rs", "use std::collections::HashMap;", "Rust"), ];
let mut file_infos = Vec::new();
for (filename, content, _) in &test_files {
let file_path = temp_dir.path().join(filename);
fs::write(&file_path, content).expect("Failed to write test file");
let extension = filename.split('.').last().map(|s| s.to_string());
file_infos.push(FileInfo {
path: file_path,
size: content.len() as u64,
extension,
is_binary: false,
});
}
let analysis = detector.analyze_files(&file_infos)
.expect("Failed to analyze files");
assert_eq!(analysis.total_files, 4);
assert!(analysis.total_bytes > 0);
assert_eq!(analysis.stats.len(), 3);
let rust_stats = analysis.stats.iter()
.find(|s| s.language == "Rust")
.expect("Rust should be detected");
assert_eq!(rust_stats.file_count, 2);
let total_percentage: f64 = analysis.stats.iter()
.map(|s| s.percentage)
.sum();
assert!((total_percentage - 100.0).abs() < 0.1,
"Percentages should add up to 100%");
}
}