1use std::path::{Path, PathBuf};
4
5use crate::docstring::extract_preceding_prefix_comments;
6use crate::{
7 ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
8 Resolution, ResolverConfig, Visibility,
9};
10use tree_sitter::Node;
11
12pub struct Go;
14
15impl Language for Go {
16 fn name(&self) -> &'static str {
17 "Go"
18 }
19 fn extensions(&self) -> &'static [&'static str] {
20 &["go"]
21 }
22 fn grammar_name(&self) -> &'static str {
23 "go"
24 }
25
26 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
27 Some(self)
28 }
29
30 fn signature_suffix(&self) -> &'static str {
31 " {}"
32 }
33
34 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
35 extract_preceding_prefix_comments(node, content, "//")
36 }
37
38 fn refine_kind(
39 &self,
40 node: &Node,
41 _content: &str,
42 tag_kind: crate::SymbolKind,
43 ) -> crate::SymbolKind {
44 if node.kind() == "type_spec"
46 && let Some(type_node) = node.child_by_field_name("type")
47 {
48 return match type_node.kind() {
49 "struct_type" => crate::SymbolKind::Struct,
50 "interface_type" => crate::SymbolKind::Interface,
51 _ => tag_kind,
52 };
53 }
54 tag_kind
55 }
56
57 fn build_signature(&self, node: &Node, content: &str) -> String {
58 let name = match self.node_name(node, content) {
59 Some(n) => n,
60 None => {
61 return content[node.byte_range()]
62 .lines()
63 .next()
64 .unwrap_or("")
65 .trim()
66 .to_string();
67 }
68 };
69 match node.kind() {
70 "function_declaration" | "method_declaration" => {
71 let params = node
72 .child_by_field_name("parameters")
73 .map(|p| content[p.byte_range()].to_string())
74 .unwrap_or_else(|| "()".to_string());
75 format!("func {}{}", name, params)
76 }
77 "type_spec" => format!("type {}", name),
78 _ => {
79 let text = &content[node.byte_range()];
80 text.lines().next().unwrap_or(text).trim().to_string()
81 }
82 }
83 }
84
85 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
86 if node.kind() != "import_declaration" {
87 return Vec::new();
88 }
89
90 let mut imports = Vec::new();
91 let line = node.start_position().row + 1;
92
93 let mut cursor = node.walk();
94 for child in node.children(&mut cursor) {
95 match child.kind() {
96 "import_spec" => {
97 if let Some(imp) = Self::parse_import_spec(&child, content, line) {
99 imports.push(imp);
100 }
101 }
102 "import_spec_list" => {
103 let mut list_cursor = child.walk();
105 for spec in child.children(&mut list_cursor) {
106 if spec.kind() == "import_spec"
107 && let Some(imp) = Self::parse_import_spec(&spec, content, line)
108 {
109 imports.push(imp);
110 }
111 }
112 }
113 _ => {}
114 }
115 }
116
117 imports
118 }
119
120 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
121 if let Some(ref alias) = import.alias {
123 format!("import {} \"{}\"", alias, import.module)
124 } else {
125 format!("import \"{}\"", import.module)
126 }
127 }
128
129 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
130 let is_exported = self
131 .node_name(node, content)
132 .and_then(|n| n.chars().next())
133 .map(|c| c.is_uppercase())
134 .unwrap_or(false);
135 if is_exported {
136 Visibility::Public
137 } else {
138 Visibility::Private
139 }
140 }
141
142 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
143 match symbol.kind {
144 crate::SymbolKind::Function => {
145 let name = symbol.name.as_str();
146 name.starts_with("Test")
147 || name.starts_with("Benchmark")
148 || name.starts_with("Example")
149 }
150 _ => false,
151 }
152 }
153
154 fn test_file_globs(&self) -> &'static [&'static str] {
155 &["**/*_test.go"]
156 }
157
158 fn extract_module_doc(&self, src: &str) -> Option<String> {
159 extract_go_package_doc(src)
160 }
161
162 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
163 node.child_by_field_name("body")
164 }
165
166 fn analyze_container_body(
167 &self,
168 body_node: &Node,
169 content: &str,
170 inner_indent: &str,
171 ) -> Option<ContainerBody> {
172 crate::body::analyze_brace_body(body_node, content, inner_indent)
173 }
174
175 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
176 static RESOLVER: GoModuleResolver = GoModuleResolver;
177 Some(&RESOLVER)
178 }
179}
180
181impl LanguageSymbols for Go {}
182
183pub struct GoModuleResolver;
193
194impl ModuleResolver for GoModuleResolver {
195 fn workspace_config(&self, root: &Path) -> ResolverConfig {
196 let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
197
198 let go_mod = root.join("go.mod");
199 if let Ok(content) = std::fs::read_to_string(&go_mod) {
200 for line in content.lines() {
202 let trimmed = line.trim();
203 if let Some(module_path) = trimmed.strip_prefix("module ") {
204 let module_path = module_path.trim().to_string();
205 path_mappings.push((module_path, root.to_path_buf()));
206 break;
207 }
208 }
209 }
210
211 ResolverConfig {
212 workspace_root: root.to_path_buf(),
213 path_mappings,
214 search_roots: Vec::new(),
215 }
216 }
217
218 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
219 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
220 if ext != "go" {
221 return Vec::new();
222 }
223
224 for (module_path, module_root) in &cfg.path_mappings {
226 let file_dir = match file.parent() {
227 Some(d) => d,
228 None => continue,
229 };
230 if let Ok(rel) = file_dir.strip_prefix(module_root) {
231 let rel_str = rel
232 .components()
233 .filter_map(|c| {
234 if let std::path::Component::Normal(s) = c {
235 s.to_str()
236 } else {
237 None
238 }
239 })
240 .collect::<Vec<_>>()
241 .join("/");
242
243 let canonical = if rel_str.is_empty() {
244 module_path.clone()
245 } else {
246 format!("{}/{}", module_path, rel_str)
247 };
248 return vec![ModuleId {
249 canonical_path: canonical,
250 }];
251 }
252 }
253
254 Vec::new()
255 }
256
257 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
258 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
259 if ext != "go" {
260 return Resolution::NotApplicable;
261 }
262
263 let raw = &spec.raw;
264
265 for (module_path, module_root) in &cfg.path_mappings {
267 if raw == module_path {
268 return Resolution::Resolved(module_root.clone(), String::new());
270 }
271 if let Some(rest) = raw.strip_prefix(&format!("{}/", module_path)) {
272 let target_dir = module_root.join(rest);
274 if target_dir.is_dir() {
275 return Resolution::Resolved(target_dir, String::new());
276 }
277 return Resolution::NotFound;
278 }
279 }
280
281 Resolution::NotFound
283 }
284}
285
286fn extract_go_package_doc(src: &str) -> Option<String> {
292 let lines: Vec<&str> = src.lines().collect();
293 let pkg_idx = lines.iter().position(|l| {
295 let t = l.trim();
296 t.starts_with("package ") || t == "package"
297 })?;
298
299 if pkg_idx > 0 && lines[pkg_idx - 1].trim().is_empty() {
301 return None;
302 }
303
304 let mut doc_lines: Vec<&str> = Vec::new();
306 let mut idx = pkg_idx;
307 while idx > 0 {
308 idx -= 1;
309 let t = lines[idx].trim();
310 if t.starts_with("//") {
311 doc_lines.push(t);
312 } else {
313 break;
314 }
315 }
316
317 if doc_lines.is_empty() {
318 return None;
319 }
320
321 doc_lines.reverse();
323 let text = doc_lines
324 .iter()
325 .map(|l| l.trim_start_matches("//").trim_start())
326 .collect::<Vec<_>>()
327 .join("\n")
328 .trim()
329 .to_string();
330
331 if text.is_empty() { None } else { Some(text) }
332}
333
334impl Go {
335 fn parse_import_spec(node: &Node, content: &str, line: usize) -> Option<Import> {
336 let mut path = String::new();
337 let mut alias = None;
338
339 let mut cursor = node.walk();
340 for child in node.children(&mut cursor) {
341 match child.kind() {
342 "interpreted_string_literal" => {
343 let text = &content[child.byte_range()];
344 path = text.trim_matches('"').to_string();
345 }
346 "package_identifier" | "blank_identifier" | "dot" => {
347 alias = Some(content[child.byte_range()].to_string());
348 }
349 _ => {}
350 }
351 }
352
353 if path.is_empty() {
354 return None;
355 }
356
357 let is_wildcard = alias.as_deref() == Some(".");
358 Some(Import {
359 module: path,
360 names: Vec::new(),
361 alias,
362 is_wildcard,
363 is_relative: false, line,
365 })
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
376 fn unused_node_kinds_audit() {
377 use crate::validate_unused_kinds_audit;
378
379 #[rustfmt::skip]
380 let documented_unused: &[&str] = &[
381 "blank_identifier", "field_declaration", "field_declaration_list", "field_identifier", "package_identifier", "parameter_declaration", "statement_list", "variadic_parameter_declaration", "default_case", "for_clause", "import_spec", "import_spec_list", "method_elem", "range_clause", "index_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_parameter_declaration", "type_parameter_list", "assignment_statement", "dec_statement", "expression_list", "expression_statement", "inc_statement", "short_var_declaration", "type_alias", "empty_statement", "fallthrough_statement", "go_statement", "labeled_statement", "receive_statement", "send_statement", "return_statement",
445 "continue_statement",
446 "break_statement",
447 "if_statement",
448 "for_statement",
449 "goto_statement",
450 "expression_switch_statement",
451 "expression_case",
452 "type_case",
453 "type_switch_statement",
454 "select_statement",
455 "block",
456 "defer_statement",
457 "binary_expression",
458 "communication_case",
459 ];
460
461 validate_unused_kinds_audit(&Go, documented_unused)
462 .expect("Go unused node kinds audit failed");
463 }
464}