1use regex::Regex;
2use std::path::Path;
3
4use crate::{
5 core::context::{ProjectMetadata, StagedFile},
6 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 && let Some(name_str) = name.to_str()
108 {
109 let config_file_names = [
111 "dockerfile",
112 ".gitignore",
113 ".gitattributes",
114 ".env",
115 "makefile",
116 "readme",
117 "license",
118 "authors",
119 "contributors",
120 "changelog",
121 "config",
122 "codeowners",
123 ".dockerignore",
124 ".npmrc",
125 ".yarnrc",
126 ".eslintrc",
127 ".prettierrc",
128 ".babelrc",
129 ".stylelintrc",
130 ];
131
132 for name in config_file_names {
133 if name_str.to_lowercase() == name.to_lowercase() {
134 return true;
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 debug!("Checking if file should be excluded: {}", path);
173 let exclude_patterns = vec![
174 (String::from(r"(^|/)\.git(/|$)"), false), (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 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 && let Some(file_name_str) = file_name.to_str()
208 && re.is_match(file_name_str)
209 {
210 debug!("File excluded: {}", path.display());
211 return true;
212 }
213 } else if let Some(path_str) = path.to_str()
214 && re.is_match(path_str)
215 {
216 debug!("File excluded: {}", path.display());
217 return true;
218 }
219 }
220 debug!("File not excluded: {}", path.display());
221 false
222}