1use crate::analyzer::{AnalysisConfig, DetectedLanguage};
2use crate::common::file_utils;
3use crate::error::Result;
4use serde_json::Value as JsonValue;
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone)]
10pub struct LanguageInfo {
11 pub name: String,
12 pub version: Option<String>,
13 pub edition: Option<String>,
14 pub package_manager: Option<String>,
15 pub main_dependencies: Vec<String>,
16 pub dev_dependencies: Vec<String>,
17 pub confidence: f32,
18 pub source_files: Vec<PathBuf>,
19 pub manifest_files: Vec<PathBuf>,
20}
21
22pub fn detect_languages(
24 files: &[PathBuf],
25 config: &AnalysisConfig,
26) -> Result<Vec<DetectedLanguage>> {
27 let mut language_info = HashMap::new();
28
29 let mut source_files_by_lang = HashMap::new();
31 let mut manifest_files = Vec::new();
32
33 for file in files {
34 if let Some(extension) = file.extension().and_then(|e| e.to_str()) {
35 match extension {
36 "rs" => source_files_by_lang
38 .entry("rust")
39 .or_insert_with(Vec::new)
40 .push(file.clone()),
41
42 "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => source_files_by_lang
44 .entry("javascript")
45 .or_insert_with(Vec::new)
46 .push(file.clone()),
47
48 "py" | "pyx" | "pyi" => source_files_by_lang
50 .entry("python")
51 .or_insert_with(Vec::new)
52 .push(file.clone()),
53
54 "go" => source_files_by_lang
56 .entry("go")
57 .or_insert_with(Vec::new)
58 .push(file.clone()),
59
60 "java" | "kt" | "kts" => source_files_by_lang
62 .entry("jvm")
63 .or_insert_with(Vec::new)
64 .push(file.clone()),
65
66 _ => {}
67 }
68 }
69
70 if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
72 if is_manifest_file(filename) {
73 manifest_files.push(file.clone());
74 }
75 }
76 }
77
78 if source_files_by_lang.contains_key("rust") || has_manifest(&manifest_files, &["Cargo.toml"]) {
80 if let Ok(info) = analyze_rust_project(&manifest_files, source_files_by_lang.get("rust"), config) {
81 language_info.insert("rust", info);
82 }
83 }
84
85 if source_files_by_lang.contains_key("javascript") || has_manifest(&manifest_files, &["package.json"]) {
86 if let Ok(info) = analyze_javascript_project(&manifest_files, source_files_by_lang.get("javascript"), config) {
87 language_info.insert("javascript", info);
88 }
89 }
90
91 if source_files_by_lang.contains_key("python") || has_manifest(&manifest_files, &["requirements.txt", "Pipfile", "pyproject.toml", "setup.py"]) {
92 if let Ok(info) = analyze_python_project(&manifest_files, source_files_by_lang.get("python"), config) {
93 language_info.insert("python", info);
94 }
95 }
96
97 if source_files_by_lang.contains_key("go") || has_manifest(&manifest_files, &["go.mod"]) {
98 if let Ok(info) = analyze_go_project(&manifest_files, source_files_by_lang.get("go"), config) {
99 language_info.insert("go", info);
100 }
101 }
102
103 if source_files_by_lang.contains_key("jvm") || has_manifest(&manifest_files, &["pom.xml", "build.gradle", "build.gradle.kts"]) {
104 if let Ok(info) = analyze_jvm_project(&manifest_files, source_files_by_lang.get("jvm"), config) {
105 language_info.insert("jvm", info);
106 }
107 }
108
109 let mut detected_languages = Vec::new();
111 for (_, info) in language_info {
112 detected_languages.push(DetectedLanguage {
113 name: info.name,
114 version: info.version,
115 confidence: info.confidence,
116 files: info.source_files,
117 main_dependencies: info.main_dependencies,
118 dev_dependencies: info.dev_dependencies,
119 package_manager: info.package_manager,
120 });
121 }
122
123 detected_languages.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
125
126 Ok(detected_languages)
127}
128
129fn analyze_rust_project(
131 manifest_files: &[PathBuf],
132 source_files: Option<&Vec<PathBuf>>,
133 config: &AnalysisConfig,
134) -> Result<LanguageInfo> {
135 let mut info = LanguageInfo {
136 name: "Rust".to_string(),
137 version: None,
138 edition: None,
139 package_manager: Some("cargo".to_string()),
140 main_dependencies: Vec::new(),
141 dev_dependencies: Vec::new(),
142 confidence: 0.5,
143 source_files: source_files.map_or(Vec::new(), |f| f.clone()),
144 manifest_files: Vec::new(),
145 };
146
147 for manifest in manifest_files {
149 if manifest.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
150 info.manifest_files.push(manifest.clone());
151
152 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
153 if let Ok(cargo_toml) = toml::from_str::<toml::Value>(&content) {
154 if let Some(package) = cargo_toml.get("package") {
156 if let Some(edition) = package.get("edition").and_then(|e| e.as_str()) {
157 info.edition = Some(edition.to_string());
158 }
159
160 info.version = match info.edition.as_deref() {
162 Some("2021") => Some("1.56+".to_string()),
163 Some("2018") => Some("1.31+".to_string()),
164 Some("2015") => Some("1.0+".to_string()),
165 _ => Some("unknown".to_string()),
166 };
167 }
168
169 if let Some(deps) = cargo_toml.get("dependencies") {
171 if let Some(deps_table) = deps.as_table() {
172 for (name, _) in deps_table {
173 info.main_dependencies.push(name.clone());
174 }
175 }
176 }
177
178 if config.include_dev_dependencies {
180 if let Some(dev_deps) = cargo_toml.get("dev-dependencies") {
181 if let Some(dev_deps_table) = dev_deps.as_table() {
182 for (name, _) in dev_deps_table {
183 info.dev_dependencies.push(name.clone());
184 }
185 }
186 }
187 }
188
189 info.confidence = 0.95; }
191 }
192 break;
193 }
194 }
195
196 if !info.source_files.is_empty() {
198 info.confidence = (info.confidence + 0.9) / 2.0;
199 }
200
201 Ok(info)
202}
203
204fn analyze_javascript_project(
206 manifest_files: &[PathBuf],
207 source_files: Option<&Vec<PathBuf>>,
208 config: &AnalysisConfig,
209) -> Result<LanguageInfo> {
210 let mut info = LanguageInfo {
211 name: "JavaScript/TypeScript".to_string(),
212 version: None,
213 edition: None,
214 package_manager: None,
215 main_dependencies: Vec::new(),
216 dev_dependencies: Vec::new(),
217 confidence: 0.5,
218 source_files: source_files.map_or(Vec::new(), |f| f.clone()),
219 manifest_files: Vec::new(),
220 };
221
222 for manifest in manifest_files {
224 if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
225 match filename {
226 "package-lock.json" => info.package_manager = Some("npm".to_string()),
227 "yarn.lock" => info.package_manager = Some("yarn".to_string()),
228 "pnpm-lock.yaml" => info.package_manager = Some("pnpm".to_string()),
229 _ => {}
230 }
231 }
232 }
233
234 if info.package_manager.is_none() {
236 info.package_manager = Some("npm".to_string());
237 }
238
239 for manifest in manifest_files {
241 if manifest.file_name().and_then(|n| n.to_str()) == Some("package.json") {
242 info.manifest_files.push(manifest.clone());
243
244 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
245 if let Ok(package_json) = serde_json::from_str::<JsonValue>(&content) {
246 if let Some(engines) = package_json.get("engines") {
248 if let Some(node_version) = engines.get("node").and_then(|v| v.as_str()) {
249 info.version = Some(node_version.to_string());
250 }
251 }
252
253 if let Some(deps) = package_json.get("dependencies") {
255 if let Some(deps_obj) = deps.as_object() {
256 for (name, _) in deps_obj {
257 info.main_dependencies.push(name.clone());
258 }
259 }
260 }
261
262 if config.include_dev_dependencies {
264 if let Some(dev_deps) = package_json.get("devDependencies") {
265 if let Some(dev_deps_obj) = dev_deps.as_object() {
266 for (name, _) in dev_deps_obj {
267 info.dev_dependencies.push(name.clone());
268 }
269 }
270 }
271 }
272
273 info.confidence = 0.95; }
275 }
276 break;
277 }
278 }
279
280 if let Some(files) = source_files {
282 let has_typescript = files.iter().any(|f| {
283 f.extension()
284 .and_then(|e| e.to_str())
285 .map_or(false, |ext| ext == "ts" || ext == "tsx")
286 });
287
288 if has_typescript {
289 info.name = "TypeScript".to_string();
290 } else {
291 info.name = "JavaScript".to_string();
292 }
293 }
294
295 if !info.source_files.is_empty() {
297 info.confidence = (info.confidence + 0.9) / 2.0;
298 }
299
300 Ok(info)
301}
302
303fn analyze_python_project(
305 manifest_files: &[PathBuf],
306 source_files: Option<&Vec<PathBuf>>,
307 config: &AnalysisConfig,
308) -> Result<LanguageInfo> {
309 let mut info = LanguageInfo {
310 name: "Python".to_string(),
311 version: None,
312 edition: None,
313 package_manager: None,
314 main_dependencies: Vec::new(),
315 dev_dependencies: Vec::new(),
316 confidence: 0.5,
317 source_files: source_files.map_or(Vec::new(), |f| f.clone()),
318 manifest_files: Vec::new(),
319 };
320
321 for manifest in manifest_files {
323 if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
324 info.manifest_files.push(manifest.clone());
325
326 match filename {
327 "requirements.txt" => {
328 info.package_manager = Some("pip".to_string());
329 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
330 parse_requirements_txt(&content, &mut info);
331 info.confidence = 0.85;
332 }
333 }
334 "Pipfile" => {
335 info.package_manager = Some("pipenv".to_string());
336 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
337 parse_pipfile(&content, &mut info, config);
338 info.confidence = 0.90;
339 }
340 }
341 "pyproject.toml" => {
342 info.package_manager = Some("poetry/pip".to_string());
343 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
344 parse_pyproject_toml(&content, &mut info, config);
345 info.confidence = 0.95;
346 }
347 }
348 "setup.py" => {
349 info.package_manager = Some("setuptools".to_string());
350 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
351 parse_setup_py(&content, &mut info);
352 info.confidence = 0.80;
353 }
354 }
355 _ => {}
356 }
357 }
358 }
359
360 if info.package_manager.is_none() && !info.source_files.is_empty() {
362 info.package_manager = Some("pip".to_string());
363 info.confidence = 0.75;
364 }
365
366 if !info.source_files.is_empty() {
368 info.confidence = (info.confidence + 0.8) / 2.0;
369 }
370
371 Ok(info)
372}
373
374fn parse_requirements_txt(content: &str, info: &mut LanguageInfo) {
376 for line in content.lines() {
377 let line = line.trim();
378 if line.is_empty() || line.starts_with('#') {
379 continue;
380 }
381
382 if let Some(package_name) = line.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
384 let clean_name = package_name.trim();
385 if !clean_name.is_empty() && !clean_name.starts_with('-') {
386 info.main_dependencies.push(clean_name.to_string());
387 }
388 }
389 }
390}
391
392fn parse_pipfile(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
394 if let Ok(pipfile) = toml::from_str::<toml::Value>(content) {
395 if let Some(requires) = pipfile.get("requires") {
397 if let Some(python_version) = requires.get("python_version").and_then(|v| v.as_str()) {
398 info.version = Some(format!("~={}", python_version));
399 } else if let Some(python_full) = requires.get("python_full_version").and_then(|v| v.as_str()) {
400 info.version = Some(format!("=={}", python_full));
401 }
402 }
403
404 if let Some(packages) = pipfile.get("packages") {
406 if let Some(packages_table) = packages.as_table() {
407 for (name, _) in packages_table {
408 info.main_dependencies.push(name.clone());
409 }
410 }
411 }
412
413 if config.include_dev_dependencies {
415 if let Some(dev_packages) = pipfile.get("dev-packages") {
416 if let Some(dev_packages_table) = dev_packages.as_table() {
417 for (name, _) in dev_packages_table {
418 info.dev_dependencies.push(name.clone());
419 }
420 }
421 }
422 }
423 }
424}
425
426fn parse_pyproject_toml(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
428 if let Ok(pyproject) = toml::from_str::<toml::Value>(content) {
429 if let Some(project) = pyproject.get("project") {
431 if let Some(requires_python) = project.get("requires-python").and_then(|v| v.as_str()) {
432 info.version = Some(requires_python.to_string());
433 }
434
435 if let Some(dependencies) = project.get("dependencies") {
437 if let Some(deps_array) = dependencies.as_array() {
438 for dep in deps_array {
439 if let Some(dep_str) = dep.as_str() {
440 if let Some(package_name) = dep_str.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
441 let clean_name = package_name.trim();
442 if !clean_name.is_empty() {
443 info.main_dependencies.push(clean_name.to_string());
444 }
445 }
446 }
447 }
448 }
449 }
450
451 if config.include_dev_dependencies {
453 if let Some(optional_deps) = project.get("optional-dependencies") {
454 if let Some(optional_table) = optional_deps.as_table() {
455 for (_, deps) in optional_table {
456 if let Some(deps_array) = deps.as_array() {
457 for dep in deps_array {
458 if let Some(dep_str) = dep.as_str() {
459 if let Some(package_name) = dep_str.split(&['=', '>', '<', '!', '~', ';'][..]).next() {
460 let clean_name = package_name.trim();
461 if !clean_name.is_empty() {
462 info.dev_dependencies.push(clean_name.to_string());
463 }
464 }
465 }
466 }
467 }
468 }
469 }
470 }
471 }
472 }
473
474 if pyproject.get("tool").and_then(|t| t.get("poetry")).is_some() {
476 info.package_manager = Some("poetry".to_string());
477
478 if let Some(tool) = pyproject.get("tool") {
480 if let Some(poetry) = tool.get("poetry") {
481 if let Some(dependencies) = poetry.get("dependencies") {
482 if let Some(deps_table) = dependencies.as_table() {
483 for (name, _) in deps_table {
484 if name != "python" {
485 info.main_dependencies.push(name.clone());
486 }
487 }
488 }
489 }
490
491 if config.include_dev_dependencies {
492 if let Some(dev_dependencies) = poetry.get("group")
493 .and_then(|g| g.get("dev"))
494 .and_then(|d| d.get("dependencies"))
495 {
496 if let Some(dev_deps_table) = dev_dependencies.as_table() {
497 for (name, _) in dev_deps_table {
498 info.dev_dependencies.push(name.clone());
499 }
500 }
501 }
502 }
503 }
504 }
505 }
506 }
507}
508
509fn parse_setup_py(content: &str, info: &mut LanguageInfo) {
511 for line in content.lines() {
513 let line = line.trim();
514
515 if line.contains("python_requires") {
517 if let Some(start) = line.find("\"") {
518 if let Some(end) = line[start + 1..].find("\"") {
519 let version = &line[start + 1..start + 1 + end];
520 info.version = Some(version.to_string());
521 }
522 } else if let Some(start) = line.find("'") {
523 if let Some(end) = line[start + 1..].find("'") {
524 let version = &line[start + 1..start + 1 + end];
525 info.version = Some(version.to_string());
526 }
527 }
528 }
529
530 if line.contains("install_requires") && line.contains("[") {
532 info.main_dependencies.push("setuptools-detected".to_string());
534 }
535 }
536}
537
538fn analyze_go_project(
540 manifest_files: &[PathBuf],
541 source_files: Option<&Vec<PathBuf>>,
542 config: &AnalysisConfig,
543) -> Result<LanguageInfo> {
544 let mut info = LanguageInfo {
545 name: "Go".to_string(),
546 version: None,
547 edition: None,
548 package_manager: Some("go mod".to_string()),
549 main_dependencies: Vec::new(),
550 dev_dependencies: Vec::new(),
551 confidence: 0.5,
552 source_files: source_files.map_or(Vec::new(), |f| f.clone()),
553 manifest_files: Vec::new(),
554 };
555
556 for manifest in manifest_files {
558 if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
559 match filename {
560 "go.mod" => {
561 info.manifest_files.push(manifest.clone());
562 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
563 parse_go_mod(&content, &mut info);
564 info.confidence = 0.95;
565 }
566 }
567 "go.sum" => {
568 info.manifest_files.push(manifest.clone());
569 info.confidence = (info.confidence + 0.9) / 2.0;
571 }
572 _ => {}
573 }
574 }
575 }
576
577 if !info.source_files.is_empty() {
579 info.confidence = (info.confidence + 0.85) / 2.0;
580 }
581
582 Ok(info)
583}
584
585fn parse_go_mod(content: &str, info: &mut LanguageInfo) {
587 for line in content.lines() {
588 let line = line.trim();
589
590 if line.starts_with("go ") {
592 let version = line[3..].trim();
593 info.version = Some(version.to_string());
594 }
595
596 if line.starts_with("require ") {
598 let require_line = &line[8..].trim();
600 if let Some(module_name) = require_line.split_whitespace().next() {
601 info.main_dependencies.push(module_name.to_string());
602 }
603 }
604 }
605
606 let mut in_require_block = false;
608 for line in content.lines() {
609 let line = line.trim();
610
611 if line == "require (" {
612 in_require_block = true;
613 continue;
614 }
615
616 if in_require_block {
617 if line == ")" {
618 in_require_block = false;
619 continue;
620 }
621
622 if !line.is_empty() && !line.starts_with("//") {
624 if let Some(module_name) = line.split_whitespace().next() {
625 info.main_dependencies.push(module_name.to_string());
626 }
627 }
628 }
629 }
630}
631
632fn analyze_jvm_project(
634 manifest_files: &[PathBuf],
635 source_files: Option<&Vec<PathBuf>>,
636 config: &AnalysisConfig,
637) -> Result<LanguageInfo> {
638 let mut info = LanguageInfo {
639 name: "Java/Kotlin".to_string(),
640 version: None,
641 edition: None,
642 package_manager: None,
643 main_dependencies: Vec::new(),
644 dev_dependencies: Vec::new(),
645 confidence: 0.5,
646 source_files: source_files.map_or(Vec::new(), |f| f.clone()),
647 manifest_files: Vec::new(),
648 };
649
650 for manifest in manifest_files {
652 if let Some(filename) = manifest.file_name().and_then(|n| n.to_str()) {
653 info.manifest_files.push(manifest.clone());
654
655 match filename {
656 "pom.xml" => {
657 info.package_manager = Some("maven".to_string());
658 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
659 parse_maven_pom(&content, &mut info, config);
660 info.confidence = 0.90;
661 }
662 }
663 "build.gradle" => {
664 info.package_manager = Some("gradle".to_string());
665 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
666 parse_gradle_build(&content, &mut info, config);
667 info.confidence = 0.85;
668 }
669 }
670 "build.gradle.kts" => {
671 info.package_manager = Some("gradle".to_string());
672 if let Ok(content) = file_utils::read_file_safe(manifest, config.max_file_size) {
673 parse_gradle_kts_build(&content, &mut info, config);
674 info.confidence = 0.85;
675 }
676 }
677 _ => {}
678 }
679 }
680 }
681
682 if let Some(files) = source_files {
684 let has_kotlin = files.iter().any(|f| {
685 f.extension()
686 .and_then(|e| e.to_str())
687 .map_or(false, |ext| ext == "kt" || ext == "kts")
688 });
689
690 if has_kotlin {
691 info.name = "Kotlin".to_string();
692 } else {
693 info.name = "Java".to_string();
694 }
695 }
696
697 if !info.source_files.is_empty() {
699 info.confidence = (info.confidence + 0.8) / 2.0;
700 }
701
702 Ok(info)
703}
704
705fn parse_maven_pom(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
707 for line in content.lines() {
711 let line = line.trim();
712
713 if line.contains("<maven.compiler.source>") {
715 if let Some(version) = extract_xml_content(line, "maven.compiler.source") {
716 info.version = Some(version);
717 }
718 } else if line.contains("<java.version>") {
719 if let Some(version) = extract_xml_content(line, "java.version") {
720 info.version = Some(version);
721 }
722 } else if line.contains("<maven.compiler.target>") && info.version.is_none() {
723 if let Some(version) = extract_xml_content(line, "maven.compiler.target") {
724 info.version = Some(version);
725 }
726 }
727
728 if line.contains("<groupId>") && line.contains("<artifactId>") {
730 if let Some(group_id) = extract_xml_content(line, "groupId") {
732 if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
733 let dependency = format!("{}:{}", group_id, artifact_id);
734 info.main_dependencies.push(dependency);
735 }
736 }
737 } else if line.contains("<artifactId>") && !line.contains("<groupId>") {
738 if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
739 info.main_dependencies.push(artifact_id);
740 }
741 }
742 }
743
744 let mut in_dependencies = false;
746 let mut in_test_dependencies = false;
747
748 for line in content.lines() {
749 let line = line.trim();
750
751 if line.contains("<dependencies>") {
752 in_dependencies = true;
753 continue;
754 }
755
756 if line.contains("</dependencies>") {
757 in_dependencies = false;
758 in_test_dependencies = false;
759 continue;
760 }
761
762 if in_dependencies && line.contains("<scope>test</scope>") {
763 in_test_dependencies = true;
764 }
765
766 if in_dependencies && line.contains("<artifactId>") {
767 if let Some(artifact_id) = extract_xml_content(line, "artifactId") {
768 if in_test_dependencies && config.include_dev_dependencies {
769 info.dev_dependencies.push(artifact_id);
770 } else if !in_test_dependencies {
771 info.main_dependencies.push(artifact_id);
772 }
773 }
774 }
775 }
776}
777
778fn parse_gradle_build(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
780 for line in content.lines() {
781 let line = line.trim();
782
783 if line.contains("sourceCompatibility") || line.contains("targetCompatibility") {
785 if let Some(version) = extract_gradle_version(line) {
786 info.version = Some(version);
787 }
788 } else if line.contains("JavaVersion.VERSION_") {
789 if let Some(pos) = line.find("VERSION_") {
790 let version_part = &line[pos + 8..];
791 if let Some(end) = version_part.find(|c: char| !c.is_numeric() && c != '_') {
792 let version = &version_part[..end].replace('_', ".");
793 info.version = Some(version.to_string());
794 }
795 }
796 }
797
798 if line.starts_with("implementation ") || line.starts_with("compile ") {
800 if let Some(dep) = extract_gradle_dependency(line) {
801 info.main_dependencies.push(dep);
802 }
803 } else if (line.starts_with("testImplementation ") || line.starts_with("testCompile ")) && config.include_dev_dependencies {
804 if let Some(dep) = extract_gradle_dependency(line) {
805 info.dev_dependencies.push(dep);
806 }
807 }
808 }
809}
810
811fn parse_gradle_kts_build(content: &str, info: &mut LanguageInfo, config: &AnalysisConfig) {
813 parse_gradle_build(content, info, config); }
816
817fn extract_xml_content(line: &str, tag: &str) -> Option<String> {
819 let open_tag = format!("<{}>", tag);
820 let close_tag = format!("</{}>", tag);
821
822 if let Some(start) = line.find(&open_tag) {
823 if let Some(end) = line.find(&close_tag) {
824 let content_start = start + open_tag.len();
825 if content_start < end {
826 return Some(line[content_start..end].trim().to_string());
827 }
828 }
829 }
830 None
831}
832
833fn extract_gradle_version(line: &str) -> Option<String> {
835 if let Some(equals_pos) = line.find('=') {
837 let value_part = line[equals_pos + 1..].trim();
838 if let Some(start_quote) = value_part.find(['\'', '"']) {
839 let quote_char = value_part.chars().nth(start_quote).unwrap();
840 if let Some(end_quote) = value_part[start_quote + 1..].find(quote_char) {
841 let version = &value_part[start_quote + 1..start_quote + 1 + end_quote];
842 return Some(version.to_string());
843 }
844 }
845 }
846 None
847}
848
849fn extract_gradle_dependency(line: &str) -> Option<String> {
851 if let Some(start_quote) = line.find(['\'', '"']) {
853 let quote_char = line.chars().nth(start_quote).unwrap();
854 if let Some(end_quote) = line[start_quote + 1..].find(quote_char) {
855 let dependency = &line[start_quote + 1..start_quote + 1 + end_quote];
856 if let Some(last_colon) = dependency.rfind(':') {
858 if let Some(first_colon) = dependency[..last_colon].rfind(':') {
859 return Some(dependency[first_colon + 1..last_colon].to_string());
860 }
861 }
862 return Some(dependency.to_string());
863 }
864 }
865 None
866}
867
868fn is_manifest_file(filename: &str) -> bool {
870 matches!(
871 filename,
872 "Cargo.toml" | "Cargo.lock" |
873 "package.json" | "package-lock.json" | "yarn.lock" | "pnpm-lock.yaml" |
874 "requirements.txt" | "Pipfile" | "Pipfile.lock" | "pyproject.toml" | "setup.py" |
875 "go.mod" | "go.sum" |
876 "pom.xml" | "build.gradle" | "build.gradle.kts"
877 )
878}
879
880fn has_manifest(manifest_files: &[PathBuf], target_files: &[&str]) -> bool {
882 manifest_files.iter().any(|path| {
883 path.file_name()
884 .and_then(|name| name.to_str())
885 .map_or(false, |name| target_files.contains(&name))
886 })
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use tempfile::TempDir;
893 use std::fs;
894
895 #[test]
896 fn test_rust_project_detection() {
897 let temp_dir = TempDir::new().unwrap();
898 let root = temp_dir.path();
899
900 let cargo_toml = r#"
902[package]
903name = "test-project"
904version = "0.1.0"
905edition = "2021"
906
907[dependencies]
908serde = "1.0"
909tokio = "1.0"
910
911[dev-dependencies]
912assert_cmd = "2.0"
913"#;
914 fs::write(root.join("Cargo.toml"), cargo_toml).unwrap();
915 fs::create_dir_all(root.join("src")).unwrap();
916 fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
917
918 let config = AnalysisConfig::default();
919 let files = vec![
920 root.join("Cargo.toml"),
921 root.join("src/main.rs"),
922 ];
923
924 let languages = detect_languages(&files, &config).unwrap();
925 assert_eq!(languages.len(), 1);
926 assert_eq!(languages[0].name, "Rust");
927 assert_eq!(languages[0].version, Some("1.56+".to_string()));
928 assert!(languages[0].confidence > 0.9);
929 }
930
931 #[test]
932 fn test_javascript_project_detection() {
933 let temp_dir = TempDir::new().unwrap();
934 let root = temp_dir.path();
935
936 let package_json = r#"
938{
939 "name": "test-project",
940 "version": "1.0.0",
941 "engines": {
942 "node": ">=16.0.0"
943 },
944 "dependencies": {
945 "express": "^4.18.0",
946 "lodash": "^4.17.21"
947 },
948 "devDependencies": {
949 "jest": "^29.0.0"
950 }
951}
952"#;
953 fs::write(root.join("package.json"), package_json).unwrap();
954 fs::write(root.join("index.js"), "console.log('hello');").unwrap();
955
956 let config = AnalysisConfig::default();
957 let files = vec![
958 root.join("package.json"),
959 root.join("index.js"),
960 ];
961
962 let languages = detect_languages(&files, &config).unwrap();
963 assert_eq!(languages.len(), 1);
964 assert_eq!(languages[0].name, "JavaScript");
965 assert_eq!(languages[0].version, Some(">=16.0.0".to_string()));
966 assert!(languages[0].confidence > 0.9);
967 }
968
969 #[test]
970 fn test_python_project_detection() {
971 let temp_dir = TempDir::new().unwrap();
972 let root = temp_dir.path();
973
974 let pyproject_toml = r#"
976[project]
977name = "test-project"
978version = "0.1.0"
979requires-python = ">=3.8"
980dependencies = [
981 "flask>=2.0.0",
982 "requests>=2.25.0",
983 "pandas>=1.3.0"
984]
985
986[project.optional-dependencies]
987dev = [
988 "pytest>=6.0.0",
989 "black>=21.0.0"
990]
991"#;
992 fs::write(root.join("pyproject.toml"), pyproject_toml).unwrap();
993 fs::write(root.join("app.py"), "print('Hello, World!')").unwrap();
994
995 let config = AnalysisConfig::default();
996 let files = vec![
997 root.join("pyproject.toml"),
998 root.join("app.py"),
999 ];
1000
1001 let languages = detect_languages(&files, &config).unwrap();
1002 assert_eq!(languages.len(), 1);
1003 assert_eq!(languages[0].name, "Python");
1004 assert_eq!(languages[0].version, Some(">=3.8".to_string()));
1005 assert!(languages[0].confidence > 0.8);
1006 }
1007
1008 #[test]
1009 fn test_go_project_detection() {
1010 let temp_dir = TempDir::new().unwrap();
1011 let root = temp_dir.path();
1012
1013 let go_mod = r#"
1015module example.com/myproject
1016
1017go 1.21
1018
1019require (
1020 github.com/gin-gonic/gin v1.9.1
1021 github.com/stretchr/testify v1.8.4
1022 golang.org/x/time v0.3.0
1023)
1024"#;
1025 fs::write(root.join("go.mod"), go_mod).unwrap();
1026 fs::write(root.join("main.go"), "package main\n\nfunc main() {}").unwrap();
1027
1028 let config = AnalysisConfig::default();
1029 let files = vec![
1030 root.join("go.mod"),
1031 root.join("main.go"),
1032 ];
1033
1034 let languages = detect_languages(&files, &config).unwrap();
1035 assert_eq!(languages.len(), 1);
1036 assert_eq!(languages[0].name, "Go");
1037 assert_eq!(languages[0].version, Some("1.21".to_string()));
1038 assert!(languages[0].confidence > 0.8);
1039 }
1040
1041 #[test]
1042 fn test_java_maven_project_detection() {
1043 let temp_dir = TempDir::new().unwrap();
1044 let root = temp_dir.path();
1045
1046 let pom_xml = r#"
1048<?xml version="1.0" encoding="UTF-8"?>
1049<project xmlns="http://maven.apache.org/POM/4.0.0">
1050 <modelVersion>4.0.0</modelVersion>
1051
1052 <groupId>com.example</groupId>
1053 <artifactId>test-project</artifactId>
1054 <version>1.0.0</version>
1055
1056 <properties>
1057 <maven.compiler.source>17</maven.compiler.source>
1058 <maven.compiler.target>17</maven.compiler.target>
1059 </properties>
1060
1061 <dependencies>
1062 <dependency>
1063 <groupId>org.springframework</groupId>
1064 <artifactId>spring-core</artifactId>
1065 <version>5.3.21</version>
1066 </dependency>
1067 <dependency>
1068 <groupId>junit</groupId>
1069 <artifactId>junit</artifactId>
1070 <version>4.13.2</version>
1071 <scope>test</scope>
1072 </dependency>
1073 </dependencies>
1074</project>
1075"#;
1076 fs::create_dir_all(root.join("src/main/java")).unwrap();
1077 fs::write(root.join("pom.xml"), pom_xml).unwrap();
1078 fs::write(root.join("src/main/java/App.java"), "public class App {}").unwrap();
1079
1080 let config = AnalysisConfig::default();
1081 let files = vec![
1082 root.join("pom.xml"),
1083 root.join("src/main/java/App.java"),
1084 ];
1085
1086 let languages = detect_languages(&files, &config).unwrap();
1087 assert_eq!(languages.len(), 1);
1088 assert_eq!(languages[0].name, "Java");
1089 assert_eq!(languages[0].version, Some("17".to_string()));
1090 assert!(languages[0].confidence > 0.8);
1091 }
1092
1093 #[test]
1094 fn test_kotlin_gradle_project_detection() {
1095 let temp_dir = TempDir::new().unwrap();
1096 let root = temp_dir.path();
1097
1098 let build_gradle_kts = r#"
1100plugins {
1101 kotlin("jvm") version "1.9.0"
1102 application
1103}
1104
1105java {
1106 sourceCompatibility = JavaVersion.VERSION_17
1107 targetCompatibility = JavaVersion.VERSION_17
1108}
1109
1110dependencies {
1111 implementation("org.jetbrains.kotlin:kotlin-stdlib")
1112 implementation("io.ktor:ktor-server-core:2.3.2")
1113 testImplementation("org.jetbrains.kotlin:kotlin-test")
1114}
1115"#;
1116 fs::create_dir_all(root.join("src/main/kotlin")).unwrap();
1117 fs::write(root.join("build.gradle.kts"), build_gradle_kts).unwrap();
1118 fs::write(root.join("src/main/kotlin/Main.kt"), "fun main() {}").unwrap();
1119
1120 let config = AnalysisConfig::default();
1121 let files = vec![
1122 root.join("build.gradle.kts"),
1123 root.join("src/main/kotlin/Main.kt"),
1124 ];
1125
1126 let languages = detect_languages(&files, &config).unwrap();
1127 assert_eq!(languages.len(), 1);
1128 assert_eq!(languages[0].name, "Kotlin");
1129 assert!(languages[0].confidence > 0.8);
1130 }
1131
1132 #[test]
1133 fn test_python_requirements_txt_detection() {
1134 let temp_dir = TempDir::new().unwrap();
1135 let root = temp_dir.path();
1136
1137 let requirements_txt = r#"
1139Flask==2.3.2
1140requests>=2.28.0
1141pandas==1.5.3
1142pytest==7.4.0
1143black>=23.0.0
1144"#;
1145 fs::write(root.join("requirements.txt"), requirements_txt).unwrap();
1146 fs::write(root.join("app.py"), "import flask").unwrap();
1147
1148 let config = AnalysisConfig::default();
1149 let files = vec![
1150 root.join("requirements.txt"),
1151 root.join("app.py"),
1152 ];
1153
1154 let languages = detect_languages(&files, &config).unwrap();
1155 assert_eq!(languages.len(), 1);
1156 assert_eq!(languages[0].name, "Python");
1157 assert!(languages[0].confidence > 0.8);
1158 }
1159}