1pub mod detector;
2pub mod languages;
3pub mod patterns;
4pub mod reporter;
5pub mod scanner;
6
7pub use detector::{Analysis, LanguageDetector, LanguageStats};
8pub use languages::{LanguageCategory, LanguageInfo, LanguageRegistry};
9pub use reporter::Reporter;
10pub use scanner::{FileInfo, Scanner};
11
12pub use crate::cli::OutputFormat;
14
15mod cli;
16
17#[cfg(test)]
18mod tests {
19 use super::*;
20 use tempfile::TempDir;
21 use std::fs;
22
23 pub struct TestCase {
25 pub language: String,
26 pub extension: String,
27 pub content: String,
28 pub filename: String,
29 }
30
31 fn generate_sample_content(language: &str) -> String {
33 match language {
34 "Rust" => "fn main() {\n println!(\"Hello, world!\");\n}".to_string(),
35 "Python" => "def hello():\n print(\"Hello, world!\")\n\nif __name__ == \"__main__\":\n hello()".to_string(),
36 "JavaScript" => "function hello() {\n console.log(\"Hello, world!\");\n}\n\nhello();".to_string(),
37 "TypeScript" => "interface Greeting {\n message: string;\n}\n\nfunction hello(): void {\n console.log(\"Hello, world!\");\n}".to_string(),
38 "Go" => "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello, world!\")\n}".to_string(),
39 "Java" => "public class Hello {\n public static void main(String[] args) {\n System.out.println(\"Hello, world!\");\n }\n}".to_string(),
40 "C" => "#include <stdio.h>\n\nint main() {\n printf(\"Hello, world!\\n\");\n return 0;\n}".to_string(),
41 "C++" => "#include <iostream>\n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}".to_string(),
42 "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(),
43 "PHP" => "<?php\nfunction hello() {\n echo \"Hello, world!\";\n}\nhello();\n?>".to_string(),
44 "Ruby" => "def hello\n puts \"Hello, world!\"\nend\n\nhello".to_string(),
45 "Swift" => "import Foundation\n\nfunc hello() {\n print(\"Hello, world!\")\n}\n\nhello()".to_string(),
46 "Kotlin" => "fun main() {\n println(\"Hello, world!\")\n}".to_string(),
47 "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(),
48 "CSS" => "body {\n margin: 0;\n padding: 0;\n font-family: Arial, sans-serif;\n}".to_string(),
49 "JSON" => "{\n \"message\": \"Hello, world!\",\n \"version\": \"1.0.0\",\n \"active\": true\n}".to_string(),
50 "YAML" => "---\nmessage: \"Hello, world!\"\nversion: 1.0.0\nactive: true".to_string(),
51 "XML" => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n <message>Hello, world!</message>\n</root>".to_string(),
52 "Markdown" => "# Hello World\n\nThis is a **markdown** document.\n\n```code\nSample code block\n```".to_string(),
53 "Shell" => "#!/bin/bash\necho \"Hello, world!\"\nif [ $? -eq 0 ]; then\n echo \"Success\"\nfi".to_string(),
54 _ => format!("// Sample {} code\nfunction hello() {{\n return \"Hello, world!\";\n}}", language),
55 }
56 }
57
58 #[test]
59 fn test_language_detection_by_extension() {
60 let detector = LanguageDetector::new().expect("Failed to create detector");
61
62 let test_cases = vec![
64 ("Rust", "rs"),
65 ("Python", "py"),
66 ("JavaScript", "js"),
67 ("TypeScript", "ts"),
68 ("Go", "go"),
69 ("Java", "java"),
70 ("C", "c"),
71 ("C++", "cpp"),
72 ("C#", "cs"),
73 ("PHP", "php"),
74 ("Ruby", "rb"),
75 ("Swift", "swift"),
76 ("Kotlin", "kt"),
77 ("HTML", "html"),
78 ("CSS", "css"),
79 ("JSON", "json"),
80 ("YAML", "yml"),
81 ("XML", "xml"), ("Markdown", "md"),
83 ("Shell", "sh"),
84 ];
85
86 for (language, extension) in test_cases {
87 let temp_dir = TempDir::new().expect("Failed to create temp dir");
88 let filename = format!("test.{}", extension);
89 let file_path = temp_dir.path().join(&filename);
90
91 let content = generate_sample_content(language);
92 fs::write(&file_path, &content)
93 .expect("Failed to write test file");
94
95 let file_info = FileInfo {
96 path: file_path.clone(),
97 size: content.len() as u64,
98 extension: Some(extension.to_string()),
99 is_binary: false,
100 };
101
102 let files = vec![file_info];
103 let analysis = detector.analyze_files(&files)
104 .expect("Failed to analyze files");
105
106 assert!(!analysis.stats.is_empty(),
108 "No language detected for {} file: {}",
109 language, filename);
110
111 let detected_lang = &analysis.stats[0].language;
113 assert_eq!(detected_lang, language,
114 "Expected {} but detected {} for file: {}",
115 language, detected_lang, filename);
116 }
117 }
118
119 #[test]
120 fn test_language_detection_by_content() {
121 let detector = LanguageDetector::new().expect("Failed to create detector");
122
123 let content_tests = vec![
126 ("#!/usr/bin/env python3\nprint('hello')", "Python"),
127 ("#!/bin/bash\necho hello", "Shell"),
128 ("#!/usr/bin/env node\nconsole.log('hello')", "JavaScript"),
129 ("<?php\necho 'Hello World';\n?>", "PHP"),
130 ("#include <stdio.h>\nint main() { return 0; }", "C"),
131 ];
132
133 let mut successful_detections = 0;
134 let total_tests = content_tests.len();
135
136 for (content, expected_lang) in content_tests {
137 let temp_dir = TempDir::new().expect("Failed to create temp dir");
138 let file_path = temp_dir.path().join("testfile"); fs::write(&file_path, content)
141 .expect("Failed to write test file");
142
143 let file_info = FileInfo {
144 path: file_path.clone(),
145 size: content.len() as u64,
146 extension: None, is_binary: false,
148 };
149
150 let files = vec![file_info];
151 let analysis = detector.analyze_files(&files)
152 .expect("Failed to analyze files");
153
154 if !analysis.stats.is_empty() {
155 let detected_lang = &analysis.stats[0].language;
156 if detected_lang == expected_lang {
157 successful_detections += 1;
158 } else {
159 println!("Content detection mismatch: expected {} but got {} for: {}",
160 expected_lang, detected_lang, content.lines().next().unwrap_or(content));
161 }
162 } else {
163 println!("No language detected for content: {}", content.lines().next().unwrap_or(content));
164 }
165 }
166
167 let success_rate = (successful_detections as f64 / total_tests as f64) * 100.0;
170 assert!(success_rate >= 80.0,
171 "Content-based detection success rate too low: {:.1}% ({}/{})",
172 success_rate, successful_detections, total_tests);
173 }
174
175 #[test]
176 fn test_special_files_detection() {
177 let detector = LanguageDetector::new().expect("Failed to create detector");
178
179 let special_files = vec![
180 ("Dockerfile", "FROM ubuntu:20.04\nRUN apt-get update", "Dockerfile"),
181 ("Makefile", "all:\n\techo 'Building...'\n\n.PHONY: all", "Makefile"),
182 ("Rakefile", "task :default do\n puts 'Hello'\nend", "Ruby"),
183 ("Gemfile", "source 'https://rubygems.org'\ngem 'rails'", "Ruby"),
184 ];
185
186 for (filename, content, expected_lang) in special_files {
187 let temp_dir = TempDir::new().expect("Failed to create temp dir");
188 let file_path = temp_dir.path().join(filename);
189
190 fs::write(&file_path, content)
191 .expect("Failed to write test file");
192
193 let file_info = FileInfo {
194 path: file_path.clone(),
195 size: content.len() as u64,
196 extension: None,
197 is_binary: false,
198 };
199
200 let files = vec![file_info];
201 let analysis = detector.analyze_files(&files)
202 .expect("Failed to analyze files");
203
204 assert!(!analysis.stats.is_empty(),
205 "No language detected for special file: {}", filename);
206
207 let detected_lang = &analysis.stats[0].language;
208 assert_eq!(detected_lang, expected_lang,
209 "Expected {} but detected {} for special file: {}",
210 expected_lang, detected_lang, filename);
211 }
212 }
213
214 #[test]
215 fn test_multiple_extensions_same_language() {
216 let detector = LanguageDetector::new().expect("Failed to create detector");
217
218 let multi_ext_tests = vec![
220 ("Python", vec!["py", "pyw", "py3", "pyi"]),
221 ("JavaScript", vec!["js", "mjs", "cjs", "es6"]),
222 ("TypeScript", vec!["ts", "mts", "cts"]),
223 ("C++", vec!["cpp", "cc", "cxx", "hpp"]),
224 ];
225
226 for (language, extensions) in multi_ext_tests {
227 for ext in extensions {
228 let temp_dir = TempDir::new().expect("Failed to create temp dir");
229 let filename = format!("test.{}", ext);
230 let file_path = temp_dir.path().join(&filename);
231
232 let content = generate_sample_content(language);
233 fs::write(&file_path, &content)
234 .expect("Failed to write test file");
235
236 let file_info = FileInfo {
237 path: file_path.clone(),
238 size: content.len() as u64,
239 extension: Some(ext.to_string()),
240 is_binary: false,
241 };
242
243 let files = vec![file_info];
244 let analysis = detector.analyze_files(&files)
245 .expect("Failed to analyze files");
246
247 assert!(!analysis.stats.is_empty(),
248 "No language detected for {}.{}", language, ext);
249
250 let detected_lang = &analysis.stats[0].language;
251 assert_eq!(detected_lang, language,
252 "Expected {} but detected {} for extension: {}",
253 language, detected_lang, ext);
254 }
255 }
256 }
257
258 #[test]
259 fn test_analysis_statistics() {
260 let detector = LanguageDetector::new().expect("Failed to create detector");
261 let temp_dir = TempDir::new().expect("Failed to create temp dir");
262
263 let test_files = vec![
265 ("test1.rs", "fn main() { println!(\"Hello\"); }", "Rust"),
266 ("test2.py", "def hello():\n print('Hello')", "Python"),
267 ("test3.js", "function hello() { console.log('Hello'); }", "JavaScript"),
268 ("test4.rs", "use std::collections::HashMap;", "Rust"), ];
270
271 let mut file_infos = Vec::new();
272
273 for (filename, content, _) in &test_files {
274 let file_path = temp_dir.path().join(filename);
275 fs::write(&file_path, content).expect("Failed to write test file");
276
277 let extension = filename.split('.').last().map(|s| s.to_string());
278 file_infos.push(FileInfo {
279 path: file_path,
280 size: content.len() as u64,
281 extension,
282 is_binary: false,
283 });
284 }
285
286 let analysis = detector.analyze_files(&file_infos)
287 .expect("Failed to analyze files");
288
289 assert_eq!(analysis.total_files, 4);
291 assert!(analysis.total_bytes > 0);
292
293 assert_eq!(analysis.stats.len(), 3);
295
296 let rust_stats = analysis.stats.iter()
298 .find(|s| s.language == "Rust")
299 .expect("Rust should be detected");
300 assert_eq!(rust_stats.file_count, 2);
301
302 let total_percentage: f64 = analysis.stats.iter()
304 .map(|s| s.percentage)
305 .sum();
306 assert!((total_percentage - 100.0).abs() < 0.1,
307 "Percentages should add up to 100%");
308 }
309}