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
40#[allow(clippy::case_sensitive_file_extension_comparisons)] // todo: check if we should compare case-insensitively
41pub fn get_analyzer(file: &str) -> Box<dyn FileAnalyzer + Send + Sync> {
42    if file.ends_with(".c") || file == "Makefile" {
43        Box::new(c::CAnalyzer)
44    } else if file.ends_with(".cpp")
45        || file.ends_with(".cc")
46        || file.ends_with(".cxx")
47        || file == "CMakeLists.txt"
48    {
49        Box::new(cpp::CppAnalyzer)
50    } else if file.ends_with(".rs") {
51        Box::new(rust::RustAnalyzer)
52    } else if file.ends_with(".js") || file.ends_with(".ts") {
53        Box::new(javascript::JavaScriptAnalyzer)
54    } else if file.ends_with(".py") {
55        Box::new(python::PythonAnalyzer)
56    } else if file.ends_with(".yaml") || file.ends_with(".yml") {
57        Box::new(yaml::YamlAnalyzer)
58    } else if file.ends_with(".json") {
59        Box::new(json::JsonAnalyzer)
60    } else if file.ends_with(".md") {
61        Box::new(markdown::MarkdownAnalyzer)
62    } else if file.ends_with(".java") {
63        Box::new(java::JavaAnalyzer)
64    } else if file.ends_with(".kt") {
65        Box::new(kotlin::KotlinAnalyzer)
66    } else if file.ends_with(".gradle") || file.ends_with(".gradle.kts") {
67        Box::new(gradle::GradleAnalyzer)
68    } else {
69        Box::new(DefaultAnalyzer)
70    }
71}
72
73/// Default analyzer for unsupported file types
74struct DefaultAnalyzer;
75
76impl FileAnalyzer for DefaultAnalyzer {
77    fn analyze(&self, _file: &str, _staged_file: &StagedFile) -> Vec<String> {
78        vec![]
79    }
80
81    fn get_file_type(&self) -> &'static str {
82        "Unknown file type"
83    }
84
85    fn extract_metadata(&self, _file: &str, _content: &str) -> ProjectMetadata {
86        ProjectMetadata {
87            language: Some("Unknown".to_string()),
88            ..Default::default()
89        }
90    }
91}
92
93/// Checks if a file should be excluded from analysis.
94///
95/// # Arguments
96///
97/// * `path` - The path of the file to check.
98///
99/// # Returns
100///
101/// A boolean indicating whether the file should be excluded.
102#[allow(clippy::unwrap_used)]
103pub fn should_exclude_file(path: &str) -> bool {
104    log_debug!("Checking if file should be excluded: {}", path);
105    let exclude_patterns = vec![
106        (String::from(r"\.git"), false),
107        (String::from(r"\.svn"), false),
108        (String::from(r"\.hg"), false),
109        (String::from(r"\.DS_Store"), false),
110        (String::from(r"node_modules"), false),
111        (String::from(r"target"), false),
112        (String::from(r"build"), false),
113        (String::from(r"dist"), false),
114        (String::from(r"\.vscode"), false),
115        (String::from(r"\.idea"), false),
116        (String::from(r"\.vs"), false),
117        (String::from(r"package-lock\.json$"), true),
118        (String::from(r"\.lock$"), true),
119        (String::from(r"\.log$"), true),
120        (String::from(r"\.tmp$"), true),
121        (String::from(r"\.temp$"), true),
122        (String::from(r"\.swp$"), true),
123        (String::from(r"\.min\.js$"), true),
124    ];
125
126    let path = Path::new(path);
127
128    for (pattern, is_extension) in exclude_patterns {
129        let re = Regex::new(&pattern).expect("Could not compile regex");
130        if is_extension {
131            if let Some(file_name) = path.file_name() {
132                if re.is_match(file_name.to_str().unwrap()) {
133                    log_debug!("File excluded: {}", path.display());
134                    return true;
135                }
136            }
137        } else if re.is_match(path.to_str().unwrap()) {
138            log_debug!("File excluded: {}", path.display());
139            return true;
140        }
141    }
142    log_debug!("File not excluded: {}", path.display());
143    false
144}