1#[allow(unused_imports)]
2use crate::analyzer::{AnalysisConfig, DetectedTechnology, DetectedLanguage, EntryPoint, EnvVar, Port, Protocol, ProjectType, BuildScript, TechnologyCategory, LibraryType};
3use crate::error::{Result, AnalysisError};
4use crate::common::file_utils::{read_file_safe, is_readable_file};
5use std::path::{Path, PathBuf};
6use std::collections::{HashSet, HashMap};
7use regex::Regex;
8use serde_json::Value;
9
10pub struct ProjectContext {
12 pub entry_points: Vec<EntryPoint>,
13 pub ports: Vec<Port>,
14 pub environment_variables: Vec<EnvVar>,
15 pub project_type: ProjectType,
16 pub build_scripts: Vec<BuildScript>,
17}
18
19fn create_regex(pattern: &str) -> Result<Regex> {
21 Regex::new(pattern)
22 .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex pattern '{}': {}", pattern, e)).into())
23}
24
25pub fn analyze_context(
27 project_root: &Path,
28 languages: &[DetectedLanguage],
29 technologies: &[DetectedTechnology],
30 config: &AnalysisConfig,
31) -> Result<ProjectContext> {
32 log::info!("Analyzing project context");
33
34 let mut entry_points = Vec::new();
35 let mut ports = HashSet::new();
36 let mut env_vars = HashMap::new();
37 let mut build_scripts = Vec::new();
38
39 for language in languages {
41 match language.name.as_str() {
42 "JavaScript" | "TypeScript" => {
43 analyze_node_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
44 }
45 "Python" => {
46 analyze_python_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
47 }
48 "Rust" => {
49 analyze_rust_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
50 }
51 "Go" => {
52 analyze_go_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
53 }
54 "Java" | "Kotlin" => {
55 analyze_jvm_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
56 }
57 _ => {}
58 }
59 }
60
61 analyze_docker_files(project_root, &mut ports, &mut env_vars)?;
63 analyze_env_files(project_root, &mut env_vars)?;
64 analyze_makefile(project_root, &mut build_scripts)?;
65
66 for technology in technologies {
68 analyze_technology_specifics(technology, project_root, &mut entry_points, &mut ports)?;
69 }
70
71 let microservices = detect_microservices_structure(project_root)?;
73
74 let ports_vec: Vec<Port> = ports.iter().cloned().collect();
76 let project_type = determine_project_type_with_structure(
77 languages,
78 technologies,
79 &entry_points,
80 &ports_vec,
81 µservices
82 );
83
84 let ports: Vec<Port> = ports.into_iter().collect();
86 let environment_variables: Vec<EnvVar> = env_vars.into_iter()
87 .map(|(name, (default, required, desc))| EnvVar {
88 name,
89 default_value: default,
90 required,
91 description: desc,
92 })
93 .collect();
94
95 Ok(ProjectContext {
96 entry_points,
97 ports,
98 environment_variables,
99 project_type,
100 build_scripts,
101 })
102}
103
104#[derive(Debug)]
106struct MicroserviceInfo {
107 name: String,
108 has_db: bool,
109 has_api: bool,
110}
111
112fn detect_microservices_structure(project_root: &Path) -> Result<Vec<MicroserviceInfo>> {
114 let mut microservices = Vec::new();
115
116 let service_indicators = ["api", "service", "encore.service.ts", "main.ts", "main.go", "main.py"];
118 let db_indicators = ["db", "database", "migrations", "schema", "models"];
119
120 if let Ok(entries) = std::fs::read_dir(project_root) {
122 for entry in entries.flatten() {
123 if entry.file_type()?.is_dir() {
124 let dir_name = entry.file_name().to_string_lossy().to_string();
125 let dir_path = entry.path();
126
127 if dir_name.starts_with('.') ||
129 ["node_modules", "target", "dist", "build", "__pycache__", "vendor"].contains(&dir_name.as_str()) {
130 continue;
131 }
132
133 let mut has_api = false;
135 let mut has_db = false;
136
137 if let Ok(sub_entries) = std::fs::read_dir(&dir_path) {
138 for sub_entry in sub_entries.flatten() {
139 let sub_name = sub_entry.file_name().to_string_lossy().to_string();
140
141 if service_indicators.iter().any(|&ind| sub_name.contains(ind)) {
143 has_api = true;
144 }
145
146 if db_indicators.iter().any(|&ind| sub_name.contains(ind)) {
148 has_db = true;
149 }
150 }
151 }
152
153 if has_api || has_db {
155 microservices.push(MicroserviceInfo {
156 name: dir_name,
157 has_db,
158 has_api,
159 });
160 }
161 }
162 }
163 }
164
165 Ok(microservices)
166}
167
168fn determine_project_type_with_structure(
170 languages: &[DetectedLanguage],
171 technologies: &[DetectedTechnology],
172 entry_points: &[EntryPoint],
173 ports: &[Port],
174 microservices: &[MicroserviceInfo],
175) -> ProjectType {
176 let services_with_db = microservices.iter().filter(|s| s.has_db).count();
178 if services_with_db >= 2 || microservices.len() >= 3 {
179 return ProjectType::Microservice;
180 }
181
182 determine_project_type(languages, technologies, entry_points, ports)
184}
185
186fn analyze_node_project(
188 root: &Path,
189 entry_points: &mut Vec<EntryPoint>,
190 ports: &mut HashSet<Port>,
191 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
192 build_scripts: &mut Vec<BuildScript>,
193 config: &AnalysisConfig,
194) -> Result<()> {
195 let package_json_path = root.join("package.json");
196
197 if is_readable_file(&package_json_path) {
198 let content = read_file_safe(&package_json_path, config.max_file_size)?;
199 let package_json: Value = serde_json::from_str(&content)?;
200
201 if let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object()) {
203 for (name, command) in scripts {
204 if let Some(cmd) = command.as_str() {
205 build_scripts.push(BuildScript {
206 name: name.clone(),
207 command: cmd.to_string(),
208 description: get_script_description(name),
209 is_default: name == "start" || name == "dev",
210 });
211
212 extract_ports_from_command(cmd, ports);
214 }
215 }
216 }
217
218 if let Some(main) = package_json.get("main").and_then(|m| m.as_str()) {
220 entry_points.push(EntryPoint {
221 file: root.join(main),
222 function: None,
223 command: Some(format!("node {}", main)),
224 });
225 }
226
227 let common_entries = ["index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts", "main.js", "main.ts"];
229 for entry in &common_entries {
230 let path = root.join(entry);
231 if is_readable_file(&path) {
232 scan_js_file_for_context(&path, entry_points, ports, env_vars, config)?;
233 }
234 }
235
236 let src_dir = root.join("src");
238 if src_dir.is_dir() {
239 for entry in &common_entries {
240 let path = src_dir.join(entry);
241 if is_readable_file(&path) {
242 scan_js_file_for_context(&path, entry_points, ports, env_vars, config)?;
243 }
244 }
245 }
246 }
247
248 Ok(())
249}
250
251fn scan_js_file_for_context(
253 path: &Path,
254 entry_points: &mut Vec<EntryPoint>,
255 ports: &mut HashSet<Port>,
256 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
257 config: &AnalysisConfig,
258) -> Result<()> {
259 let content = read_file_safe(path, config.max_file_size)?;
260
261 let port_regex = Regex::new(r"(?:PORT|port)\s*[=:]\s*(?:process\.env\.PORT\s*\|\|\s*)?(\d{1,5})")
263 .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
264 for cap in port_regex.captures_iter(&content) {
265 if let Some(port_str) = cap.get(1) {
266 if let Ok(port) = port_str.as_str().parse::<u16>() {
267 ports.insert(Port {
268 number: port,
269 protocol: Protocol::Http,
270 description: Some("HTTP server port".to_string()),
271 });
272 }
273 }
274 }
275
276 let listen_regex = Regex::new(r"\.listen\s*\(\s*(\d{1,5})")
278 .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
279 for cap in listen_regex.captures_iter(&content) {
280 if let Some(port_str) = cap.get(1) {
281 if let Ok(port) = port_str.as_str().parse::<u16>() {
282 ports.insert(Port {
283 number: port,
284 protocol: Protocol::Http,
285 description: Some("Express/HTTP server".to_string()),
286 });
287 }
288 }
289 }
290
291 let env_regex = Regex::new(r"process\.env\.([A-Z_][A-Z0-9_]*)")
293 .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
294 for cap in env_regex.captures_iter(&content) {
295 if let Some(var_name) = cap.get(1) {
296 let name = var_name.as_str().to_string();
297 if !name.starts_with("NODE_") { env_vars.entry(name.clone()).or_insert((None, false, None));
299 }
300 }
301 }
302
303 if content.contains("encore.dev") {
305 let encore_patterns = [
307 (r#"secret\s*\(\s*['"]([A-Z_][A-Z0-9_]*)['"]"#, "Encore secret configuration"),
308 (r#"SQLDatabase\s*\(\s*['"](\w+)['"]"#, "Encore database"),
309 ];
310
311 for (pattern, description) in &encore_patterns {
312 let regex = Regex::new(pattern).unwrap_or_else(|_| Regex::new(r"").unwrap());
313 for cap in regex.captures_iter(&content) {
314 if let Some(match_str) = cap.get(1) {
315 let name = match_str.as_str();
316 if pattern.contains("secret") {
317 env_vars.entry(name.to_string())
318 .or_insert((None, true, Some(description.to_string())));
319 }
320 }
321 }
322 }
323 }
324
325 if content.contains("createServer") || content.contains(".listen(") ||
327 content.contains("app.listen") || content.contains("server.listen") ||
328 content.contains("encore.dev") && content.contains("api.") {
329 entry_points.push(EntryPoint {
330 file: path.to_path_buf(),
331 function: Some("main".to_string()),
332 command: Some(format!("node {}", path.file_name().unwrap().to_string_lossy())),
333 });
334 }
335
336 Ok(())
337}
338
339fn analyze_python_project(
341 root: &Path,
342 entry_points: &mut Vec<EntryPoint>,
343 ports: &mut HashSet<Port>,
344 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
345 build_scripts: &mut Vec<BuildScript>,
346 config: &AnalysisConfig,
347) -> Result<()> {
348 let common_entries = ["main.py", "app.py", "wsgi.py", "asgi.py", "manage.py", "run.py", "__main__.py"];
350
351 for entry in &common_entries {
352 let path = root.join(entry);
353 if is_readable_file(&path) {
354 scan_python_file_for_context(&path, entry_points, ports, env_vars, config)?;
355 }
356 }
357
358 let setup_py = root.join("setup.py");
360 if is_readable_file(&setup_py) {
361 let content = read_file_safe(&setup_py, config.max_file_size)?;
362
363 let console_regex = create_regex(r#"console_scripts['"]\s*:\s*\[(.*?)\]"#)?;
365 if let Some(cap) = console_regex.captures(&content) {
366 if let Some(scripts) = cap.get(1) {
367 let script_regex = create_regex(r#"['"](\w+)\s*=\s*([\w\.]+):(\w+)"#)?;
368 for script_cap in script_regex.captures_iter(scripts.as_str()) {
369 if let (Some(name), Some(module), Some(func)) =
370 (script_cap.get(1), script_cap.get(2), script_cap.get(3)) {
371 entry_points.push(EntryPoint {
372 file: PathBuf::from(format!("{}.py", module.as_str().replace('.', "/"))),
373 function: Some(func.as_str().to_string()),
374 command: Some(name.as_str().to_string()),
375 });
376 }
377 }
378 }
379 }
380 }
381
382 let pyproject = root.join("pyproject.toml");
384 if is_readable_file(&pyproject) {
385 let content = read_file_safe(&pyproject, config.max_file_size)?;
386 if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
387 if let Some(scripts) = toml_value.get("tool")
389 .and_then(|t| t.get("poetry"))
390 .and_then(|p| p.get("scripts"))
391 .and_then(|s| s.as_table()) {
392 for (name, cmd) in scripts {
393 if let Some(command) = cmd.as_str() {
394 build_scripts.push(BuildScript {
395 name: name.clone(),
396 command: command.to_string(),
397 description: None,
398 is_default: name == "start" || name == "run",
399 });
400 }
401 }
402 }
403 }
404 }
405
406 build_scripts.push(BuildScript {
408 name: "install".to_string(),
409 command: "pip install -r requirements.txt".to_string(),
410 description: Some("Install dependencies".to_string()),
411 is_default: false,
412 });
413
414 Ok(())
415}
416
417fn scan_python_file_for_context(
419 path: &Path,
420 entry_points: &mut Vec<EntryPoint>,
421 ports: &mut HashSet<Port>,
422 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
423 config: &AnalysisConfig,
424) -> Result<()> {
425 let content = read_file_safe(path, config.max_file_size)?;
426
427 let port_patterns = [
429 r"port\s*=\s*(\d{1,5})",
430 r"PORT\s*=\s*(\d{1,5})",
431 r"\.run\s*\([^)]*port\s*=\s*(\d{1,5})",
432 r"uvicorn\.run\s*\([^)]*port\s*=\s*(\d{1,5})",
433 ];
434
435 for pattern in &port_patterns {
436 let regex = create_regex(pattern)?;
437 for cap in regex.captures_iter(&content) {
438 if let Some(port_str) = cap.get(1) {
439 if let Ok(port) = port_str.as_str().parse::<u16>() {
440 ports.insert(Port {
441 number: port,
442 protocol: Protocol::Http,
443 description: Some("Python web server".to_string()),
444 });
445 }
446 }
447 }
448 }
449
450 let env_patterns = [
452 r#"os\.environ\.get\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']"#,
453 r#"os\.environ\s*\[\s*["']([A-Z_][A-Z0-9_]*)["']\s*\]"#,
454 r#"os\.getenv\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']"#,
455 ];
456
457 for pattern in &env_patterns {
458 let regex = create_regex(pattern)?;
459 for cap in regex.captures_iter(&content) {
460 if let Some(var_name) = cap.get(1) {
461 let name = var_name.as_str().to_string();
462 env_vars.entry(name.clone()).or_insert((None, false, None));
463 }
464 }
465 }
466
467 if content.contains("if __name__ == '__main__':") ||
469 content.contains("if __name__ == \"__main__\":") {
470 entry_points.push(EntryPoint {
471 file: path.to_path_buf(),
472 function: Some("main".to_string()),
473 command: Some(format!("python {}", path.file_name().unwrap().to_string_lossy())),
474 });
475 }
476
477 Ok(())
478}
479
480fn analyze_rust_project(
482 root: &Path,
483 entry_points: &mut Vec<EntryPoint>,
484 ports: &mut HashSet<Port>,
485 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
486 build_scripts: &mut Vec<BuildScript>,
487 config: &AnalysisConfig,
488) -> Result<()> {
489 let cargo_toml = root.join("Cargo.toml");
490
491 if is_readable_file(&cargo_toml) {
492 let content = read_file_safe(&cargo_toml, config.max_file_size)?;
493 if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
494 if let Some(bins) = toml_value.get("bin").and_then(|b| b.as_array()) {
496 for bin in bins {
497 if let Some(name) = bin.get("name").and_then(|n| n.as_str()) {
498 let path = bin.get("path")
499 .and_then(|p| p.as_str())
500 .map(PathBuf::from)
501 .unwrap_or_else(|| root.join("src").join("bin").join(format!("{}.rs", name)));
502
503 entry_points.push(EntryPoint {
504 file: path,
505 function: Some("main".to_string()),
506 command: Some(format!("cargo run --bin {}", name)),
507 });
508 }
509 }
510 }
511
512 if let Some(_package_name) = toml_value.get("package")
514 .and_then(|p| p.get("name"))
515 .and_then(|n| n.as_str()) {
516 let main_rs = root.join("src").join("main.rs");
517 if is_readable_file(&main_rs) {
518 entry_points.push(EntryPoint {
519 file: main_rs.clone(),
520 function: Some("main".to_string()),
521 command: Some("cargo run".to_string()),
522 });
523
524 scan_rust_file_for_context(&main_rs, ports, env_vars, config)?;
526 }
527 }
528 }
529 }
530
531 build_scripts.extend(vec![
533 BuildScript {
534 name: "build".to_string(),
535 command: "cargo build".to_string(),
536 description: Some("Build the project".to_string()),
537 is_default: false,
538 },
539 BuildScript {
540 name: "build-release".to_string(),
541 command: "cargo build --release".to_string(),
542 description: Some("Build optimized release version".to_string()),
543 is_default: false,
544 },
545 BuildScript {
546 name: "test".to_string(),
547 command: "cargo test".to_string(),
548 description: Some("Run tests".to_string()),
549 is_default: false,
550 },
551 BuildScript {
552 name: "run".to_string(),
553 command: "cargo run".to_string(),
554 description: Some("Run the application".to_string()),
555 is_default: true,
556 },
557 ]);
558
559 Ok(())
560}
561
562fn scan_rust_file_for_context(
564 path: &Path,
565 ports: &mut HashSet<Port>,
566 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
567 config: &AnalysisConfig,
568) -> Result<()> {
569 let content = read_file_safe(path, config.max_file_size)?;
570
571 let port_patterns = [
573 r#"bind\s*\(\s*"[^"]*:(\d{1,5})"\s*\)"#,
574 r#"bind\s*\(\s*\([^,]+,\s*(\d{1,5})\)\s*\)"#,
575 r#"listen\s*\(\s*"[^"]*:(\d{1,5})"\s*\)"#,
576 r"PORT[^=]*=\s*(\d{1,5})",
577 ];
578
579 for pattern in &port_patterns {
580 let regex = create_regex(pattern)?;
581 for cap in regex.captures_iter(&content) {
582 if let Some(port_str) = cap.get(1) {
583 if let Ok(port) = port_str.as_str().parse::<u16>() {
584 ports.insert(Port {
585 number: port,
586 protocol: Protocol::Http,
587 description: Some("Rust web server".to_string()),
588 });
589 }
590 }
591 }
592 }
593
594 let env_patterns = [
596 r#"env::var\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
597 r#"std::env::var\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
598 r#"env!\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
599 ];
600
601 for pattern in &env_patterns {
602 let regex = create_regex(pattern)?;
603 for cap in regex.captures_iter(&content) {
604 if let Some(var_name) = cap.get(1) {
605 let name = var_name.as_str().to_string();
606 env_vars.entry(name.clone()).or_insert((None, false, None));
607 }
608 }
609 }
610
611 Ok(())
612}
613
614fn analyze_go_project(
616 root: &Path,
617 entry_points: &mut Vec<EntryPoint>,
618 ports: &mut HashSet<Port>,
619 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
620 build_scripts: &mut Vec<BuildScript>,
621 config: &AnalysisConfig,
622) -> Result<()> {
623 let main_go = root.join("main.go");
625 if is_readable_file(&main_go) {
626 entry_points.push(EntryPoint {
627 file: main_go.clone(),
628 function: Some("main".to_string()),
629 command: Some("go run main.go".to_string()),
630 });
631
632 scan_go_file_for_context(&main_go, ports, env_vars, config)?;
633 }
634
635 let cmd_dir = root.join("cmd");
637 if cmd_dir.is_dir() {
638 if let Ok(entries) = std::fs::read_dir(&cmd_dir) {
639 for entry in entries.flatten() {
640 if entry.file_type()?.is_dir() {
641 let main_file = entry.path().join("main.go");
642 if is_readable_file(&main_file) {
643 let cmd_name = entry.file_name().to_string_lossy().to_string();
644 entry_points.push(EntryPoint {
645 file: main_file.clone(),
646 function: Some("main".to_string()),
647 command: Some(format!("go run ./cmd/{}", cmd_name)),
648 });
649
650 scan_go_file_for_context(&main_file, ports, env_vars, config)?;
651 }
652 }
653 }
654 }
655 }
656
657 build_scripts.extend(vec![
659 BuildScript {
660 name: "build".to_string(),
661 command: "go build".to_string(),
662 description: Some("Build the project".to_string()),
663 is_default: false,
664 },
665 BuildScript {
666 name: "test".to_string(),
667 command: "go test ./...".to_string(),
668 description: Some("Run tests".to_string()),
669 is_default: false,
670 },
671 BuildScript {
672 name: "run".to_string(),
673 command: "go run .".to_string(),
674 description: Some("Run the application".to_string()),
675 is_default: true,
676 },
677 BuildScript {
678 name: "mod-download".to_string(),
679 command: "go mod download".to_string(),
680 description: Some("Download dependencies".to_string()),
681 is_default: false,
682 },
683 ]);
684
685 Ok(())
686}
687
688fn scan_go_file_for_context(
690 path: &Path,
691 ports: &mut HashSet<Port>,
692 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
693 config: &AnalysisConfig,
694) -> Result<()> {
695 let content = read_file_safe(path, config.max_file_size)?;
696
697 let port_patterns = [
699 r#"Listen\s*\(\s*":(\d{1,5})"\s*\)"#,
700 r#"ListenAndServe\s*\(\s*":(\d{1,5})"\s*,"#,
701 r#"Addr:\s*":(\d{1,5})""#,
702 r"PORT[^=]*=\s*(\d{1,5})",
703 ];
704
705 for pattern in &port_patterns {
706 let regex = create_regex(pattern)?;
707 for cap in regex.captures_iter(&content) {
708 if let Some(port_str) = cap.get(1) {
709 if let Ok(port) = port_str.as_str().parse::<u16>() {
710 ports.insert(Port {
711 number: port,
712 protocol: Protocol::Http,
713 description: Some("Go web server".to_string()),
714 });
715 }
716 }
717 }
718 }
719
720 let env_patterns = [
722 r#"os\.Getenv\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
723 r#"os\.LookupEnv\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
724 ];
725
726 for pattern in &env_patterns {
727 let regex = create_regex(pattern)?;
728 for cap in regex.captures_iter(&content) {
729 if let Some(var_name) = cap.get(1) {
730 let name = var_name.as_str().to_string();
731 env_vars.entry(name.clone()).or_insert((None, false, None));
732 }
733 }
734 }
735
736 Ok(())
737}
738
739fn analyze_jvm_project(
741 root: &Path,
742 _entry_points: &mut Vec<EntryPoint>,
743 ports: &mut HashSet<Port>,
744 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
745 build_scripts: &mut Vec<BuildScript>,
746 config: &AnalysisConfig,
747) -> Result<()> {
748 let pom_xml = root.join("pom.xml");
750 if is_readable_file(&pom_xml) {
751 build_scripts.extend(vec![
752 BuildScript {
753 name: "build".to_string(),
754 command: "mvn clean package".to_string(),
755 description: Some("Build with Maven".to_string()),
756 is_default: false,
757 },
758 BuildScript {
759 name: "test".to_string(),
760 command: "mvn test".to_string(),
761 description: Some("Run tests".to_string()),
762 is_default: false,
763 },
764 BuildScript {
765 name: "run".to_string(),
766 command: "mvn spring-boot:run".to_string(),
767 description: Some("Run Spring Boot application".to_string()),
768 is_default: true,
769 },
770 ]);
771 }
772
773 let gradle_files = ["build.gradle", "build.gradle.kts"];
775 for gradle_file in &gradle_files {
776 if is_readable_file(&root.join(gradle_file)) {
777 build_scripts.extend(vec![
778 BuildScript {
779 name: "build".to_string(),
780 command: "./gradlew build".to_string(),
781 description: Some("Build with Gradle".to_string()),
782 is_default: false,
783 },
784 BuildScript {
785 name: "test".to_string(),
786 command: "./gradlew test".to_string(),
787 description: Some("Run tests".to_string()),
788 is_default: false,
789 },
790 BuildScript {
791 name: "run".to_string(),
792 command: "./gradlew bootRun".to_string(),
793 description: Some("Run Spring Boot application".to_string()),
794 is_default: true,
795 },
796 ]);
797 break;
798 }
799 }
800
801 let app_props_locations = [
803 "src/main/resources/application.properties",
804 "src/main/resources/application.yml",
805 "src/main/resources/application.yaml",
806 ];
807
808 for props_path in &app_props_locations {
809 let full_path = root.join(props_path);
810 if is_readable_file(&full_path) {
811 analyze_application_properties(&full_path, ports, env_vars, config)?;
812 }
813 }
814
815 Ok(())
816}
817
818fn analyze_application_properties(
820 path: &Path,
821 ports: &mut HashSet<Port>,
822 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
823 config: &AnalysisConfig,
824) -> Result<()> {
825 let content = read_file_safe(path, config.max_file_size)?;
826
827 let port_regex = create_regex(r"server\.port\s*[=:]\s*(\d{1,5})")?;
829 for cap in port_regex.captures_iter(&content) {
830 if let Some(port_str) = cap.get(1) {
831 if let Ok(port) = port_str.as_str().parse::<u16>() {
832 ports.insert(Port {
833 number: port,
834 protocol: Protocol::Http,
835 description: Some("Spring Boot server".to_string()),
836 });
837 }
838 }
839 }
840
841 let env_regex = create_regex(r"\$\{([A-Z_][A-Z0-9_]*)\}")?;
843 for cap in env_regex.captures_iter(&content) {
844 if let Some(var_name) = cap.get(1) {
845 let name = var_name.as_str().to_string();
846 env_vars.entry(name.clone()).or_insert((None, false, None));
847 }
848 }
849
850 Ok(())
851}
852
853fn analyze_docker_files(
855 root: &Path,
856 ports: &mut HashSet<Port>,
857 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
858) -> Result<()> {
859 let dockerfile = root.join("Dockerfile");
860
861 if is_readable_file(&dockerfile) {
862 let content = std::fs::read_to_string(&dockerfile)?;
863
864 let expose_regex = create_regex(r"EXPOSE\s+(\d{1,5})(?:/(\w+))?")?;
866 for cap in expose_regex.captures_iter(&content) {
867 if let Some(port_str) = cap.get(1) {
868 if let Ok(port) = port_str.as_str().parse::<u16>() {
869 let protocol = cap.get(2)
870 .and_then(|p| match p.as_str().to_lowercase().as_str() {
871 "tcp" => Some(Protocol::Tcp),
872 "udp" => Some(Protocol::Udp),
873 _ => None,
874 })
875 .unwrap_or(Protocol::Tcp);
876
877 ports.insert(Port {
878 number: port,
879 protocol,
880 description: Some("Exposed in Dockerfile".to_string()),
881 });
882 }
883 }
884 }
885
886 let env_regex = create_regex(r"ENV\s+([A-Z_][A-Z0-9_]*)\s+(.+)")?;
888 for cap in env_regex.captures_iter(&content) {
889 if let (Some(name), Some(value)) = (cap.get(1), cap.get(2)) {
890 let var_name = name.as_str().to_string();
891 let var_value = value.as_str().trim().to_string();
892 env_vars.entry(var_name).or_insert((Some(var_value), false, None));
893 }
894 }
895 }
896
897 let compose_files = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
899 for compose_file in &compose_files {
900 let path = root.join(compose_file);
901 if is_readable_file(&path) {
902 analyze_docker_compose(&path, ports, env_vars)?;
903 break;
904 }
905 }
906
907 Ok(())
908}
909
910fn analyze_docker_compose(
912 path: &Path,
913 ports: &mut HashSet<Port>,
914 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
915) -> Result<()> {
916 let content = std::fs::read_to_string(path)?;
917 let value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| AnalysisError::InvalidStructure(format!("Invalid YAML: {}", e)))?;
918
919 if let Some(services) = value.get("services").and_then(|s| s.as_mapping()) {
920 for (service_name, service) in services {
921 let service_name_str = service_name.as_str().unwrap_or("unknown");
922
923 let service_type = determine_service_type(service_name_str, service);
925
926 if let Some(service_ports) = service.get("ports").and_then(|p| p.as_sequence()) {
928 for port_entry in service_ports {
929 if let Some(port_str) = port_entry.as_str() {
930 let parts: Vec<&str> = port_str.split(':').collect();
932
933 let (external_port, internal_port, protocol_suffix) = if parts.len() >= 2 {
934 let external = parts[0].trim();
936 let internal_parts: Vec<&str> = parts[1].split('/').collect();
937 let internal = internal_parts[0].trim();
938 let protocol = internal_parts.get(1).map(|p| p.trim());
939 (external, internal, protocol)
940 } else {
941 let port_parts: Vec<&str> = parts[0].split('/').collect();
943 let port = port_parts[0].trim();
944 let protocol = port_parts.get(1).map(|p| p.trim());
945 (port, port, protocol)
946 };
947
948 let protocol = match protocol_suffix {
950 Some("udp") => Protocol::Udp,
951 _ => Protocol::Tcp,
952 };
953
954 if let Ok(port) = external_port.parse::<u16>() {
956 let description = create_port_description(&service_type, service_name_str, external_port, internal_port);
957
958 ports.insert(Port {
959 number: port,
960 protocol,
961 description: Some(description),
962 });
963 }
964 }
965 }
966 }
967
968 if let Some(env) = service.get("environment") {
970 let env_context = format!(" ({})", service_type.as_str());
971
972 if let Some(env_map) = env.as_mapping() {
973 for (key, value) in env_map {
974 if let Some(key_str) = key.as_str() {
975 let val_str = value.as_str().map(|s| s.to_string());
976 let description = get_env_var_description(key_str, &service_type);
977 env_vars.entry(key_str.to_string())
978 .or_insert((val_str, false, description.or_else(|| Some(env_context.clone()))));
979 }
980 }
981 } else if let Some(env_list) = env.as_sequence() {
982 for item in env_list {
983 if let Some(env_str) = item.as_str() {
984 if let Some(eq_pos) = env_str.find('=') {
985 let (key, value) = env_str.split_at(eq_pos);
986 let value = &value[1..]; let description = get_env_var_description(key, &service_type);
988 env_vars.entry(key.to_string())
989 .or_insert((Some(value.to_string()), false, description.or_else(|| Some(env_context.clone()))));
990 }
991 }
992 }
993 }
994 }
995 }
996 }
997
998 Ok(())
999}
1000
1001#[derive(Debug, Clone)]
1003enum ServiceType {
1004 PostgreSQL,
1005 MySQL,
1006 MongoDB,
1007 Redis,
1008 RabbitMQ,
1009 Kafka,
1010 Elasticsearch,
1011 Application,
1012 Nginx,
1013 Unknown,
1014}
1015
1016impl ServiceType {
1017 fn as_str(&self) -> &'static str {
1018 match self {
1019 ServiceType::PostgreSQL => "PostgreSQL database",
1020 ServiceType::MySQL => "MySQL database",
1021 ServiceType::MongoDB => "MongoDB database",
1022 ServiceType::Redis => "Redis cache",
1023 ServiceType::RabbitMQ => "RabbitMQ message broker",
1024 ServiceType::Kafka => "Kafka message broker",
1025 ServiceType::Elasticsearch => "Elasticsearch search engine",
1026 ServiceType::Application => "Application service",
1027 ServiceType::Nginx => "Nginx web server",
1028 ServiceType::Unknown => "Service",
1029 }
1030 }
1031}
1032
1033fn determine_service_type(name: &str, service: &serde_yaml::Value) -> ServiceType {
1035 let name_lower = name.to_lowercase();
1036
1037 if name_lower.contains("postgres") || name_lower.contains("pg") || name_lower.contains("psql") {
1039 return ServiceType::PostgreSQL;
1040 } else if name_lower.contains("mysql") || name_lower.contains("mariadb") {
1041 return ServiceType::MySQL;
1042 } else if name_lower.contains("mongo") {
1043 return ServiceType::MongoDB;
1044 } else if name_lower.contains("redis") {
1045 return ServiceType::Redis;
1046 } else if name_lower.contains("rabbit") || name_lower.contains("amqp") {
1047 return ServiceType::RabbitMQ;
1048 } else if name_lower.contains("kafka") {
1049 return ServiceType::Kafka;
1050 } else if name_lower.contains("elastic") || name_lower.contains("es") {
1051 return ServiceType::Elasticsearch;
1052 } else if name_lower.contains("nginx") || name_lower.contains("proxy") {
1053 return ServiceType::Nginx;
1054 }
1055
1056 if let Some(image) = service.get("image").and_then(|i| i.as_str()) {
1058 let image_lower = image.to_lowercase();
1059 if image_lower.contains("postgres") {
1060 return ServiceType::PostgreSQL;
1061 } else if image_lower.contains("mysql") || image_lower.contains("mariadb") {
1062 return ServiceType::MySQL;
1063 } else if image_lower.contains("mongo") {
1064 return ServiceType::MongoDB;
1065 } else if image_lower.contains("redis") {
1066 return ServiceType::Redis;
1067 } else if image_lower.contains("rabbitmq") {
1068 return ServiceType::RabbitMQ;
1069 } else if image_lower.contains("kafka") {
1070 return ServiceType::Kafka;
1071 } else if image_lower.contains("elastic") {
1072 return ServiceType::Elasticsearch;
1073 } else if image_lower.contains("nginx") {
1074 return ServiceType::Nginx;
1075 }
1076 }
1077
1078 if let Some(env) = service.get("environment") {
1080 if let Some(env_map) = env.as_mapping() {
1081 for (key, _) in env_map {
1082 if let Some(key_str) = key.as_str() {
1083 if key_str.contains("POSTGRES") || key_str.contains("PGPASSWORD") {
1084 return ServiceType::PostgreSQL;
1085 } else if key_str.contains("MYSQL") {
1086 return ServiceType::MySQL;
1087 } else if key_str.contains("MONGO") {
1088 return ServiceType::MongoDB;
1089 }
1090 }
1091 }
1092 }
1093 }
1094
1095 if service.get("build").is_some() {
1097 return ServiceType::Application;
1098 }
1099
1100 ServiceType::Unknown
1101}
1102
1103fn create_port_description(service_type: &ServiceType, service_name: &str, external: &str, internal: &str) -> String {
1105 let base_desc = match service_type {
1106 ServiceType::PostgreSQL => format!("PostgreSQL database ({})", service_name),
1107 ServiceType::MySQL => format!("MySQL database ({})", service_name),
1108 ServiceType::MongoDB => format!("MongoDB database ({})", service_name),
1109 ServiceType::Redis => format!("Redis cache ({})", service_name),
1110 ServiceType::RabbitMQ => format!("RabbitMQ message broker ({})", service_name),
1111 ServiceType::Kafka => format!("Kafka message broker ({})", service_name),
1112 ServiceType::Elasticsearch => format!("Elasticsearch ({})", service_name),
1113 ServiceType::Nginx => format!("Nginx proxy ({})", service_name),
1114 ServiceType::Application => format!("Application service ({})", service_name),
1115 ServiceType::Unknown => format!("Docker service ({})", service_name),
1116 };
1117
1118 if external != internal {
1119 format!("{} - external:{}, internal:{}", base_desc, external, internal)
1120 } else {
1121 format!("{} - port {}", base_desc, external)
1122 }
1123}
1124
1125fn get_env_var_description(var_name: &str, service_type: &ServiceType) -> Option<String> {
1127 match var_name {
1128 "POSTGRES_PASSWORD" | "POSTGRES_USER" | "POSTGRES_DB" =>
1129 Some("PostgreSQL configuration".to_string()),
1130 "MYSQL_ROOT_PASSWORD" | "MYSQL_PASSWORD" | "MYSQL_USER" | "MYSQL_DATABASE" =>
1131 Some("MySQL configuration".to_string()),
1132 "MONGO_INITDB_ROOT_USERNAME" | "MONGO_INITDB_ROOT_PASSWORD" =>
1133 Some("MongoDB configuration".to_string()),
1134 "REDIS_PASSWORD" => Some("Redis configuration".to_string()),
1135 "RABBITMQ_DEFAULT_USER" | "RABBITMQ_DEFAULT_PASS" =>
1136 Some("RabbitMQ configuration".to_string()),
1137 "DATABASE_URL" | "DB_CONNECTION_STRING" =>
1138 Some("Database connection string".to_string()),
1139 "GOOGLE_APPLICATION_CREDENTIALS" =>
1140 Some("Google Cloud service account credentials".to_string()),
1141 _ => None,
1142 }
1143}
1144
1145fn analyze_env_files(
1147 root: &Path,
1148 env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
1149) -> Result<()> {
1150 let env_files = [".env", ".env.example", ".env.local", ".env.development", ".env.production"];
1151
1152 for env_file in &env_files {
1153 let path = root.join(env_file);
1154 if is_readable_file(&path) {
1155 let content = std::fs::read_to_string(&path)?;
1156
1157 for line in content.lines() {
1158 let line = line.trim();
1159 if line.is_empty() || line.starts_with('#') {
1160 continue;
1161 }
1162
1163 if let Some(eq_pos) = line.find('=') {
1164 let (key, value) = line.split_at(eq_pos);
1165 let key = key.trim();
1166 let value = value[1..].trim(); let required = value.is_empty() || value == "required" || value == "REQUIRED";
1170 let actual_value = if required { None } else { Some(value.to_string()) };
1171
1172 env_vars.entry(key.to_string()).or_insert((actual_value, required, None));
1173 }
1174 }
1175 }
1176 }
1177
1178 Ok(())
1179}
1180
1181fn analyze_makefile(
1183 root: &Path,
1184 build_scripts: &mut Vec<BuildScript>,
1185) -> Result<()> {
1186 let makefiles = ["Makefile", "makefile"];
1187
1188 for makefile in &makefiles {
1189 let path = root.join(makefile);
1190 if is_readable_file(&path) {
1191 let content = std::fs::read_to_string(&path)?;
1192
1193 let target_regex = create_regex(r"^([a-zA-Z0-9_-]+):\s*(?:[^\n]*)?$")?;
1195 let mut in_recipe = false;
1196 let mut current_target = String::new();
1197 let mut current_command = String::new();
1198
1199 for line in content.lines() {
1200 if let Some(cap) = target_regex.captures(line) {
1201 if !current_target.is_empty() && !current_command.is_empty() {
1203 build_scripts.push(BuildScript {
1204 name: current_target.clone(),
1205 command: format!("make {}", current_target),
1206 description: None,
1207 is_default: current_target == "run" || current_target == "start",
1208 });
1209 }
1210
1211 if let Some(target) = cap.get(1) {
1212 current_target = target.as_str().to_string();
1213 current_command.clear();
1214 in_recipe = true;
1215 }
1216 } else if in_recipe && line.starts_with('\t') {
1217 if current_command.is_empty() {
1218 current_command = line.trim().to_string();
1219 }
1220 } else if !line.trim().is_empty() {
1221 in_recipe = false;
1222 }
1223 }
1224
1225 if !current_target.is_empty() && !current_command.is_empty() {
1227 build_scripts.push(BuildScript {
1228 name: current_target.clone(),
1229 command: format!("make {}", current_target),
1230 description: None,
1231 is_default: current_target == "run" || current_target == "start",
1232 });
1233 }
1234
1235 break;
1236 }
1237 }
1238
1239 Ok(())
1240}
1241
1242fn analyze_technology_specifics(
1244 technology: &DetectedTechnology,
1245 root: &Path,
1246 entry_points: &mut Vec<EntryPoint>,
1247 ports: &mut HashSet<Port>,
1248) -> Result<()> {
1249 match technology.name.as_str() {
1250 "Next.js" => {
1251 ports.insert(Port {
1253 number: 3000,
1254 protocol: Protocol::Http,
1255 description: Some("Next.js development server".to_string()),
1256 });
1257
1258 let pages_dir = root.join("pages");
1260 if pages_dir.is_dir() {
1261 entry_points.push(EntryPoint {
1262 file: pages_dir,
1263 function: None,
1264 command: Some("npm run dev".to_string()),
1265 });
1266 }
1267 }
1268 "Express" | "Fastify" | "Koa" | "Hono" | "Elysia" => {
1269 ports.insert(Port {
1271 number: 3000,
1272 protocol: Protocol::Http,
1273 description: Some(format!("{} server", technology.name)),
1274 });
1275 }
1276 "Encore" => {
1277 ports.insert(Port {
1279 number: 4000,
1280 protocol: Protocol::Http,
1281 description: Some("Encore development server".to_string()),
1282 });
1283 }
1284 "Astro" => {
1285 ports.insert(Port {
1287 number: 4321,
1288 protocol: Protocol::Http,
1289 description: Some("Astro development server".to_string()),
1290 });
1291 }
1292 "SvelteKit" => {
1293 ports.insert(Port {
1295 number: 5173,
1296 protocol: Protocol::Http,
1297 description: Some("SvelteKit development server".to_string()),
1298 });
1299 }
1300 "Nuxt.js" => {
1301 ports.insert(Port {
1303 number: 3000,
1304 protocol: Protocol::Http,
1305 description: Some("Nuxt.js development server".to_string()),
1306 });
1307 }
1308 "Tanstack Start" => {
1309 ports.insert(Port {
1311 number: 3000,
1312 protocol: Protocol::Http,
1313 description: Some(format!("{} development server", technology.name)),
1314 });
1315 }
1316 "React Router v7" => {
1317 ports.insert(Port {
1319 number: 5173,
1320 protocol: Protocol::Http,
1321 description: Some("React Router v7 development server".to_string()),
1322 });
1323 }
1324 "Django" => {
1325 ports.insert(Port {
1326 number: 8000,
1327 protocol: Protocol::Http,
1328 description: Some("Django development server".to_string()),
1329 });
1330 }
1331 "Flask" | "FastAPI" => {
1332 ports.insert(Port {
1333 number: 5000,
1334 protocol: Protocol::Http,
1335 description: Some(format!("{} server", technology.name)),
1336 });
1337 }
1338 "Spring Boot" => {
1339 ports.insert(Port {
1340 number: 8080,
1341 protocol: Protocol::Http,
1342 description: Some("Spring Boot server".to_string()),
1343 });
1344 }
1345 "Actix Web" | "Rocket" => {
1346 ports.insert(Port {
1347 number: 8080,
1348 protocol: Protocol::Http,
1349 description: Some(format!("{} server", technology.name)),
1350 });
1351 }
1352 _ => {}
1353 }
1354
1355 Ok(())
1356}
1357
1358fn extract_ports_from_command(command: &str, ports: &mut HashSet<Port>) {
1360 let patterns = [
1362 r"-p\s+(\d{1,5})",
1363 r"--port\s+(\d{1,5})",
1364 r"--port=(\d{1,5})",
1365 r"PORT=(\d{1,5})",
1366 ];
1367
1368 for pattern in &patterns {
1369 if let Ok(regex) = Regex::new(pattern) {
1370 for cap in regex.captures_iter(command) {
1371 if let Some(port_str) = cap.get(1) {
1372 if let Ok(port) = port_str.as_str().parse::<u16>() {
1373 ports.insert(Port {
1374 number: port,
1375 protocol: Protocol::Http,
1376 description: Some("Port from command".to_string()),
1377 });
1378 }
1379 }
1380 }
1381 }
1382 }
1383}
1384
1385fn get_script_description(name: &str) -> Option<String> {
1387 match name {
1388 "start" => Some("Start the application".to_string()),
1389 "dev" => Some("Start development server".to_string()),
1390 "build" => Some("Build the application".to_string()),
1391 "test" => Some("Run tests".to_string()),
1392 "lint" => Some("Run linter".to_string()),
1393 "format" => Some("Format code".to_string()),
1394 _ => None,
1395 }
1396}
1397
1398fn determine_project_type(
1400 languages: &[DetectedLanguage],
1401 technologies: &[DetectedTechnology],
1402 entry_points: &[EntryPoint],
1403 ports: &[Port],
1404) -> ProjectType {
1405 let has_database_ports = ports.iter().any(|p| {
1407 if let Some(desc) = &p.description {
1408 let desc_lower = desc.to_lowercase();
1409 desc_lower.contains("postgres") || desc_lower.contains("mysql") ||
1410 desc_lower.contains("mongodb") || desc_lower.contains("database")
1411 } else {
1412 false
1413 }
1414 });
1415
1416 let has_multiple_services = ports.iter()
1417 .filter_map(|p| p.description.as_ref())
1418 .filter(|desc| {
1419 let desc_lower = desc.to_lowercase();
1420 desc_lower.contains("service") || desc_lower.contains("application")
1421 })
1422 .count() > 1;
1423
1424 let has_orchestration_framework = technologies.iter()
1425 .any(|t| t.name == "Encore" || t.name == "Dapr" || t.name == "Temporal");
1426
1427 let web_frameworks = ["Express", "Fastify", "Koa", "Next.js", "React", "Vue", "Angular",
1429 "Django", "Flask", "FastAPI", "Spring Boot", "Actix Web", "Rocket",
1430 "Gin", "Echo", "Fiber", "Svelte", "SvelteKit", "SolidJS", "Astro",
1431 "Encore", "Hono", "Elysia", "React Router v7", "Tanstack Start",
1432 "SolidStart", "Qwik", "Nuxt.js", "Gatsby"];
1433
1434 let has_web_framework = technologies.iter()
1435 .any(|t| web_frameworks.contains(&t.name.as_str()));
1436
1437 let cli_indicators = ["cobra", "clap", "argparse", "commander"];
1439 let has_cli_framework = technologies.iter()
1440 .any(|t| cli_indicators.contains(&t.name.to_lowercase().as_str()));
1441
1442 let api_frameworks = ["FastAPI", "Express", "Gin", "Echo", "Actix Web", "Spring Boot",
1444 "Fastify", "Koa", "Nest.js", "Encore", "Hono", "Elysia"];
1445 let has_api_framework = technologies.iter()
1446 .any(|t| api_frameworks.contains(&t.name.as_str()));
1447
1448 let static_generators = ["Gatsby", "Hugo", "Jekyll", "Eleventy", "Astro"];
1450 let has_static_generator = technologies.iter()
1451 .any(|t| static_generators.contains(&t.name.as_str()));
1452
1453 if (has_database_ports || has_multiple_services) && (has_orchestration_framework || has_api_framework) {
1455 ProjectType::Microservice
1456 } else if has_static_generator {
1457 ProjectType::StaticSite
1458 } else if has_api_framework && !has_web_framework {
1459 ProjectType::ApiService
1460 } else if has_web_framework {
1461 ProjectType::WebApplication
1462 } else if has_cli_framework || (entry_points.len() == 1 && ports.is_empty()) {
1463 ProjectType::CliTool
1464 } else if entry_points.is_empty() && ports.is_empty() {
1465 let has_lib_indicators = languages.iter().any(|l| {
1467 match l.name.as_str() {
1468 "Rust" => l.files.iter().any(|f| f.to_string_lossy().contains("lib.rs")),
1469 "Python" => l.files.iter().any(|f| f.to_string_lossy().contains("__init__.py")),
1470 "JavaScript" | "TypeScript" => l.main_dependencies.is_empty(),
1471 _ => false,
1472 }
1473 });
1474
1475 if has_lib_indicators {
1476 ProjectType::Library
1477 } else {
1478 ProjectType::Unknown
1479 }
1480 } else {
1481 ProjectType::Unknown
1482 }
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use super::*;
1488 use crate::analyzer::{TechnologyCategory, LibraryType};
1489 use std::fs;
1490 use tempfile::TempDir;
1491
1492 fn create_test_language(name: &str) -> DetectedLanguage {
1493 DetectedLanguage {
1494 name: name.to_string(),
1495 version: None,
1496 confidence: 0.9,
1497 files: vec![],
1498 main_dependencies: vec![],
1499 dev_dependencies: vec![],
1500 package_manager: None,
1501 }
1502 }
1503
1504 fn create_test_technology(name: &str, category: TechnologyCategory) -> DetectedTechnology {
1505 DetectedTechnology {
1506 name: name.to_string(),
1507 version: None,
1508 category,
1509 confidence: 0.8,
1510 requires: vec![],
1511 conflicts_with: vec![],
1512 is_primary: false,
1513 }
1514 }
1515
1516 #[test]
1517 fn test_node_project_context() {
1518 let temp_dir = TempDir::new().unwrap();
1519 let root = temp_dir.path();
1520
1521 let package_json = r#"{
1523 "name": "test-app",
1524 "main": "index.js",
1525 "scripts": {
1526 "start": "node index.js",
1527 "dev": "nodemon index.js",
1528 "test": "jest",
1529 "build": "webpack"
1530 }
1531 }"#;
1532 fs::write(root.join("package.json"), package_json).unwrap();
1533
1534 let index_js = r#"
1536const express = require('express');
1537const app = express();
1538
1539const PORT = process.env.PORT || 3000;
1540const API_KEY = process.env.API_KEY;
1541const DATABASE_URL = process.env.DATABASE_URL;
1542
1543app.listen(PORT, () => {
1544 console.log(`Server running on port ${PORT}`);
1545});
1546 "#;
1547 fs::write(root.join("index.js"), index_js).unwrap();
1548
1549 let languages = vec![create_test_language("JavaScript")];
1550 let technologies = vec![create_test_technology("Express", TechnologyCategory::BackendFramework)];
1551 let config = AnalysisConfig::default();
1552
1553 let context = analyze_context(root, &languages, &technologies, &config).unwrap();
1554
1555 assert!(!context.entry_points.is_empty());
1557 assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("index.js")));
1558
1559 assert!(!context.ports.is_empty());
1561 assert!(context.ports.iter().any(|p| p.number == 3000));
1562
1563 assert!(context.environment_variables.iter().any(|ev| ev.name == "PORT"));
1565 assert!(context.environment_variables.iter().any(|ev| ev.name == "API_KEY"));
1566 assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1567
1568 assert_eq!(context.build_scripts.len(), 4);
1570 assert!(context.build_scripts.iter().any(|bs| bs.name == "start" && bs.is_default));
1571 assert!(context.build_scripts.iter().any(|bs| bs.name == "dev" && bs.is_default));
1572 assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1573 assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1574
1575 assert_eq!(context.project_type, ProjectType::WebApplication);
1577 }
1578
1579 #[test]
1580 fn test_python_project_context() {
1581 let temp_dir = TempDir::new().unwrap();
1582 let root = temp_dir.path();
1583
1584 let app_py = r#"
1586import os
1587from flask import Flask
1588
1589app = Flask(__name__)
1590
1591PORT = 5000
1592SECRET_KEY = os.environ.get('SECRET_KEY')
1593DEBUG = os.getenv('DEBUG', 'False')
1594
1595if __name__ == '__main__':
1596 app.run(port=PORT)
1597 "#;
1598 fs::write(root.join("app.py"), app_py).unwrap();
1599
1600 let languages = vec![create_test_language("Python")];
1601 let technologies = vec![create_test_technology("Flask", TechnologyCategory::BackendFramework)];
1602 let config = AnalysisConfig::default();
1603
1604 let context = analyze_context(root, &languages, &technologies, &config).unwrap();
1605
1606 assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("app.py")));
1608
1609 assert!(context.ports.iter().any(|p| p.number == 5000));
1611
1612 assert!(context.environment_variables.iter().any(|ev| ev.name == "SECRET_KEY"));
1614 assert!(context.environment_variables.iter().any(|ev| ev.name == "DEBUG"));
1615
1616 assert_eq!(context.project_type, ProjectType::WebApplication);
1618 }
1619
1620 #[test]
1621 fn test_rust_project_context() {
1622 let temp_dir = TempDir::new().unwrap();
1623 let root = temp_dir.path();
1624
1625 let cargo_toml = r#"
1627[package]
1628name = "test-server"
1629version = "0.1.0"
1630
1631[[bin]]
1632name = "server"
1633path = "src/main.rs"
1634 "#;
1635 fs::write(root.join("Cargo.toml"), cargo_toml).unwrap();
1636
1637 fs::create_dir_all(root.join("src")).unwrap();
1639
1640 let main_rs = r#"
1642use std::env;
1643
1644fn main() {
1645 let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
1646 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
1647
1648 println!("Starting server on port {}", port);
1649}
1650 "#;
1651 fs::write(root.join("src/main.rs"), main_rs).unwrap();
1652
1653 let languages = vec![create_test_language("Rust")];
1654 let frameworks = vec![];
1655 let config = AnalysisConfig::default();
1656
1657 let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1658
1659 assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("main.rs")));
1661 assert!(context.entry_points.iter().any(|ep| ep.command == Some("cargo run".to_string())));
1662
1663 assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1665 assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1666 assert!(context.build_scripts.iter().any(|bs| bs.name == "run" && bs.is_default));
1667
1668 assert!(context.environment_variables.iter().any(|ev| ev.name == "PORT"));
1670 assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1671 }
1672
1673 #[test]
1674 fn test_dockerfile_analysis() {
1675 let temp_dir = TempDir::new().unwrap();
1676 let root = temp_dir.path();
1677
1678 let dockerfile = r#"
1680FROM node:14
1681WORKDIR /app
1682
1683ENV NODE_ENV=production
1684ENV PORT=3000
1685
1686EXPOSE 3000
1687EXPOSE 9229/tcp
1688
1689CMD ["node", "server.js"]
1690 "#;
1691 fs::write(root.join("Dockerfile"), dockerfile).unwrap();
1692
1693 let languages = vec![];
1694 let frameworks = vec![];
1695 let config = AnalysisConfig::default();
1696
1697 let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1698
1699 assert!(context.ports.iter().any(|p| p.number == 3000));
1701 assert!(context.ports.iter().any(|p| p.number == 9229 && p.protocol == Protocol::Tcp));
1702
1703 assert!(context.environment_variables.iter().any(|ev|
1705 ev.name == "NODE_ENV" && ev.default_value == Some("production".to_string())
1706 ));
1707 assert!(context.environment_variables.iter().any(|ev|
1708 ev.name == "PORT" && ev.default_value == Some("3000".to_string())
1709 ));
1710 }
1711
1712 #[test]
1713 fn test_docker_compose_analysis() {
1714 let temp_dir = TempDir::new().unwrap();
1715 let root = temp_dir.path();
1716
1717 let compose = r#"
1719version: '3.8'
1720services:
1721 web:
1722 build: .
1723 ports:
1724 - "8080:80"
1725 - "443"
1726 environment:
1727 - DATABASE_URL=postgres://user:pass@db:5432/mydb
1728 - REDIS_URL=redis://cache:6379
1729 db:
1730 image: postgres
1731 ports:
1732 - "5432"
1733 environment:
1734 POSTGRES_PASSWORD: secret
1735 "#;
1736 fs::write(root.join("docker-compose.yml"), compose).unwrap();
1737
1738 let languages = vec![];
1739 let frameworks = vec![];
1740 let config = AnalysisConfig::default();
1741
1742 let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1743
1744 assert!(context.ports.iter().any(|p| p.number == 80));
1746 assert!(context.ports.iter().any(|p| p.number == 443));
1747 assert!(context.ports.iter().any(|p| p.number == 5432));
1748
1749 assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1751 assert!(context.environment_variables.iter().any(|ev| ev.name == "REDIS_URL"));
1752 assert!(context.environment_variables.iter().any(|ev| ev.name == "POSTGRES_PASSWORD"));
1753 }
1754
1755 #[test]
1756 fn test_env_file_analysis() {
1757 let temp_dir = TempDir::new().unwrap();
1758 let root = temp_dir.path();
1759
1760 let env_file = r#"
1762# Database configuration
1763DATABASE_URL=postgresql://localhost:5432/myapp
1764REDIS_URL=redis://localhost:6379
1765
1766# API Keys
1767API_KEY=
1768SECRET_KEY=required
1769
1770# Feature flags
1771ENABLE_FEATURE_X=true
1772DEBUG=false
1773 "#;
1774 fs::write(root.join(".env"), env_file).unwrap();
1775
1776 let languages = vec![];
1777 let frameworks = vec![];
1778 let config = AnalysisConfig::default();
1779
1780 let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1781
1782 assert!(context.environment_variables.iter().any(|ev|
1784 ev.name == "DATABASE_URL" && ev.default_value.is_some()
1785 ));
1786 assert!(context.environment_variables.iter().any(|ev|
1787 ev.name == "API_KEY" && ev.required
1788 ));
1789 assert!(context.environment_variables.iter().any(|ev|
1790 ev.name == "SECRET_KEY" && ev.required
1791 ));
1792 assert!(context.environment_variables.iter().any(|ev|
1793 ev.name == "ENABLE_FEATURE_X" && ev.default_value == Some("true".to_string())
1794 ));
1795 }
1796
1797 #[test]
1798 fn test_makefile_analysis() {
1799 let temp_dir = TempDir::new().unwrap();
1800 let root = temp_dir.path();
1801
1802 let makefile = r#"
1804build:
1805 go build -o app main.go
1806
1807test:
1808 go test ./...
1809
1810run: build
1811 ./app
1812
1813docker-build:
1814 docker build -t myapp .
1815
1816clean:
1817 rm -f app
1818 "#;
1819 fs::write(root.join("Makefile"), makefile).unwrap();
1820
1821 let languages = vec![];
1822 let frameworks = vec![];
1823 let config = AnalysisConfig::default();
1824
1825 let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1826
1827 assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1829 assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1830 assert!(context.build_scripts.iter().any(|bs| bs.name == "run" && bs.is_default));
1831 assert!(context.build_scripts.iter().any(|bs| bs.name == "docker-build"));
1832 assert!(context.build_scripts.iter().any(|bs| bs.name == "clean"));
1833 }
1834
1835 #[test]
1836 fn test_project_type_detection() {
1837 let languages = vec![create_test_language("Rust")];
1839 let technologies = vec![create_test_technology("clap", TechnologyCategory::Library(LibraryType::Other("CLI".to_string())))];
1840 let entry_points = vec![EntryPoint {
1841 file: PathBuf::from("src/main.rs"),
1842 function: Some("main".to_string()),
1843 command: Some("cargo run".to_string()),
1844 }];
1845 let ports = vec![];
1846
1847 let project_type = determine_project_type(&languages, &technologies, &entry_points, &ports);
1848 assert_eq!(project_type, ProjectType::CliTool);
1849
1850 let technologies = vec![create_test_technology("FastAPI", TechnologyCategory::BackendFramework)];
1852 let ports = vec![Port {
1853 number: 8000,
1854 protocol: Protocol::Http,
1855 description: None,
1856 }];
1857
1858 let project_type = determine_project_type(&languages, &technologies, &vec![], &ports);
1859 assert_eq!(project_type, ProjectType::ApiService);
1860
1861 let languages = vec![create_test_language("Python")];
1863 let mut lang = languages[0].clone();
1864 lang.files = vec![PathBuf::from("__init__.py")];
1865 let languages = vec![lang];
1866
1867 let project_type = determine_project_type(&languages, &vec![], &vec![], &vec![]);
1868 assert_eq!(project_type, ProjectType::Library);
1869 }
1870}