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