1use crate::ManifestParser;
4use kdo_core::{DepKind, Dependency, KdoError, Language, Project};
5use std::path::Path;
6
7pub struct GoParser;
9
10impl ManifestParser for GoParser {
11 fn manifest_name(&self) -> &str {
12 "go.mod"
13 }
14
15 fn can_parse(&self, manifest_path: &Path) -> bool {
16 manifest_path
17 .file_name()
18 .map(|f| f == "go.mod")
19 .unwrap_or(false)
20 }
21
22 fn parse(
23 &self,
24 manifest_path: &Path,
25 workspace_root: &Path,
26 ) -> Result<(Project, Vec<Dependency>), KdoError> {
27 let content = std::fs::read_to_string(manifest_path)?;
28
29 let name = parse_module_name(&content).unwrap_or_else(|| {
30 manifest_path
31 .parent()
32 .and_then(|p| p.file_name())
33 .map(|n| n.to_string_lossy().to_string())
34 .unwrap_or_else(|| "unknown".to_string())
35 });
36
37 let project_path = manifest_path
38 .parent()
39 .unwrap_or(manifest_path)
40 .to_path_buf();
41
42 let deps = parse_requires(&content);
44
45 let workspace_deps = deps
47 .into_iter()
48 .filter_map(|(dep_name, version)| {
49 let local_path = find_replace_path(&content, &dep_name)?;
51 let resolved = workspace_root.join(&local_path);
52 if resolved.exists() {
53 Some(Dependency {
54 name: dep_name
55 .split('/')
56 .next_back()
57 .unwrap_or(&dep_name)
58 .to_string(),
59 version_req: version,
60 kind: DepKind::Source,
61 is_workspace: true,
62 resolved_path: Some(resolved),
63 })
64 } else {
65 None
66 }
67 })
68 .collect();
69
70 let project = Project {
71 name: short_name(&name),
72 path: project_path,
73 language: Language::Go,
74 manifest_path: manifest_path.to_path_buf(),
75 context_summary: None,
76 public_api_files: Vec::new(),
77 internal_files: Vec::new(),
78 content_hash: [0u8; 32],
79 };
80
81 Ok((project, workspace_deps))
82 }
83}
84
85fn parse_module_name(content: &str) -> Option<String> {
87 for line in content.lines() {
88 let trimmed = line.trim();
89 if let Some(rest) = trimmed.strip_prefix("module ") {
90 let name = rest.trim().to_string();
91 if !name.is_empty() {
92 return Some(name);
93 }
94 }
95 }
96 None
97}
98
99fn parse_requires(content: &str) -> Vec<(String, String)> {
101 let mut deps = Vec::new();
102 let mut in_require_block = false;
103
104 for line in content.lines() {
105 let trimmed = line.trim();
106
107 if trimmed == "require (" {
108 in_require_block = true;
109 continue;
110 }
111 if in_require_block && trimmed == ")" {
112 in_require_block = false;
113 continue;
114 }
115
116 if in_require_block {
117 let parts: Vec<&str> = trimmed.splitn(2, ' ').collect();
119 if parts.len() == 2 {
120 let module = parts[0].trim().to_string();
121 let version = parts[1].split("//").next().unwrap_or("").trim().to_string();
122 if !module.is_empty() && !version.is_empty() {
123 deps.push((module, version));
124 }
125 }
126 } else if let Some(rest) = trimmed.strip_prefix("require ") {
127 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
129 if parts.len() == 2 {
130 deps.push((parts[0].trim().to_string(), parts[1].trim().to_string()));
131 }
132 }
133 }
134
135 deps
136}
137
138fn find_replace_path(content: &str, module_path: &str) -> Option<String> {
140 for line in content.lines() {
141 let trimmed = line.trim();
142 if let Some(rest) = trimmed.strip_prefix("replace ") {
144 if rest.contains(module_path) {
145 if let Some(arrow_pos) = rest.find("=>") {
146 let path = rest[arrow_pos + 2..].trim().to_string();
147 if path.starts_with("./") || path.starts_with("../") {
149 return Some(path);
150 }
151 }
152 }
153 }
154 }
155 None
156}
157
158fn short_name(module_path: &str) -> String {
160 module_path
161 .split('/')
162 .next_back()
163 .unwrap_or(module_path)
164 .to_string()
165}