Skip to main content

normalize_languages/
go.rs

1//! Go language support.
2
3use 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
12/// Go language support.
13pub 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        // Go type_spec wraps the actual type (struct_type, interface_type, etc.)
45        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                    // import "path" or import alias "path"
98                    if let Some(imp) = Self::parse_import_spec(&child, content, line) {
99                        imports.push(imp);
100                    }
101                }
102                "import_spec_list" => {
103                    // Grouped imports
104                    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        // Go: import "pkg" or import alias "pkg"
122        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
183// =============================================================================
184// Go Module Resolver
185// =============================================================================
186
187/// Module resolver for Go.
188///
189/// Uses `go.mod` at the workspace root to extract the module path.
190/// In Go, a package = a directory, so all `.go` files in the same directory
191/// belong to the same package (same import path).
192pub 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            // Parse `module <path>` line
201            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        // Package import path = module path + path from root to file's directory
225        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        // Check if the import path starts with our module path
266        for (module_path, module_root) in &cfg.path_mappings {
267            if raw == module_path {
268                // Importing the root package itself
269                return Resolution::Resolved(module_root.clone(), String::new());
270            }
271            if let Some(rest) = raw.strip_prefix(&format!("{}/", module_path)) {
272                // rest is the subdirectory path within the module
273                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        // Not in this module (stdlib or third-party)
282        Resolution::NotFound
283    }
284}
285
286/// Extract the Go package comment from source.
287///
288/// The Go convention is a block of `//` comments immediately before
289/// the `package` keyword. Scans backwards from the `package` line.
290/// A blank line between the comment and `package` means it is NOT a doc comment.
291fn extract_go_package_doc(src: &str) -> Option<String> {
292    let lines: Vec<&str> = src.lines().collect();
293    // Find the package declaration line
294    let pkg_idx = lines.iter().position(|l| {
295        let t = l.trim();
296        t.starts_with("package ") || t == "package"
297    })?;
298
299    // A blank line immediately before package means no doc comment
300    if pkg_idx > 0 && lines[pkg_idx - 1].trim().is_empty() {
301        return None;
302    }
303
304    // Collect comment lines immediately preceding the package line
305    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    // Reverse to get lines in original order and strip `//` prefix
322    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, // Go doesn't have relative imports in the traditional sense
364            line,
365        })
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    /// Documents node kinds that exist in the Go grammar but aren't used in trait methods.
374    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
375    #[test]
376    fn unused_node_kinds_audit() {
377        use crate::validate_unused_kinds_audit;
378
379        #[rustfmt::skip]
380        let documented_unused: &[&str] = &[
381            // STRUCTURAL
382            "blank_identifier",        // _
383            "field_declaration",       // struct field
384            "field_declaration_list",  // struct body
385            "field_identifier",        // field name              // too common          // package foo
386            "package_identifier",      // package name
387            "parameter_declaration",   // func param
388            "statement_list",          // block contents
389            "variadic_parameter_declaration", // ...T
390
391            // CLAUSE
392            "default_case",            // default:
393            "for_clause",              // for init; cond; post
394            "import_spec",             // import spec
395            "import_spec_list",        // import block
396            "method_elem",             // interface method
397            "range_clause",            // for range
398
399            // EXPRESSION         // foo()
400            "index_expression",        // arr[i]// (expr)     // foo.bar
401            "slice_expression",        // arr[1:3]
402            "type_assertion_expression", // x.(T)
403            "type_conversion_expression", // T(x)
404            "type_instantiation_expression", // generic instantiation
405            "unary_expression",        // -x, !x
406
407            // TYPE
408            "array_type",              // [N]T
409            "channel_type",            // chan T
410            "implicit_length_array_type", // [...]T
411            "function_type",           // func(T) U
412            "generic_type",            // T[U]
413            "interface_type",          // interface{}
414            "map_type",                // map[K]V
415            "negated_type",            // ~T
416            "parenthesized_type",      // (T)
417            "pointer_type",            // *T
418            "qualified_type",          // pkg.Type
419            "slice_type",              // []T
420            "struct_type",             // struct{}
421            "type_arguments",          // [T, U]
422            "type_constraint",         // T constraint
423            "type_elem",               // type element         // type name
424            "type_parameter_declaration", // [T any]
425            "type_parameter_list",     // type params
426
427            // DECLARATION
428            "assignment_statement",    // x = y       // const x = 1
429            "dec_statement",           // x--
430            "expression_list",         // a, b, c
431            "expression_statement",    // expr
432            "inc_statement",           // x++
433            "short_var_declaration",   // x := y
434            "type_alias",              // type X = Y        // type X struct{}         // var x int
435
436            // CONTROL FLOW DETAILS
437            "empty_statement",         // ;
438            "fallthrough_statement",   // fallthrough
439            "go_statement",            // go foo()
440            "labeled_statement",       // label:
441            "receive_statement",       // <-ch
442            "send_statement",          // ch <- x
443            // control flow — not extracted as symbols
444            "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}