Skip to main content

normalize_languages/
go.rs

1//! Go language support.
2
3use 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// ============================================================================
10// Go module parsing (for local import resolution)
11// ============================================================================
12
13/// Information from a go.mod file
14#[derive(Debug, Clone)]
15struct GoModule {
16    /// Module path (e.g., "github.com/user/project")
17    path: String,
18    /// Go version (e.g., "1.21")
19    #[allow(dead_code)]
20    go_version: Option<String>,
21}
22
23/// Parse a go.mod file to extract module information.
24fn 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
29/// Parse go.mod content string.
30fn 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        // module github.com/user/project
38        if line.starts_with("module ") {
39            module_path = Some(line.trim_start_matches("module ").trim().to_string());
40        }
41
42        // go 1.21
43        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
51/// Find go.mod by walking up from a directory.
52fn 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
73/// Resolve a Go import path to a local directory path.
74///
75/// Returns the computed path if the import is within the module, None for external imports.
76/// Does not check if the path exists - caller should verify.
77fn resolve_go_import(import_path: &str, module: &GoModule, project_root: &Path) -> Option<PathBuf> {
78    // Check if import is within our module
79    if !import_path.starts_with(&module.path) {
80        return None; // External import
81    }
82
83    // Get the relative path after the module prefix
84    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
96// ============================================================================
97// Go external package resolution
98// ============================================================================
99
100/// Get Go version.
101pub 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        // "go version go1.21.0 linux/amd64" -> "1.21"
107        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                // Take major.minor only
111                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
122/// Find Go stdlib directory (GOROOT/src).
123pub fn find_go_stdlib() -> Option<PathBuf> {
124    // Try GOROOT env var
125    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    // Try `go env GOROOT`
133    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    // Common locations
144    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
154/// Check if a Go import is a stdlib import (no dots in first path segment).
155fn 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
160/// Resolve a Go stdlib import to its source location.
161fn 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
178/// Find Go module cache directory.
179///
180/// Uses GOMODCACHE env var, falls back to ~/go/pkg/mod
181pub fn find_go_mod_cache() -> Option<PathBuf> {
182    // Check GOMODCACHE env var
183    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    // Fall back to ~/go/pkg/mod using HOME env var
191    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    // Windows fallback
199    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
209/// Resolve a Go import from mod cache to its source location.
210///
211/// Import paths like "github.com/user/repo/pkg" are mapped to
212/// $GOMODCACHE/github.com/user/repo@version/pkg
213fn resolve_go_mod_cache_import(import_path: &str, mod_cache: &Path) -> Option<ResolvedPackage> {
214    // Skip standard library imports (no dots in first segment)
215    let first_segment = import_path.split('/').next()?;
216    if !first_segment.contains('.') {
217        // This is stdlib (fmt, os, etc.) - not in mod cache
218        return None;
219    }
220
221    // Find the module in cache
222    // Import path: github.com/user/repo/internal/pkg
223    // Cache path: github.com/user/repo@v1.2.3/internal/pkg
224
225    // We need to find the right version directory
226    // Start with the full path and try progressively shorter prefixes
227    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        // The parent directory might contain version directories
234        if let Some(parent) = module_dir.parent() {
235            if parent.is_dir() {
236                // Look for versioned directories matching this module
237                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                        // Match module@version pattern
243                        if name_str.starts_with(&format!("{}@", module_name)) {
244                            let versioned_path = entry.path();
245                            // Add remaining path components
246                            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
274// ============================================================================
275// Go language support
276// ============================================================================
277
278/// Go language support.
279pub 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        &[] // Go types don't have children in the tree-sitter sense
298    }
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"] // The actual type is in type_spec, not type_declaration
306    }
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 // Go types are extracted via extract_type
419    }
420
421    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
422        // Go type_spec: name field + type field (struct_type, interface_type, etc.)
423        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                    // import "path" or import alias "path"
472                    if let Some(imp) = Self::parse_import_spec(&child, content, line) {
473                        imports.push(imp);
474                    }
475                }
476                "import_spec_list" => {
477                    // Grouped imports
478                    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        // Go: import "pkg" or import alias "pkg"
496        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        // Go exports are determined by uppercase first letter
505        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        // Go doc comments could be extracted but need special handling
560        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        // Go uses directories as packages, not individual files
585        path.parent()?.to_str().map(|s| s.to_string())
586    }
587
588    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
589        // Go packages are directories, look for .go files within
590        vec![format!("{}/*.go", module)]
591    }
592
593    // === Import Resolution ===
594
595    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        // Find go.mod to understand module boundaries
606        if let Some(go_mod_path) = find_go_mod(current_file)
607            && let Some(module) = parse_go_mod(&go_mod_path)
608        {
609            // Try local resolution within the module
610            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        // Check stdlib first
627        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        // Then mod cache
635        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        // Skip dotfiles
686        if name.starts_with('.') {
687            return true;
688        }
689        // Skip common non-source directories
690        if is_dir && (name == "vendor" || name == "internal" || name == "testdata") {
691            return true;
692        }
693        // Skip non-Go files
694        if !is_dir && !name.ends_with(".go") {
695            return true;
696        }
697        // Skip test files
698        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        // For directories, Go packages don't have a single entry point
720        // Return the directory itself if it contains .go files
721        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, // Go doesn't have relative imports in the traditional sense
766            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        // Internal import
800        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        // External import
808        let result = resolve_go_import("github.com/other/lib", &module, Path::new("/fake/root"));
809        assert!(result.is_none());
810    }
811
812    /// Documents node kinds that exist in the Go grammar but aren't used in trait methods.
813    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
814    #[test]
815    fn unused_node_kinds_audit() {
816        use crate::validate_unused_kinds_audit;
817
818        #[rustfmt::skip]
819        let documented_unused: &[&str] = &[
820            // STRUCTURAL
821            "blank_identifier",        // _
822            "field_declaration",       // struct field
823            "field_declaration_list",  // struct body
824            "field_identifier",        // field name
825            "identifier",              // too common
826            "package_clause",          // package foo
827            "package_identifier",      // package name
828            "parameter_declaration",   // func param
829            "statement_list",          // block contents
830            "variadic_parameter_declaration", // ...T
831
832            // CLAUSE
833            "default_case",            // default:
834            "for_clause",              // for init; cond; post
835            "import_spec",             // import spec
836            "import_spec_list",        // import block
837            "method_elem",             // interface method
838            "range_clause",            // for range
839
840            // EXPRESSION
841            "call_expression",         // foo()
842            "index_expression",        // arr[i]
843            "parenthesized_expression",// (expr)
844            "selector_expression",     // foo.bar
845            "slice_expression",        // arr[1:3]
846            "type_assertion_expression", // x.(T)
847            "type_conversion_expression", // T(x)
848            "type_instantiation_expression", // generic instantiation
849            "unary_expression",        // -x, !x
850
851            // TYPE
852            "array_type",              // [N]T
853            "channel_type",            // chan T
854            "implicit_length_array_type", // [...]T
855            "function_type",           // func(T) U
856            "generic_type",            // T[U]
857            "interface_type",          // interface{}
858            "map_type",                // map[K]V
859            "negated_type",            // ~T
860            "parenthesized_type",      // (T)
861            "pointer_type",            // *T
862            "qualified_type",          // pkg.Type
863            "slice_type",              // []T
864            "struct_type",             // struct{}
865            "type_arguments",          // [T, U]
866            "type_constraint",         // T constraint
867            "type_elem",               // type element
868            "type_identifier",         // type name
869            "type_parameter_declaration", // [T any]
870            "type_parameter_list",     // type params
871
872            // DECLARATION
873            "assignment_statement",    // x = y
874            "const_declaration",       // const x = 1
875            "dec_statement",           // x--
876            "expression_list",         // a, b, c
877            "expression_statement",    // expr
878            "inc_statement",           // x++
879            "short_var_declaration",   // x := y
880            "type_alias",              // type X = Y
881            "type_declaration",        // type X struct{}
882            "var_declaration",         // var x int
883
884            // CONTROL FLOW DETAILS
885            "empty_statement",         // ;
886            "fallthrough_statement",   // fallthrough
887            "go_statement",            // go foo()
888            "labeled_statement",       // label:
889            "receive_statement",       // <-ch
890            "send_statement",          // ch <- x
891        ];
892
893        validate_unused_kinds_audit(&Go, documented_unused)
894            .expect("Go unused node kinds audit failed");
895    }
896}