1use crate::data::context::{
4 ArchitecturalLayer, ChangeImpact, FileContext, FilePurpose, ProjectSignificance,
5};
6use std::path::{Path, PathBuf};
7
8pub struct FileAnalyzer;
10
11impl FileAnalyzer {
12 pub fn analyze_file(path: &Path, change_type: &str) -> FileContext {
14 let file_purpose = determine_file_purpose(path);
15 let architectural_layer = determine_architectural_layer(path, &file_purpose);
16 let change_impact = determine_change_impact(change_type, &file_purpose);
17 let project_significance = determine_project_significance(path, &file_purpose);
18
19 FileContext {
20 path: path.to_path_buf(),
21 file_purpose,
22 architectural_layer,
23 change_impact,
24 project_significance,
25 }
26 }
27
28 pub fn analyze_file_set(files: &[(PathBuf, String)]) -> Vec<FileContext> {
30 files
31 .iter()
32 .map(|(path, change_type)| Self::analyze_file(path, change_type))
33 .collect()
34 }
35
36 pub fn primary_architectural_impact(contexts: &[FileContext]) -> ArchitecturalLayer {
38 use std::collections::HashMap;
39
40 let mut layer_counts = HashMap::new();
41 for context in contexts {
42 *layer_counts
43 .entry(context.architectural_layer.clone())
44 .or_insert(0) += 1;
45 }
46
47 layer_counts
49 .into_iter()
50 .max_by_key(|(layer, count)| {
51 let priority = match layer {
52 ArchitecturalLayer::Business => 100,
53 ArchitecturalLayer::Data => 90,
54 ArchitecturalLayer::Presentation => 80,
55 ArchitecturalLayer::Infrastructure => 70,
56 ArchitecturalLayer::Cross => 60,
57 };
58 priority + count
59 })
60 .map(|(layer, _)| layer)
61 .unwrap_or(ArchitecturalLayer::Cross)
62 }
63
64 pub fn is_architectural_change(contexts: &[FileContext]) -> bool {
66 let critical_files = contexts
67 .iter()
68 .filter(|c| matches!(c.project_significance, ProjectSignificance::Critical))
69 .count();
70
71 let breaking_changes = contexts
72 .iter()
73 .filter(|c| {
74 matches!(
75 c.change_impact,
76 ChangeImpact::Breaking | ChangeImpact::Critical
77 )
78 })
79 .count();
80
81 critical_files > 0 || breaking_changes > 1 || contexts.len() > 10
82 }
83}
84
85fn determine_file_purpose(path: &Path) -> FilePurpose {
87 let path_str = path.to_string_lossy().to_lowercase();
88 let file_name = path
89 .file_name()
90 .and_then(|name| name.to_str())
91 .unwrap_or("")
92 .to_lowercase();
93
94 if is_config_file(&path_str, &file_name) {
96 return FilePurpose::Config;
97 }
98
99 if is_test_file(&path_str, &file_name) {
101 return FilePurpose::Test;
102 }
103
104 if is_documentation_file(&path_str, &file_name) {
106 return FilePurpose::Documentation;
107 }
108
109 if is_build_file(&path_str, &file_name) {
111 return FilePurpose::Build;
112 }
113
114 if is_tooling_file(&path_str, &file_name) {
116 return FilePurpose::Tooling;
117 }
118
119 if is_interface_file(&path_str, &file_name) {
121 return FilePurpose::Interface;
122 }
123
124 FilePurpose::CoreLogic
126}
127
128fn determine_architectural_layer(path: &Path, file_purpose: &FilePurpose) -> ArchitecturalLayer {
130 let path_str = path.to_string_lossy().to_lowercase();
131
132 match file_purpose {
133 FilePurpose::Config | FilePurpose::Build | FilePurpose::Tooling => {
134 ArchitecturalLayer::Infrastructure
135 }
136 FilePurpose::Test | FilePurpose::Documentation => ArchitecturalLayer::Cross,
137 FilePurpose::Interface => ArchitecturalLayer::Presentation,
138 FilePurpose::CoreLogic => {
139 if path_str.contains("ui") || path_str.contains("web") || path_str.contains("cli") {
141 ArchitecturalLayer::Presentation
142 } else if path_str.contains("data")
143 || path_str.contains("db")
144 || path_str.contains("storage")
145 {
146 ArchitecturalLayer::Data
147 } else if path_str.contains("core")
148 || path_str.contains("business")
149 || path_str.contains("logic")
150 {
151 ArchitecturalLayer::Business
152 } else if path_str.contains("infra")
153 || path_str.contains("system")
154 || path_str.contains("network")
155 {
156 ArchitecturalLayer::Infrastructure
157 } else {
158 ArchitecturalLayer::Business }
160 }
161 }
162}
163
164fn determine_change_impact(change_type: &str, file_purpose: &FilePurpose) -> ChangeImpact {
166 match change_type {
167 "A" => ChangeImpact::Additive, "D" => {
169 match file_purpose {
171 FilePurpose::Interface | FilePurpose::CoreLogic => ChangeImpact::Breaking,
172 _ => ChangeImpact::Modification,
173 }
174 }
175 "M" => {
176 match file_purpose {
178 FilePurpose::Config => ChangeImpact::Modification,
179 FilePurpose::Test | FilePurpose::Documentation => ChangeImpact::Style,
180 FilePurpose::Interface => ChangeImpact::Breaking, _ => ChangeImpact::Modification,
182 }
183 }
184 "R" => ChangeImpact::Modification, "C" => ChangeImpact::Additive, _ => ChangeImpact::Modification, }
188}
189
190fn determine_project_significance(path: &Path, file_purpose: &FilePurpose) -> ProjectSignificance {
192 let path_str = path.to_string_lossy().to_lowercase();
193 let file_name = path
194 .file_name()
195 .and_then(|name| name.to_str())
196 .unwrap_or("")
197 .to_lowercase();
198
199 if is_critical_file(&path_str, &file_name) {
201 return ProjectSignificance::Critical;
202 }
203
204 match file_purpose {
206 FilePurpose::Interface | FilePurpose::CoreLogic => ProjectSignificance::Important,
207 FilePurpose::Config => {
208 if file_name.contains("cargo.toml") || file_name.contains("package.json") {
209 ProjectSignificance::Critical
210 } else {
211 ProjectSignificance::Important
212 }
213 }
214 FilePurpose::Test | FilePurpose::Documentation | FilePurpose::Tooling => {
215 ProjectSignificance::Routine
216 }
217 FilePurpose::Build => ProjectSignificance::Important,
218 }
219}
220
221fn is_config_file(path_str: &str, file_name: &str) -> bool {
223 let config_patterns = [
224 ".toml",
225 ".json",
226 ".yaml",
227 ".yml",
228 ".ini",
229 ".cfg",
230 ".conf",
231 ".env",
232 ".properties",
233 "config",
234 "settings",
235 "options",
236 ];
237
238 let config_names = [
239 "cargo.toml",
240 "package.json",
241 "pyproject.toml",
242 "go.mod",
243 "pom.xml",
244 "build.gradle",
245 "makefile",
246 "dockerfile",
247 ".gitignore",
248 ".gitattributes",
249 ];
250
251 config_patterns
252 .iter()
253 .any(|pattern| file_name.contains(pattern))
254 || config_names.contains(&file_name)
255 || path_str.contains("config")
256 || path_str.contains(".github/workflows")
257}
258
259fn is_test_file(path_str: &str, file_name: &str) -> bool {
261 path_str.contains("test")
262 || path_str.contains("spec")
263 || file_name.contains("test")
264 || file_name.contains("spec")
265 || file_name.ends_with("_test.rs")
266 || file_name.ends_with("_test.py")
267 || file_name.ends_with(".test.js")
268 || file_name.ends_with("_test.go")
269}
270
271fn is_documentation_file(path_str: &str, file_name: &str) -> bool {
273 let doc_extensions = [".md", ".rst", ".txt", ".adoc"];
274 let doc_names = ["readme", "changelog", "contributing", "license", "authors"];
275
276 doc_extensions.iter().any(|ext| file_name.ends_with(ext))
277 || doc_names.iter().any(|name| file_name.contains(name))
278 || path_str.contains("doc")
279 || path_str.contains("guide")
280 || path_str.contains("manual")
281}
282
283fn is_build_file(path_str: &str, file_name: &str) -> bool {
285 let build_names = [
286 "makefile",
287 "dockerfile",
288 "build.gradle",
289 "pom.xml",
290 "cmake",
291 "webpack.config",
292 "rollup.config",
293 "vite.config",
294 ];
295
296 build_names.iter().any(|name| file_name.contains(name))
297 || path_str.contains("build")
298 || path_str.contains("scripts")
299 || file_name.ends_with(".sh")
300 || file_name.ends_with(".bat")
301}
302
303fn is_tooling_file(path_str: &str, file_name: &str) -> bool {
305 path_str.contains("tool")
306 || path_str.contains("util")
307 || path_str.contains(".vscode")
308 || path_str.contains(".idea")
309 || file_name.starts_with(".")
310 || file_name.contains("prettier")
311 || file_name.contains("eslint")
312 || file_name.contains("clippy")
313}
314
315fn is_interface_file(path_str: &str, file_name: &str) -> bool {
317 path_str.contains("api")
318 || path_str.contains("interface")
319 || path_str.contains("proto")
320 || file_name.contains("lib.rs")
321 || file_name.contains("mod.rs")
322 || file_name.contains("index")
323 || file_name.ends_with(".proto")
324 || file_name.ends_with(".graphql")
325}
326
327fn is_critical_file(path_str: &str, file_name: &str) -> bool {
329 let critical_names = [
330 "main.rs",
331 "lib.rs",
332 "index.js",
333 "app.js",
334 "main.py",
335 "__init__.py",
336 "main.go",
337 "main.java",
338 "cargo.toml",
339 "package.json",
340 "go.mod",
341 "pom.xml",
342 ];
343
344 critical_names.contains(&file_name)
345 || (path_str.contains("src") && (file_name == "lib.rs" || file_name == "main.rs"))
346}