Skip to main content

normalize_languages/
go.rs

1//! Go language support.
2
3use crate::docstring::extract_preceding_prefix_comments;
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// Go language support.
8pub struct Go;
9
10impl Language for Go {
11    fn name(&self) -> &'static str {
12        "Go"
13    }
14    fn extensions(&self) -> &'static [&'static str] {
15        &["go"]
16    }
17    fn grammar_name(&self) -> &'static str {
18        "go"
19    }
20
21    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
22        Some(self)
23    }
24
25    fn signature_suffix(&self) -> &'static str {
26        " {}"
27    }
28
29    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
30        extract_preceding_prefix_comments(node, content, "//")
31    }
32
33    fn refine_kind(
34        &self,
35        node: &Node,
36        _content: &str,
37        tag_kind: crate::SymbolKind,
38    ) -> crate::SymbolKind {
39        // Go type_spec wraps the actual type (struct_type, interface_type, etc.)
40        if node.kind() == "type_spec"
41            && let Some(type_node) = node.child_by_field_name("type")
42        {
43            return match type_node.kind() {
44                "struct_type" => crate::SymbolKind::Struct,
45                "interface_type" => crate::SymbolKind::Interface,
46                _ => tag_kind,
47            };
48        }
49        tag_kind
50    }
51
52    fn build_signature(&self, node: &Node, content: &str) -> String {
53        let name = match self.node_name(node, content) {
54            Some(n) => n,
55            None => {
56                return content[node.byte_range()]
57                    .lines()
58                    .next()
59                    .unwrap_or("")
60                    .trim()
61                    .to_string();
62            }
63        };
64        match node.kind() {
65            "function_declaration" | "method_declaration" => {
66                let params = node
67                    .child_by_field_name("parameters")
68                    .map(|p| content[p.byte_range()].to_string())
69                    .unwrap_or_else(|| "()".to_string());
70                format!("func {}{}", name, params)
71            }
72            "type_spec" => format!("type {}", name),
73            _ => {
74                let text = &content[node.byte_range()];
75                text.lines().next().unwrap_or(text).trim().to_string()
76            }
77        }
78    }
79
80    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
81        if node.kind() != "import_declaration" {
82            return Vec::new();
83        }
84
85        let mut imports = Vec::new();
86        let line = node.start_position().row + 1;
87
88        let mut cursor = node.walk();
89        for child in node.children(&mut cursor) {
90            match child.kind() {
91                "import_spec" => {
92                    // import "path" or import alias "path"
93                    if let Some(imp) = Self::parse_import_spec(&child, content, line) {
94                        imports.push(imp);
95                    }
96                }
97                "import_spec_list" => {
98                    // Grouped imports
99                    let mut list_cursor = child.walk();
100                    for spec in child.children(&mut list_cursor) {
101                        if spec.kind() == "import_spec"
102                            && let Some(imp) = Self::parse_import_spec(&spec, content, line)
103                        {
104                            imports.push(imp);
105                        }
106                    }
107                }
108                _ => {}
109            }
110        }
111
112        imports
113    }
114
115    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
116        // Go: import "pkg" or import alias "pkg"
117        if let Some(ref alias) = import.alias {
118            format!("import {} \"{}\"", alias, import.module)
119        } else {
120            format!("import \"{}\"", import.module)
121        }
122    }
123
124    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
125        let is_exported = self
126            .node_name(node, content)
127            .and_then(|n| n.chars().next())
128            .map(|c| c.is_uppercase())
129            .unwrap_or(false);
130        if is_exported {
131            Visibility::Public
132        } else {
133            Visibility::Private
134        }
135    }
136
137    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
138        match symbol.kind {
139            crate::SymbolKind::Function => {
140                let name = symbol.name.as_str();
141                name.starts_with("Test")
142                    || name.starts_with("Benchmark")
143                    || name.starts_with("Example")
144            }
145            _ => false,
146        }
147    }
148
149    fn test_file_globs(&self) -> &'static [&'static str] {
150        &["**/*_test.go"]
151    }
152
153    fn extract_module_doc(&self, src: &str) -> Option<String> {
154        extract_go_package_doc(src)
155    }
156
157    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
158        node.child_by_field_name("body")
159    }
160
161    fn analyze_container_body(
162        &self,
163        body_node: &Node,
164        content: &str,
165        inner_indent: &str,
166    ) -> Option<ContainerBody> {
167        crate::body::analyze_brace_body(body_node, content, inner_indent)
168    }
169}
170
171impl LanguageSymbols for Go {}
172
173/// Extract the Go package comment from source.
174///
175/// The Go convention is a block of `//` comments immediately before
176/// the `package` keyword. Scans backwards from the `package` line.
177/// A blank line between the comment and `package` means it is NOT a doc comment.
178fn extract_go_package_doc(src: &str) -> Option<String> {
179    let lines: Vec<&str> = src.lines().collect();
180    // Find the package declaration line
181    let pkg_idx = lines.iter().position(|l| {
182        let t = l.trim();
183        t.starts_with("package ") || t == "package"
184    })?;
185
186    // A blank line immediately before package means no doc comment
187    if pkg_idx > 0 && lines[pkg_idx - 1].trim().is_empty() {
188        return None;
189    }
190
191    // Collect comment lines immediately preceding the package line
192    let mut doc_lines: Vec<&str> = Vec::new();
193    let mut idx = pkg_idx;
194    while idx > 0 {
195        idx -= 1;
196        let t = lines[idx].trim();
197        if t.starts_with("//") {
198            doc_lines.push(t);
199        } else {
200            break;
201        }
202    }
203
204    if doc_lines.is_empty() {
205        return None;
206    }
207
208    // Reverse to get lines in original order and strip `//` prefix
209    doc_lines.reverse();
210    let text = doc_lines
211        .iter()
212        .map(|l| l.trim_start_matches("//").trim_start())
213        .collect::<Vec<_>>()
214        .join("\n")
215        .trim()
216        .to_string();
217
218    if text.is_empty() { None } else { Some(text) }
219}
220
221impl Go {
222    fn parse_import_spec(node: &Node, content: &str, line: usize) -> Option<Import> {
223        let mut path = String::new();
224        let mut alias = None;
225
226        let mut cursor = node.walk();
227        for child in node.children(&mut cursor) {
228            match child.kind() {
229                "interpreted_string_literal" => {
230                    let text = &content[child.byte_range()];
231                    path = text.trim_matches('"').to_string();
232                }
233                "package_identifier" | "blank_identifier" | "dot" => {
234                    alias = Some(content[child.byte_range()].to_string());
235                }
236                _ => {}
237            }
238        }
239
240        if path.is_empty() {
241            return None;
242        }
243
244        let is_wildcard = alias.as_deref() == Some(".");
245        Some(Import {
246            module: path,
247            names: Vec::new(),
248            alias,
249            is_wildcard,
250            is_relative: false, // Go doesn't have relative imports in the traditional sense
251            line,
252        })
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    /// Documents node kinds that exist in the Go grammar but aren't used in trait methods.
261    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
262    #[test]
263    fn unused_node_kinds_audit() {
264        use crate::validate_unused_kinds_audit;
265
266        #[rustfmt::skip]
267        let documented_unused: &[&str] = &[
268            // STRUCTURAL
269            "blank_identifier",        // _
270            "field_declaration",       // struct field
271            "field_declaration_list",  // struct body
272            "field_identifier",        // field name              // too common          // package foo
273            "package_identifier",      // package name
274            "parameter_declaration",   // func param
275            "statement_list",          // block contents
276            "variadic_parameter_declaration", // ...T
277
278            // CLAUSE
279            "default_case",            // default:
280            "for_clause",              // for init; cond; post
281            "import_spec",             // import spec
282            "import_spec_list",        // import block
283            "method_elem",             // interface method
284            "range_clause",            // for range
285
286            // EXPRESSION         // foo()
287            "index_expression",        // arr[i]// (expr)     // foo.bar
288            "slice_expression",        // arr[1:3]
289            "type_assertion_expression", // x.(T)
290            "type_conversion_expression", // T(x)
291            "type_instantiation_expression", // generic instantiation
292            "unary_expression",        // -x, !x
293
294            // TYPE
295            "array_type",              // [N]T
296            "channel_type",            // chan T
297            "implicit_length_array_type", // [...]T
298            "function_type",           // func(T) U
299            "generic_type",            // T[U]
300            "interface_type",          // interface{}
301            "map_type",                // map[K]V
302            "negated_type",            // ~T
303            "parenthesized_type",      // (T)
304            "pointer_type",            // *T
305            "qualified_type",          // pkg.Type
306            "slice_type",              // []T
307            "struct_type",             // struct{}
308            "type_arguments",          // [T, U]
309            "type_constraint",         // T constraint
310            "type_elem",               // type element         // type name
311            "type_parameter_declaration", // [T any]
312            "type_parameter_list",     // type params
313
314            // DECLARATION
315            "assignment_statement",    // x = y       // const x = 1
316            "dec_statement",           // x--
317            "expression_list",         // a, b, c
318            "expression_statement",    // expr
319            "inc_statement",           // x++
320            "short_var_declaration",   // x := y
321            "type_alias",              // type X = Y        // type X struct{}         // var x int
322
323            // CONTROL FLOW DETAILS
324            "empty_statement",         // ;
325            "fallthrough_statement",   // fallthrough
326            "go_statement",            // go foo()
327            "labeled_statement",       // label:
328            "receive_statement",       // <-ch
329            "send_statement",          // ch <- x
330            // control flow — not extracted as symbols
331            "return_statement",
332            "continue_statement",
333            "break_statement",
334            "if_statement",
335            "for_statement",
336            "goto_statement",
337            "expression_switch_statement",
338            "expression_case",
339            "type_case",
340            "type_switch_statement",
341            "select_statement",
342            "block",
343            "defer_statement",
344            "binary_expression",
345            "communication_case",
346        ];
347
348        validate_unused_kinds_audit(&Go, documented_unused)
349            .expect("Go unused node kinds audit failed");
350    }
351}