mati_core/analysis/resolvers/
go.rs1use super::{FileIndex, LanguageResolver};
17use crate::analysis::parser::ImportStatement;
18use crate::analysis::walker::Language;
19
20pub struct GoResolver;
21
22impl LanguageResolver for GoResolver {
23 fn resolve(
24 &self,
25 import: &ImportStatement,
26 importing_file: &str,
27 file_index: &FileIndex,
28 ) -> Option<String> {
29 if !import.path.contains('/') {
31 return None;
32 }
33
34 let module_path = self.find_module_path(importing_file, file_index)?;
36
37 let Some(relative) = import.path.strip_prefix(&module_path) else {
39 return None; };
41 let relative = relative.trim_start_matches('/');
43
44 let prefix = if relative.is_empty() {
46 String::new()
47 } else {
48 format!("{relative}/")
49 };
50 let candidates = file_index.files_with_prefix(&prefix);
51 candidates
52 .into_iter()
53 .find(|f| f.ends_with(".go") && !f.ends_with("_test.go"))
54 .cloned()
55 }
56
57 fn language(&self) -> Language {
58 Language::Go
59 }
60
61 fn name(&self) -> &'static str {
62 "go"
63 }
64}
65
66impl Default for GoResolver {
67 fn default() -> Self {
68 Self
69 }
70}
71
72impl GoResolver {
73 pub fn new() -> Self {
74 Self
75 }
76
77 fn find_module_path(&self, importing_file: &str, file_index: &FileIndex) -> Option<String> {
81 use std::path::Path;
82 let mut current = Path::new(importing_file).parent();
83 while let Some(dir) = current {
84 let candidate = if dir.as_os_str().is_empty() {
85 "go.mod".to_string()
86 } else {
87 format!("{}/go.mod", dir.display())
88 };
89 if file_index.contains(&candidate) {
90 if let Some(content) = file_index.read_file(&candidate) {
91 return Self::parse_module_line(&content);
92 }
93 }
94 current = dir.parent();
95 }
96 None
97 }
98
99 fn parse_module_line(content: &str) -> Option<String> {
102 for line in content.lines() {
103 let trimmed = line.trim();
104 if let Some(rest) = trimmed.strip_prefix("module ") {
105 let rest = rest.split("//").next().unwrap_or("");
107 let rest = rest.trim().trim_matches('"');
109 if !rest.is_empty() {
110 return Some(rest.to_string());
111 }
112 }
113 }
114 None
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::analysis::parser::import::ImportKind;
122 use tempfile::TempDir;
123
124 fn idx(paths: &[&str]) -> FileIndex {
125 FileIndex::new(paths.iter().map(|s| s.to_string()))
126 }
127
128 fn import(path: &str) -> ImportStatement {
129 ImportStatement::new(path, ImportKind::Normal, 1)
130 }
131
132 fn setup_go_project(dir: &TempDir, module_name: &str, files: &[&str]) -> FileIndex {
134 let go_mod_content = format!("module {module_name}\n\ngo 1.21\n");
135 std::fs::write(dir.path().join("go.mod"), &go_mod_content).unwrap();
136
137 for file in files {
139 let path = dir.path().join(file);
140 if let Some(parent) = path.parent() {
141 std::fs::create_dir_all(parent).unwrap();
142 }
143 std::fs::write(&path, "package main\n").unwrap();
144 }
145
146 let mut all_files: Vec<String> = vec!["go.mod".to_string()];
147 all_files.extend(files.iter().map(|s| s.to_string()));
148 FileIndex::new_with_root(dir.path().to_path_buf(), all_files)
149 }
150
151 #[test]
154 fn stdlib_single_segment_skipped() {
155 let file_index = idx(&["main.go"]);
156 assert_eq!(
157 GoResolver.resolve(&import("fmt"), "main.go", &file_index),
158 None
159 );
160 }
161
162 #[test]
163 fn external_domain_skipped_without_gomod() {
164 let file_index = idx(&["main.go"]);
166 assert_eq!(
167 GoResolver.resolve(&import("github.com/user/pkg"), "main.go", &file_index),
168 None
169 );
170 }
171
172 #[test]
173 fn nonexistent_package_returns_none() {
174 let file_index = idx(&["main.go"]);
175 assert_eq!(
176 GoResolver.resolve(&import("internal/missing"), "main.go", &file_index),
177 None
178 );
179 }
180
181 #[test]
184 fn parses_simple_gomod_module_line() {
185 let content = "module github.com/acme/project\n\ngo 1.21\n";
186 assert_eq!(
187 GoResolver::parse_module_line(content),
188 Some("github.com/acme/project".into())
189 );
190 }
191
192 #[test]
193 fn parses_gomod_with_comment_on_module_line() {
194 let content = "module github.com/acme/project // my project\n\ngo 1.21\n";
195 assert_eq!(
196 GoResolver::parse_module_line(content),
197 Some("github.com/acme/project".into())
198 );
199 }
200
201 #[test]
202 fn parses_gomod_with_require_block() {
203 let content = "module github.com/acme/project\n\ngo 1.21\n\nrequire (\n\tgithub.com/pkg/errors v0.9.1\n)\n";
204 assert_eq!(
205 GoResolver::parse_module_line(content),
206 Some("github.com/acme/project".into())
207 );
208 }
209
210 #[test]
211 fn parses_gomod_with_quoted_module() {
212 let content = "module \"github.com/acme/project\"\n\ngo 1.21\n";
213 assert_eq!(
214 GoResolver::parse_module_line(content),
215 Some("github.com/acme/project".into())
216 );
217 }
218
219 #[test]
222 fn walks_up_from_nested_file_to_find_gomod() {
223 let dir = TempDir::new().unwrap();
224 let file_index = setup_go_project(
225 &dir,
226 "github.com/acme/project",
227 &["cmd/server/main.go", "internal/auth/auth.go"],
228 );
229
230 let module = GoResolver.find_module_path("cmd/server/main.go", &file_index);
231 assert_eq!(module, Some("github.com/acme/project".into()));
232 }
233
234 #[test]
237 fn resolves_internal_import_with_module_path_prefix() {
238 let dir = TempDir::new().unwrap();
239 let file_index = setup_go_project(
240 &dir,
241 "github.com/acme/project",
242 &["main.go", "auth/auth.go", "auth/token.go"],
243 );
244
245 let result = GoResolver.resolve(
246 &import("github.com/acme/project/auth"),
247 "main.go",
248 &file_index,
249 );
250 assert!(result.is_some(), "should resolve internal import");
251 let resolved = result.unwrap();
252 assert!(
253 resolved.starts_with("auth/") && resolved.ends_with(".go"),
254 "expected auth/*.go, got: {resolved}"
255 );
256 }
257
258 #[test]
259 fn rejects_stdlib_import() {
260 let dir = TempDir::new().unwrap();
261 let file_index = setup_go_project(&dir, "github.com/acme/project", &["main.go"]);
262 assert_eq!(
263 GoResolver.resolve(&import("net/http"), "main.go", &file_index),
264 None,
265 "stdlib multi-segment imports should not resolve"
266 );
267 }
268
269 #[test]
270 fn rejects_third_party_import() {
271 let dir = TempDir::new().unwrap();
272 let file_index = setup_go_project(&dir, "github.com/acme/project", &["main.go"]);
273 assert_eq!(
274 GoResolver.resolve(
275 &import("github.com/other/library/pkg"),
276 "main.go",
277 &file_index,
278 ),
279 None,
280 "third-party imports should not resolve"
281 );
282 }
283
284 #[test]
285 fn skips_test_go_files_in_package_resolution() {
286 let dir = TempDir::new().unwrap();
287 let go_mod = "module github.com/acme/project\n\ngo 1.21\n";
288 std::fs::write(dir.path().join("go.mod"), go_mod).unwrap();
289
290 std::fs::create_dir_all(dir.path().join("auth")).unwrap();
292 std::fs::write(dir.path().join("auth/auth_test.go"), "package auth\n").unwrap();
293
294 let file_index = FileIndex::new_with_root(
295 dir.path().to_path_buf(),
296 vec![
297 "go.mod".to_string(),
298 "main.go".to_string(),
299 "auth/auth_test.go".to_string(),
300 ],
301 );
302
303 let result = GoResolver.resolve(
304 &import("github.com/acme/project/auth"),
305 "main.go",
306 &file_index,
307 );
308 assert_eq!(result, None, "_test.go files should be skipped");
309 }
310
311 #[test]
312 fn resolves_nested_package_import() {
313 let dir = TempDir::new().unwrap();
314 let file_index = setup_go_project(
315 &dir,
316 "github.com/acme/project",
317 &[
318 "main.go",
319 "internal/auth/handler.go",
320 "internal/db/client.go",
321 ],
322 );
323
324 let result = GoResolver.resolve(
325 &import("github.com/acme/project/internal/auth"),
326 "main.go",
327 &file_index,
328 );
329 assert_eq!(result, Some("internal/auth/handler.go".into()));
330
331 let result = GoResolver.resolve(
332 &import("github.com/acme/project/internal/db"),
333 "main.go",
334 &file_index,
335 );
336 assert_eq!(result, Some("internal/db/client.go".into()));
337 }
338
339 #[test]
340 fn no_gomod_returns_none_for_all_multi_segment() {
341 let file_index = idx(&["main.go", "internal/auth/auth.go"]);
343 assert_eq!(
344 GoResolver.resolve(&import("internal/auth"), "main.go", &file_index),
345 None
346 );
347 }
348}