git_iris/file_analyzers/
mod.rs

1use regex::Regex;
2use std::path::Path;
3
4use crate::{
5    context::{ProjectMetadata, StagedFile},
6    log_debug,
7};
8
9/// Trait for analyzing files and extracting relevant information
10pub trait FileAnalyzer: Send + Sync {
11    fn analyze(&self, file: &str, staged_file: &StagedFile) -> Vec<String>;
12    fn get_file_type(&self) -> &'static str;
13    fn extract_metadata(&self, file: &str, content: &str) -> ProjectMetadata;
14}
15
16/// Module for analyzing C files
17mod c;
18/// Module for analyzing C++ files
19mod cpp;
20/// Module for analyzing Gradle files
21mod gradle;
22/// Module for analyzing Java files
23mod java;
24/// Module for analyzing JavaScript files
25mod javascript;
26/// Module for analyzing JSON files
27mod json;
28/// Module for analyzing Kotlin files
29mod kotlin;
30/// Module for analyzing Markdown files
31mod markdown;
32/// Module for analyzing Python files
33mod python;
34/// Module for analyzing Rust files
35mod rust;
36/// Module for analyzing YAML files
37mod yaml;
38
39/// Get the appropriate file analyzer based on the file extension
40pub fn get_analyzer(file: &str) -> Box<dyn FileAnalyzer + Send + Sync> {
41    let file_lower = file.to_lowercase();
42    let path = std::path::Path::new(&file_lower);
43
44    // Special cases for files with specific names
45    if file == "Makefile" {
46        return Box::new(c::CAnalyzer);
47    } else if file == "CMakeLists.txt" {
48        return Box::new(cpp::CppAnalyzer);
49    }
50
51    // Special cases for compound extensions
52    if file_lower.ends_with(".gradle") || file_lower.ends_with(".gradle.kts") {
53        return Box::new(gradle::GradleAnalyzer);
54    }
55
56    // Standard extension-based matching
57    if let Some(ext) = path.extension() {
58        if let Some(ext_str) = ext.to_str() {
59            let ext_lower = ext_str.to_lowercase();
60            match ext_lower.as_str() {
61                "c" => return Box::new(c::CAnalyzer),
62                "cpp" | "cc" | "cxx" => return Box::new(cpp::CppAnalyzer),
63                "rs" => return Box::new(rust::RustAnalyzer),
64                "py" => return Box::new(python::PythonAnalyzer),
65                "js" | "jsx" | "ts" | "tsx" => return Box::new(javascript::JavaScriptAnalyzer),
66                "java" => return Box::new(java::JavaAnalyzer),
67                "kt" | "kts" => return Box::new(kotlin::KotlinAnalyzer),
68                "json" => return Box::new(json::JsonAnalyzer),
69                "md" | "markdown" => return Box::new(markdown::MarkdownAnalyzer),
70                "yaml" | "yml" => return Box::new(yaml::YamlAnalyzer),
71                _ => {}
72            }
73        }
74    }
75
76    Box::new(DefaultAnalyzer)
77}
78
79/// Default analyzer for unsupported file types
80struct DefaultAnalyzer;
81
82impl FileAnalyzer for DefaultAnalyzer {
83    fn analyze(&self, _file: &str, _staged_file: &StagedFile) -> Vec<String> {
84        vec![]
85    }
86
87    fn get_file_type(&self) -> &'static str {
88        "Unknown file type"
89    }
90
91    fn extract_metadata(&self, _file: &str, _content: &str) -> ProjectMetadata {
92        ProjectMetadata {
93            language: Some("Unknown".to_string()),
94            ..Default::default()
95        }
96    }
97}
98
99/// Checks if a file should be excluded from analysis.
100///
101/// # Arguments
102///
103/// * `path` - The path of the file to check.
104///
105/// # Returns
106///
107/// A boolean indicating whether the file should be excluded.
108pub fn should_exclude_file(path: &str) -> bool {
109    log_debug!("Checking if file should be excluded: {}", path);
110    let exclude_patterns = vec![
111        (String::from(r"\.git"), false),
112        (String::from(r"\.svn"), false),
113        (String::from(r"\.hg"), false),
114        (String::from(r"\.DS_Store"), false),
115        (String::from(r"node_modules"), false),
116        (String::from(r"target"), false),
117        (String::from(r"build"), false),
118        (String::from(r"dist"), false),
119        (String::from(r"\.vscode"), false),
120        (String::from(r"\.idea"), false),
121        (String::from(r"\.vs"), false),
122        (String::from(r"package-lock\.json$"), true),
123        (String::from(r"\.lock$"), true),
124        (String::from(r"\.log$"), true),
125        (String::from(r"\.tmp$"), true),
126        (String::from(r"\.temp$"), true),
127        (String::from(r"\.swp$"), true),
128        (String::from(r"\.min\.js$"), true),
129    ];
130
131    let path = Path::new(path);
132
133    for (pattern, is_extension) in exclude_patterns {
134        let re = match Regex::new(&pattern) {
135            Ok(re) => re,
136            Err(e) => {
137                log_debug!("Failed to compile regex '{}': {}", pattern, e);
138                continue;
139            }
140        };
141
142        if is_extension {
143            if let Some(file_name) = path.file_name() {
144                if let Some(file_name_str) = file_name.to_str() {
145                    if re.is_match(file_name_str) {
146                        log_debug!("File excluded: {}", path.display());
147                        return true;
148                    }
149                }
150            }
151        } else if let Some(path_str) = path.to_str() {
152            if re.is_match(path_str) {
153                log_debug!("File excluded: {}", path.display());
154                return true;
155            }
156        }
157    }
158    log_debug!("File not excluded: {}", path.display());
159    false
160}