1use regex::Regex;
2use std::path::Path;
3
4use crate::{
5 context::{ProjectMetadata, StagedFile},
6 log_debug,
7};
8
9pub 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
16mod c;
18mod cpp;
20mod gradle;
22mod java;
24mod javascript;
26mod json;
28mod kotlin;
30mod markdown;
32mod python;
34mod rust;
36mod toml;
38mod yaml;
40
41mod text;
43
44pub fn get_analyzer(file: &str) -> Box<dyn FileAnalyzer + Send + Sync> {
46 let file_lower = file.to_lowercase();
47 let path = std::path::Path::new(&file_lower);
48
49 if file == "Makefile" {
51 return Box::new(c::CAnalyzer);
52 } else if file == "CMakeLists.txt" {
53 return Box::new(cpp::CppAnalyzer);
54 }
55
56 if file_lower.ends_with(".gradle") || file_lower.ends_with(".gradle.kts") {
58 return Box::new(gradle::GradleAnalyzer);
59 }
60
61 if let Some(ext) = path.extension() {
63 if let Some(ext_str) = ext.to_str() {
64 let ext_lower = ext_str.to_lowercase();
65 match ext_lower.as_str() {
66 "c" => return Box::new(c::CAnalyzer),
67 "cpp" | "cc" | "cxx" => return Box::new(cpp::CppAnalyzer),
68 "rs" => return Box::new(rust::RustAnalyzer),
69 "py" => return Box::new(python::PythonAnalyzer),
70 "js" | "jsx" | "ts" | "tsx" => return Box::new(javascript::JavaScriptAnalyzer),
71 "java" => return Box::new(java::JavaAnalyzer),
72 "kt" | "kts" => return Box::new(kotlin::KotlinAnalyzer),
73 "json" => return Box::new(json::JsonAnalyzer),
74 "md" | "markdown" => return Box::new(markdown::MarkdownAnalyzer),
75 "yaml" | "yml" => return Box::new(yaml::YamlAnalyzer),
76 "toml" => return Box::new(toml::TomlAnalyzer),
77 "txt" | "cfg" | "ini" | "properties" | "env" | "conf" | "config" | "xml"
79 | "htm" | "html" | "css" | "scss" | "sass" | "less" | "sql" | "sh" | "bash"
80 | "zsh" | "bat" | "cmd" | "ps1" | "dockerfile" | "editorconfig" | "gitignore"
81 | "gitattributes" | "nginx" | "service" => {
82 return Box::new(text::GenericTextAnalyzer);
83 }
84 _ => {
85 if is_likely_text_file(file) {
87 return Box::new(text::GenericTextAnalyzer);
88 }
89 }
90 }
91 }
92 } else {
93 if is_likely_text_file(file) {
95 return Box::new(text::GenericTextAnalyzer);
96 }
97 }
98
99 Box::new(DefaultAnalyzer)
101}
102
103fn is_likely_text_file(file: &str) -> bool {
105 let file_name = std::path::Path::new(file).file_name();
106 if let Some(name) = file_name {
107 if let Some(name_str) = name.to_str() {
108 let config_file_names = [
110 "dockerfile",
111 ".gitignore",
112 ".gitattributes",
113 ".env",
114 "makefile",
115 "readme",
116 "license",
117 "authors",
118 "contributors",
119 "changelog",
120 "config",
121 "codeowners",
122 ".dockerignore",
123 ".npmrc",
124 ".yarnrc",
125 ".eslintrc",
126 ".prettierrc",
127 ".babelrc",
128 ".stylelintrc",
129 ];
130
131 for name in config_file_names {
132 if name_str.to_lowercase() == name.to_lowercase() {
133 return true;
134 }
135 }
136 }
137 }
138
139 false
140}
141
142struct DefaultAnalyzer;
144
145impl FileAnalyzer for DefaultAnalyzer {
146 fn analyze(&self, _file: &str, _staged_file: &StagedFile) -> Vec<String> {
147 vec!["Unable to analyze non-text or binary file".to_string()]
148 }
149
150 fn get_file_type(&self) -> &'static str {
151 "Unknown or binary file"
152 }
153
154 fn extract_metadata(&self, _file: &str, _content: &str) -> ProjectMetadata {
155 ProjectMetadata {
156 language: Some("Binary/Unknown".to_string()),
157 ..Default::default()
158 }
159 }
160}
161
162pub fn should_exclude_file(path: &str) -> bool {
172 log_debug!("Checking if file should be excluded: {}", path);
173 let exclude_patterns = vec![
174 (String::from(r"\.git"), false),
175 (String::from(r"\.svn"), false),
176 (String::from(r"\.hg"), false),
177 (String::from(r"\.DS_Store"), false),
178 (String::from(r"node_modules"), false),
179 (String::from(r"target"), false),
180 (String::from(r"build"), false),
181 (String::from(r"dist"), false),
182 (String::from(r"\.vscode"), false),
183 (String::from(r"\.idea"), false),
184 (String::from(r"\.vs"), false),
185 (String::from(r"package-lock\.json$"), true),
186 (String::from(r"\.lock$"), true),
187 (String::from(r"\.log$"), true),
188 (String::from(r"\.tmp$"), true),
189 (String::from(r"\.temp$"), true),
190 (String::from(r"\.swp$"), true),
191 (String::from(r"\.min\.js$"), true),
192 ];
193
194 let path = Path::new(path);
195
196 for (pattern, is_extension) in exclude_patterns {
197 let re = match Regex::new(&pattern) {
198 Ok(re) => re,
199 Err(e) => {
200 log_debug!("Failed to compile regex '{}': {}", pattern, e);
201 continue;
202 }
203 };
204
205 if is_extension {
206 if let Some(file_name) = path.file_name() {
207 if let Some(file_name_str) = file_name.to_str() {
208 if re.is_match(file_name_str) {
209 log_debug!("File excluded: {}", path.display());
210 return true;
211 }
212 }
213 }
214 } else if let Some(path_str) = path.to_str() {
215 if re.is_match(path_str) {
216 log_debug!("File excluded: {}", path.display());
217 return true;
218 }
219 }
220 }
221 log_debug!("File not excluded: {}", path.display());
222 false
223}