1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use tree_sitter::Node;
8
9#[derive(Debug, Clone)]
15struct GoModule {
16 path: String,
18 #[allow(dead_code)]
20 go_version: Option<String>,
21}
22
23fn parse_go_mod(path: &Path) -> Option<GoModule> {
25 let content = std::fs::read_to_string(path).ok()?;
26 parse_go_mod_content(&content)
27}
28
29fn parse_go_mod_content(content: &str) -> Option<GoModule> {
31 let mut module_path = None;
32 let mut go_version = None;
33
34 for line in content.lines() {
35 let line = line.trim();
36
37 if line.starts_with("module ") {
39 module_path = Some(line.trim_start_matches("module ").trim().to_string());
40 }
41
42 if line.starts_with("go ") {
44 go_version = Some(line.trim_start_matches("go ").trim().to_string());
45 }
46 }
47
48 module_path.map(|path| GoModule { path, go_version })
49}
50
51fn find_go_mod(start: &Path) -> Option<PathBuf> {
53 let mut current = if start.is_file() {
54 start.parent()?.to_path_buf()
55 } else {
56 start.to_path_buf()
57 };
58
59 loop {
60 let go_mod = current.join("go.mod");
61 if go_mod.exists() {
62 return Some(go_mod);
63 }
64
65 if !current.pop() {
66 break;
67 }
68 }
69
70 None
71}
72
73fn resolve_go_import(import_path: &str, module: &GoModule, project_root: &Path) -> Option<PathBuf> {
78 if !import_path.starts_with(&module.path) {
80 return None; }
82
83 let rel_path = import_path.strip_prefix(&module.path)?;
85 let rel_path = rel_path.trim_start_matches('/');
86
87 let target = if rel_path.is_empty() {
88 project_root.to_path_buf()
89 } else {
90 project_root.join(rel_path)
91 };
92
93 Some(target)
94}
95
96pub fn get_go_version() -> Option<String> {
102 let output = Command::new("go").args(["version"]).output().ok()?;
103
104 if output.status.success() {
105 let version_str = String::from_utf8_lossy(&output.stdout);
106 for part in version_str.split_whitespace() {
108 if part.starts_with("go") && part.len() > 2 {
109 let ver = part.trim_start_matches("go");
110 let parts: Vec<&str> = ver.split('.').collect();
112 if parts.len() >= 2 {
113 return Some(format!("{}.{}", parts[0], parts[1]));
114 }
115 }
116 }
117 }
118
119 None
120}
121
122pub fn find_go_stdlib() -> Option<PathBuf> {
124 if let Ok(goroot) = std::env::var("GOROOT") {
126 let src = PathBuf::from(goroot).join("src");
127 if src.is_dir() {
128 return Some(src);
129 }
130 }
131
132 if let Ok(output) = Command::new("go").args(["env", "GOROOT"]).output() {
134 if output.status.success() {
135 let goroot = String::from_utf8_lossy(&output.stdout).trim().to_string();
136 let src = PathBuf::from(goroot).join("src");
137 if src.is_dir() {
138 return Some(src);
139 }
140 }
141 }
142
143 for path in &["/usr/local/go/src", "/usr/lib/go/src", "/opt/go/src"] {
145 let src = PathBuf::from(path);
146 if src.is_dir() {
147 return Some(src);
148 }
149 }
150
151 None
152}
153
154fn is_go_stdlib_import(import_path: &str) -> bool {
156 let first_segment = import_path.split('/').next().unwrap_or(import_path);
157 !first_segment.contains('.')
158}
159
160fn resolve_go_stdlib_import(import_path: &str, stdlib_path: &Path) -> Option<ResolvedPackage> {
162 if !is_go_stdlib_import(import_path) {
163 return None;
164 }
165
166 let pkg_dir = stdlib_path.join(import_path);
167 if pkg_dir.is_dir() {
168 return Some(ResolvedPackage {
169 path: pkg_dir,
170 name: import_path.to_string(),
171 is_namespace: false,
172 });
173 }
174
175 None
176}
177
178pub fn find_go_mod_cache() -> Option<PathBuf> {
182 if let Ok(cache) = std::env::var("GOMODCACHE") {
184 let path = PathBuf::from(cache);
185 if path.is_dir() {
186 return Some(path);
187 }
188 }
189
190 if let Ok(home) = std::env::var("HOME") {
192 let mod_cache = PathBuf::from(home).join("go").join("pkg").join("mod");
193 if mod_cache.is_dir() {
194 return Some(mod_cache);
195 }
196 }
197
198 if let Ok(home) = std::env::var("USERPROFILE") {
200 let mod_cache = PathBuf::from(home).join("go").join("pkg").join("mod");
201 if mod_cache.is_dir() {
202 return Some(mod_cache);
203 }
204 }
205
206 None
207}
208
209fn resolve_go_mod_cache_import(import_path: &str, mod_cache: &Path) -> Option<ResolvedPackage> {
214 let first_segment = import_path.split('/').next()?;
216 if !first_segment.contains('.') {
217 return None;
219 }
220
221 let parts: Vec<&str> = import_path.split('/').collect();
228
229 for i in (2..=parts.len()).rev() {
230 let module_prefix = parts[..i].join("/");
231 let module_dir = mod_cache.join(&module_prefix);
232
233 if let Some(parent) = module_dir.parent() {
235 if parent.is_dir() {
236 let module_name = module_dir.file_name()?.to_string_lossy();
238 if let Ok(entries) = std::fs::read_dir(parent) {
239 for entry in entries.flatten() {
240 let name = entry.file_name();
241 let name_str = name.to_string_lossy();
242 if name_str.starts_with(&format!("{}@", module_name)) {
244 let versioned_path = entry.path();
245 let remainder = if i < parts.len() {
247 parts[i..].join("/")
248 } else {
249 String::new()
250 };
251 let full_path = if remainder.is_empty() {
252 versioned_path.clone()
253 } else {
254 versioned_path.join(&remainder)
255 };
256
257 if full_path.is_dir() {
258 return Some(ResolvedPackage {
259 path: full_path,
260 name: import_path.to_string(),
261 is_namespace: false,
262 });
263 }
264 }
265 }
266 }
267 }
268 }
269 }
270
271 None
272}
273
274pub struct Go;
280
281impl Language for Go {
282 fn name(&self) -> &'static str {
283 "Go"
284 }
285 fn extensions(&self) -> &'static [&'static str] {
286 &["go"]
287 }
288 fn grammar_name(&self) -> &'static str {
289 "go"
290 }
291
292 fn has_symbols(&self) -> bool {
293 true
294 }
295
296 fn container_kinds(&self) -> &'static [&'static str] {
297 &[] }
299
300 fn function_kinds(&self) -> &'static [&'static str] {
301 &["function_declaration", "method_declaration"]
302 }
303
304 fn type_kinds(&self) -> &'static [&'static str] {
305 &["type_spec"] }
307
308 fn import_kinds(&self) -> &'static [&'static str] {
309 &["import_declaration"]
310 }
311
312 fn public_symbol_kinds(&self) -> &'static [&'static str] {
313 &[
314 "function_declaration",
315 "method_declaration",
316 "type_spec",
317 "const_spec",
318 "var_spec",
319 ]
320 }
321
322 fn visibility_mechanism(&self) -> VisibilityMechanism {
323 VisibilityMechanism::NamingConvention
324 }
325
326 fn scope_creating_kinds(&self) -> &'static [&'static str] {
327 &[
328 "for_statement",
329 "if_statement",
330 "expression_switch_statement",
331 "type_switch_statement",
332 "select_statement",
333 "block",
334 ]
335 }
336
337 fn control_flow_kinds(&self) -> &'static [&'static str] {
338 &[
339 "if_statement",
340 "for_statement",
341 "expression_switch_statement",
342 "type_switch_statement",
343 "select_statement",
344 "return_statement",
345 "break_statement",
346 "continue_statement",
347 "goto_statement",
348 "defer_statement",
349 ]
350 }
351
352 fn complexity_nodes(&self) -> &'static [&'static str] {
353 &[
354 "if_statement",
355 "for_statement",
356 "expression_switch_statement",
357 "type_switch_statement",
358 "select_statement",
359 "expression_case",
360 "type_case",
361 "communication_case",
362 "binary_expression",
363 ]
364 }
365
366 fn nesting_nodes(&self) -> &'static [&'static str] {
367 &[
368 "if_statement",
369 "for_statement",
370 "expression_switch_statement",
371 "type_switch_statement",
372 "select_statement",
373 "function_declaration",
374 "method_declaration",
375 ]
376 }
377
378 fn signature_suffix(&self) -> &'static str {
379 " {}"
380 }
381
382 fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
383 let name = self.node_name(node, content)?;
384 let params = node
385 .child_by_field_name("parameters")
386 .map(|p| content[p.byte_range()].to_string())
387 .unwrap_or_else(|| "()".to_string());
388
389 Some(Symbol {
390 name: name.to_string(),
391 kind: if in_container {
392 SymbolKind::Method
393 } else {
394 SymbolKind::Function
395 },
396 signature: format!("func {}{}", name, params),
397 docstring: None,
398 attributes: Vec::new(),
399 start_line: node.start_position().row + 1,
400 end_line: node.end_position().row + 1,
401 visibility: if name
402 .chars()
403 .next()
404 .map(|c| c.is_uppercase())
405 .unwrap_or(false)
406 {
407 Visibility::Public
408 } else {
409 Visibility::Private
410 },
411 children: Vec::new(),
412 is_interface_impl: false,
413 implements: Vec::new(),
414 })
415 }
416
417 fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
418 None }
420
421 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
422 let name_node = node.child_by_field_name("name")?;
424 let name = content[name_node.byte_range()].to_string();
425
426 let type_node = node.child_by_field_name("type");
427 let type_kind = type_node.map(|t| t.kind()).unwrap_or("");
428
429 let kind = match type_kind {
430 "struct_type" => SymbolKind::Struct,
431 "interface_type" => SymbolKind::Interface,
432 _ => SymbolKind::Type,
433 };
434
435 Some(Symbol {
436 name: name.clone(),
437 kind,
438 signature: format!("type {}", name),
439 docstring: None,
440 attributes: Vec::new(),
441 start_line: node.start_position().row + 1,
442 end_line: node.end_position().row + 1,
443 visibility: if name
444 .chars()
445 .next()
446 .map(|c| c.is_uppercase())
447 .unwrap_or(false)
448 {
449 Visibility::Public
450 } else {
451 Visibility::Private
452 },
453 children: Vec::new(),
454 is_interface_impl: false,
455 implements: Vec::new(),
456 })
457 }
458
459 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
460 if node.kind() != "import_declaration" {
461 return Vec::new();
462 }
463
464 let mut imports = Vec::new();
465 let line = node.start_position().row + 1;
466
467 let mut cursor = node.walk();
468 for child in node.children(&mut cursor) {
469 match child.kind() {
470 "import_spec" => {
471 if let Some(imp) = Self::parse_import_spec(&child, content, line) {
473 imports.push(imp);
474 }
475 }
476 "import_spec_list" => {
477 let mut list_cursor = child.walk();
479 for spec in child.children(&mut list_cursor) {
480 if spec.kind() == "import_spec" {
481 if let Some(imp) = Self::parse_import_spec(&spec, content, line) {
482 imports.push(imp);
483 }
484 }
485 }
486 }
487 _ => {}
488 }
489 }
490
491 imports
492 }
493
494 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
495 if let Some(ref alias) = import.alias {
497 format!("import {} \"{}\"", alias, import.module)
498 } else {
499 format!("import \"{}\"", import.module)
500 }
501 }
502
503 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
504 let name = match self.node_name(node, content) {
506 Some(n) if n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) => n,
507 _ => return Vec::new(),
508 };
509
510 let line = node.start_position().row + 1;
511 let kind = match node.kind() {
512 "function_declaration" => SymbolKind::Function,
513 "method_declaration" => SymbolKind::Method,
514 "type_spec" => SymbolKind::Type,
515 "const_spec" => SymbolKind::Constant,
516 "var_spec" => SymbolKind::Variable,
517 _ => return Vec::new(),
518 };
519
520 vec![Export {
521 name: name.to_string(),
522 kind,
523 line,
524 }]
525 }
526
527 fn is_public(&self, node: &Node, content: &str) -> bool {
528 self.node_name(node, content)
529 .and_then(|n| n.chars().next())
530 .map(|c| c.is_uppercase())
531 .unwrap_or(false)
532 }
533
534 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
535 if self.is_public(node, content) {
536 Visibility::Public
537 } else {
538 Visibility::Private
539 }
540 }
541
542 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
543 match symbol.kind {
544 crate::SymbolKind::Function => {
545 let name = symbol.name.as_str();
546 name.starts_with("Test")
547 || name.starts_with("Benchmark")
548 || name.starts_with("Example")
549 }
550 _ => false,
551 }
552 }
553
554 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
555 None
556 }
557
558 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
559 None
561 }
562
563 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
564 Vec::new()
565 }
566
567 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
568 node.child_by_field_name("body")
569 }
570
571 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
572 false
573 }
574
575 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
576 let name_node = node.child_by_field_name("name")?;
577 Some(&content[name_node.byte_range()])
578 }
579
580 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
581 if path.extension()?.to_str()? != "go" {
582 return None;
583 }
584 path.parent()?.to_str().map(|s| s.to_string())
586 }
587
588 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
589 vec![format!("{}/*.go", module)]
591 }
592
593 fn lang_key(&self) -> &'static str {
596 "go"
597 }
598
599 fn resolve_local_import(
600 &self,
601 import_path: &str,
602 current_file: &Path,
603 _project_root: &Path,
604 ) -> Option<PathBuf> {
605 if let Some(go_mod_path) = find_go_mod(current_file)
607 && let Some(module) = parse_go_mod(&go_mod_path)
608 {
609 let module_root = go_mod_path.parent()?;
611 if let Some(local_path) = resolve_go_import(import_path, &module, module_root)
612 && local_path.exists()
613 && local_path.is_dir()
614 {
615 return Some(local_path);
616 }
617 }
618 None
619 }
620
621 fn resolve_external_import(
622 &self,
623 import_name: &str,
624 _project_root: &Path,
625 ) -> Option<ResolvedPackage> {
626 if is_go_stdlib_import(import_name)
628 && let Some(stdlib) = find_go_stdlib()
629 && let Some(pkg) = resolve_go_stdlib_import(import_name, &stdlib)
630 {
631 return Some(pkg);
632 }
633
634 if let Some(mod_cache) = find_go_mod_cache() {
636 return resolve_go_mod_cache_import(import_name, &mod_cache);
637 }
638
639 None
640 }
641
642 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
643 is_go_stdlib_import(import_name)
644 }
645
646 fn get_version(&self, _project_root: &Path) -> Option<String> {
647 get_go_version()
648 }
649
650 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
651 find_go_mod_cache()
652 }
653
654 fn indexable_extensions(&self) -> &'static [&'static str] {
655 &["go"]
656 }
657
658 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
659 find_go_stdlib()
660 }
661
662 fn package_sources(&self, project_root: &Path) -> Vec<crate::PackageSource> {
663 use crate::{PackageSource, PackageSourceKind};
664 let mut sources = Vec::new();
665 if let Some(stdlib) = self.find_stdlib(project_root) {
666 sources.push(PackageSource {
667 name: "stdlib",
668 path: stdlib,
669 kind: PackageSourceKind::Recursive,
670 version_specific: true,
671 });
672 }
673 if let Some(cache) = self.find_package_cache(project_root) {
674 sources.push(PackageSource {
675 name: "mod-cache",
676 path: cache,
677 kind: PackageSourceKind::Recursive,
678 version_specific: false,
679 });
680 }
681 sources
682 }
683
684 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
685 if name.starts_with('.') {
687 return true;
688 }
689 if is_dir && (name == "vendor" || name == "internal" || name == "testdata") {
691 return true;
692 }
693 if !is_dir && !name.ends_with(".go") {
695 return true;
696 }
697 if name.ends_with("_test.go") {
699 return true;
700 }
701 false
702 }
703
704 fn package_module_name(&self, entry_name: &str) -> String {
705 entry_name
706 .strip_suffix(".go")
707 .unwrap_or(entry_name)
708 .to_string()
709 }
710
711 fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
712 self.discover_recursive_packages(&source.path, &source.path)
713 }
714
715 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
716 if path.is_file() && path.extension().map(|e| e == "go").unwrap_or(false) {
717 return Some(path.to_path_buf());
718 }
719 if path.is_dir() {
722 if let Ok(entries) = std::fs::read_dir(path) {
723 for entry in entries.flatten() {
724 let name = entry.file_name();
725 let name_str = name.to_string_lossy();
726 if name_str.ends_with(".go") && !name_str.ends_with("_test.go") {
727 return Some(path.to_path_buf());
728 }
729 }
730 }
731 }
732 None
733 }
734}
735
736impl Go {
737 fn parse_import_spec(node: &Node, content: &str, line: usize) -> Option<Import> {
738 let mut path = String::new();
739 let mut alias = None;
740
741 let mut cursor = node.walk();
742 for child in node.children(&mut cursor) {
743 match child.kind() {
744 "interpreted_string_literal" => {
745 let text = &content[child.byte_range()];
746 path = text.trim_matches('"').to_string();
747 }
748 "package_identifier" | "blank_identifier" | "dot" => {
749 alias = Some(content[child.byte_range()].to_string());
750 }
751 _ => {}
752 }
753 }
754
755 if path.is_empty() {
756 return None;
757 }
758
759 let is_wildcard = alias.as_deref() == Some(".");
760 Some(Import {
761 module: path,
762 names: Vec::new(),
763 alias,
764 is_wildcard,
765 is_relative: false, line,
767 })
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 #[test]
776 fn test_parse_go_mod() {
777 let content = r#"
778module github.com/user/project
779
780go 1.21
781
782require (
783 github.com/pkg/errors v0.9.1
784 golang.org/x/sync v0.3.0
785)
786"#;
787 let module = parse_go_mod_content(content).unwrap();
788 assert_eq!(module.path, "github.com/user/project");
789 assert_eq!(module.go_version, Some("1.21".to_string()));
790 }
791
792 #[test]
793 fn test_resolve_internal_import() {
794 let module = GoModule {
795 path: "github.com/user/project".to_string(),
796 go_version: Some("1.21".to_string()),
797 };
798
799 let result = resolve_go_import(
801 "github.com/user/project/pkg/utils",
802 &module,
803 Path::new("/fake/root"),
804 );
805 assert_eq!(result, Some(PathBuf::from("/fake/root/pkg/utils")));
806
807 let result = resolve_go_import("github.com/other/lib", &module, Path::new("/fake/root"));
809 assert!(result.is_none());
810 }
811
812 #[test]
815 fn unused_node_kinds_audit() {
816 use crate::validate_unused_kinds_audit;
817
818 #[rustfmt::skip]
819 let documented_unused: &[&str] = &[
820 "blank_identifier", "field_declaration", "field_declaration_list", "field_identifier", "identifier", "package_clause", "package_identifier", "parameter_declaration", "statement_list", "variadic_parameter_declaration", "default_case", "for_clause", "import_spec", "import_spec_list", "method_elem", "range_clause", "call_expression", "index_expression", "parenthesized_expression","selector_expression", "slice_expression", "type_assertion_expression", "type_conversion_expression", "type_instantiation_expression", "unary_expression", "array_type", "channel_type", "implicit_length_array_type", "function_type", "generic_type", "interface_type", "map_type", "negated_type", "parenthesized_type", "pointer_type", "qualified_type", "slice_type", "struct_type", "type_arguments", "type_constraint", "type_elem", "type_identifier", "type_parameter_declaration", "type_parameter_list", "assignment_statement", "const_declaration", "dec_statement", "expression_list", "expression_statement", "inc_statement", "short_var_declaration", "type_alias", "type_declaration", "var_declaration", "empty_statement", "fallthrough_statement", "go_statement", "labeled_statement", "receive_statement", "send_statement", ];
892
893 validate_unused_kinds_audit(&Go, documented_unused)
894 .expect("Go unused node kinds audit failed");
895 }
896}