diff --git a/Cargo.toml b/Cargo.toml index ce0adea..0b69f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "codebank" -version = "0.3.1" +version = "0.4.0" edition = "2024" description = """ A powerful code documentation generator that creates structured markdown documentation from your codebase. -Supports multiple languages including Rust, Python, TypeScript, and C with intelligent parsing and formatting. +Supports multiple languages including Rust, Python, TypeScript, C, and Go with intelligent parsing and formatting. Features test code filtering, summary generation, and customizable documentation strategies. """ authors = ["Tyr Chen "] @@ -41,6 +41,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tree-sitter = "0.23" tree-sitter-cpp = "0.23" +tree-sitter-go = "0.23" tree-sitter-python = "0.23" tree-sitter-rust = "0.23" tree-sitter-typescript = "0.23" diff --git a/README.md b/README.md index 3c658b9..779b1bd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ CodeBank is a powerful code analysis and documentation tool that parses source c - Python (fully supported with function, class, and module parsing) - TypeScript/JavaScript (fully supported with function, class, interface, and export parsing) - C (TODO) + - Go (fully supported with package, function, struct, interface, and method parsing) - **Code Structure Analysis**: - Parses functions, modules, structs/classes, traits/interfaces diff --git a/examples/parser.rs b/examples/parser.rs index aa5c0cf..d6d2a72 100644 --- a/examples/parser.rs +++ b/examples/parser.rs @@ -1,14 +1,14 @@ use std::path::Path; use anyhow::Result; -use codebank::{CppParser, LanguageParser, PythonParser, RustParser, TypeScriptParser}; +use codebank::{CppParser, GoParser, LanguageParser, PythonParser, RustParser, TypeScriptParser}; fn main() -> Result<()> { let mut rust_parser = RustParser::try_new()?; let mut python_parser = PythonParser::try_new()?; let mut cpp_parser = CppParser::try_new()?; let mut ts_parser = TypeScriptParser::try_new()?; - + let mut go_parser = GoParser::try_new()?; let data = python_parser .parse_file(Path::new("fixtures/sample.py")) .unwrap(); @@ -32,5 +32,12 @@ fn main() -> Result<()> { .unwrap(); println!("Rust:\n{:#?}", data); + + let data = go_parser + .parse_file(Path::new("fixtures/sample.go")) + .unwrap(); + + println!("Go:\n{:#?}", data); + Ok(()) } diff --git a/fixtures/sample.go b/fixtures/sample.go new file mode 100644 index 0000000..ee54461 --- /dev/null +++ b/fixtures/sample.go @@ -0,0 +1,357 @@ +// Package example is a sample Go file for testing the parser. +package example + +import ( + "fmt" + "io" + "math" + "os" + "strings" + "sync" + "time" +) + +// Constants +const ( + Pi = 3.14159 + MaxInt = 1<<63 - 1 +) + +// Variables +var ( + globalVar = "global" + globalInt = 42 +) + +// PublicConst is an exported constant +const PublicConst = "public" + +// privateConst is a non-exported constant +const privateConst = "private" + +// Basic types +type BasicTypes struct { + Bool bool + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + Float32 float32 + Float64 float64 + Complex64 complex64 + Complex128 complex128 + String string + Byte byte + Rune rune +} + +// Custom types +type CustomInt int +type CustomString string + +// Interface +type Reader interface { + Read(p []byte) (n int, err error) +} + +// Implementation of Reader interface +type FileReader struct { + file *os.File +} + +func (fr *FileReader) Read(p []byte) (n int, err error) { + return fr.file.Read(p) +} + +// Struct with methods +type Point struct { + X, Y float64 +} + +func (p Point) Distance() float64 { + return math.Sqrt(p.X*p.X + p.Y*p.Y) +} + +func (p *Point) Scale(factor float64) { + p.X *= factor + p.Y *= factor +} + +// Function with multiple return values +func divide(a, b float64) (float64, error) { + if b == 0 { + return 0, fmt.Errorf("division by zero") + } + return a / b, nil +} + +// Function with named return values +func split(sum int) (x, y int) { + x = sum * 4 / 9 + y = sum - x + return +} + +// Function with variadic parameters +func sum(nums ...int) int { + total := 0 + for _, num := range nums { + total += num + } + return total +} + +// Method with pointer receiver +func (p *Point) Move(dx, dy float64) { + p.X += dx + p.Y += dy +} + +// Goroutine example +func say(s string) { + for i := 0; i < 5; i++ { + time.Sleep(100 * time.Millisecond) + fmt.Println(s) + } +} + +// Channel example +func channelExample() { + ch := make(chan int) + go func() { + ch <- 42 + }() + value := <-ch + fmt.Println(value) +} + +// Select statement +func selectExample() { + ch1 := make(chan string) + ch2 := make(chan string) + + go func() { ch1 <- "one" }() + go func() { ch2 <- "two" }() + + select { + case msg1 := <-ch1: + fmt.Println(msg1) + case msg2 := <-ch2: + fmt.Println(msg2) + } +} + +// Defer statement +func deferExample() { + defer fmt.Println("world") + fmt.Println("hello") +} + +// Panic and recover +func panicExample() { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered:", r) + } + }() + panic("a problem") +} + +// Type switch +func typeSwitch(x interface{}) { + switch x.(type) { + case int: + fmt.Println("int") + case string: + fmt.Println("string") + default: + fmt.Println("unknown") + } +} + +// Map example +func mapExample() { + m := make(map[string]int) + m["key"] = 42 + value, exists := m["key"] + fmt.Println(value, exists) +} + +// Slice example +func sliceExample() { + s := make([]int, 5) + s = append(s, 1, 2, 3) + sub := s[1:3] + fmt.Println(sub) +} + +// Array example +func arrayExample() { + var a [5]int + a[0] = 1 + fmt.Println(a) +} + +// Struct embedding +type Animal struct { + Name string +} + +type Dog struct { + Animal + Breed string +} + +// Main function +func main() { + // Basic control flow + if x := 42; x > 0 { + fmt.Println("x is positive") + } + + for i := 0; i < 5; i++ { + fmt.Println(i) + } + + // Range loop + nums := []int{1, 2, 3} + for i, num := range nums { + fmt.Println(i, num) + } + + // Switch statement + switch os := runtime.GOOS; os { + case "darwin": + fmt.Println("OS X") + case "linux": + fmt.Println("Linux") + default: + fmt.Println(os) + } + + // Go routine and wait group + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + fmt.Println("goroutine") + }() + wg.Wait() +} + +// Generic function +func Map[T, U any](slice []T, f func(T) U) []U { + result := make([]U, len(slice)) + for i, v := range slice { + result[i] = f(v) + } + return result +} + +// Generic struct +type Container[T any] struct { + Value T +} + +// Generic method +func (c *Container[T]) Set(value T) { + c.Value = value +} + +func (c *Container[T]) Get() T { + return c.Value +} + +// Generic interface +type Stringer[T any] interface { + String() string +} + +// Generic type constraint +type Number interface { + ~int | ~float64 +} + +// Generic function with type constraint +func Sum[T Number](numbers []T) T { + var sum T + for _, n := range numbers { + sum += n + } + return sum +} + +// Generic type with multiple type parameters +type Pair[T, U any] struct { + First T + Second U +} + +// Generic method with type constraint +func (p *Pair[T, U]) Swap() Pair[U, T] { + return Pair[U, T]{ + First: p.Second, + Second: p.First, + } +} + +// Person represents a person with a name and age +type Person struct { + // Name is the person's name + Name string + // Age is the person's age + Age int + // unexported field + address string +} + +// NewPerson creates a new Person instance +func NewPerson(name string, age int) *Person { + return &Person{ + Name: name, + Age: age, + address: "unknown", + } +} + +// SetAddress sets the person's address +func (p *Person) SetAddress(address string) { + p.address = address +} + +// GetAddress returns the person's address +func (p *Person) GetAddress() string { + return p.address +} + +// String implements the Stringer interface +func (p Person) String() string { + return fmt.Sprintf("%s (%d)", p.Name, p.Age) +} + +// Greeter defines an interface for objects that can greet +type Greeter interface { + // Greet returns a greeting message + Greet() string +} + +// GreeterImpl implements the Greeter interface +type GreeterImpl struct { + greeting string +} + +// Greet returns a greeting message +func (g GreeterImpl) Greet() string { + return g.greeting +} + +// UpperCase converts a string to uppercase +func UpperCase(s string) string { + return strings.ToUpper(s) +} diff --git a/src/bank.rs b/src/bank.rs index 7000caf..38b4478 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -1,7 +1,7 @@ use crate::{ Bank, BankStrategy, Error, Result, parser::{ - CppParser, FileUnit, LanguageParser, LanguageType, PythonParser, RustParser, + CppParser, FileUnit, GoParser, LanguageParser, LanguageType, PythonParser, RustParser, TypeScriptParser, formatter::Formatter, }, }; @@ -20,6 +20,7 @@ pub struct CodeBank { python_parser: PythonParser, typescript_parser: TypeScriptParser, c_parser: CppParser, + go_parser: GoParser, } impl CodeBank { @@ -29,12 +30,14 @@ impl CodeBank { let python_parser = PythonParser::try_new()?; let typescript_parser = TypeScriptParser::try_new()?; let c_parser = CppParser::try_new()?; + let go_parser = GoParser::try_new()?; Ok(Self { rust_parser, python_parser, typescript_parser, c_parser, + go_parser, }) } @@ -45,6 +48,7 @@ impl CodeBank { Some("py") => Some(LanguageType::Python), Some("ts") | Some("tsx") | Some("js") | Some("jsx") => Some(LanguageType::TypeScript), Some("c") | Some("h") | Some("cpp") | Some("hpp") => Some(LanguageType::Cpp), + Some("go") => Some(LanguageType::Go), _ => None, } } @@ -56,6 +60,7 @@ impl CodeBank { Some("py") => "python", Some("ts") | Some("tsx") | Some("js") | Some("jsx") => "typescript", Some("c") | Some("h") | Some("cpp") | Some("hpp") => "cpp", + Some("go") => "go", _ => "", } } @@ -69,6 +74,7 @@ impl CodeBank { self.typescript_parser.parse_file(file_path).map(Some) } Some(LanguageType::Cpp) => self.c_parser.parse_file(file_path).map(Some), + Some(LanguageType::Go) => self.go_parser.parse_file(file_path).map(Some), Some(LanguageType::Unknown) => Ok(None), None => Ok(None), } @@ -84,6 +90,7 @@ impl CodeBank { "package.json", "CMakeLists.txt", "Makefile", + "go.mod", ]; const MAX_DEPTH: usize = 3; @@ -265,6 +272,10 @@ mod tests { let h_path = PathBuf::from("test.h"); assert_eq!(code_bank.detect_language(&h_path), Some(LanguageType::Cpp)); + // Test Go files + let go_path = PathBuf::from("test.go"); + assert_eq!(code_bank.detect_language(&go_path), Some(LanguageType::Go)); + // Test unsupported files let unsupported_path = PathBuf::from("test.txt"); assert_eq!(code_bank.detect_language(&unsupported_path), None); @@ -290,6 +301,10 @@ mod tests { let c_path = PathBuf::from("test.c"); assert_eq!(code_bank.get_language_name(&c_path), "cpp"); + // Test Go files + let go_path = PathBuf::from("test.go"); + assert_eq!(code_bank.get_language_name(&go_path), "go"); + // Test unsupported files let unsupported_path = PathBuf::from("test.txt"); assert_eq!(code_bank.get_language_name(&unsupported_path), ""); diff --git a/src/parser/formatter/mod.rs b/src/parser/formatter/mod.rs index a8006a5..b74e4b3 100644 --- a/src/parser/formatter/mod.rs +++ b/src/parser/formatter/mod.rs @@ -445,71 +445,39 @@ impl Formatter for StructUnit { } match strategy { - BankStrategy::Default => { + BankStrategy::Default | BankStrategy::NoTests => { if let Some(source) = &self.source { output.push_str(source); } } - BankStrategy::NoTests | BankStrategy::Summary => { + BankStrategy::Summary => { // Add head (struct definition line) output.push_str(&self.head); + output.push_str(rules.function_body_start_marker); + output.push('\n'); - // Handle body/methods based on language and strategy - if *strategy == BankStrategy::Summary { - // Check if it looks like a Rust enum based on source or head - let is_rust_enum = language == LanguageType::Rust - && (self.head.contains(" enum ") - || self.source.as_ref().is_some_and(|s| s.contains(" enum "))); - - if is_rust_enum { - if let Some(source) = &self.source { - // Construct the full enum output including docs/attrs and source - let mut enum_output = String::new(); - if let Some(doc) = &self.doc { - for line in doc.lines() { - enum_output - .push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - for attr in &self.attributes { - enum_output.push_str(&format!("{}\n", attr)); - } - enum_output.push_str(source); - return Ok(enum_output); // Return the full enum source with context - } else { - // Fallback if no source: just head + ellipsis - output.push_str(rules.summary_ellipsis); - } - } else { - // Default summary behavior for non-enum structs - output.push_str(rules.summary_ellipsis); - } - } else { - // BankStrategy::NoTests - // NoTests Mode: Include methods - let body_start = if language == LanguageType::Python { - ":\n" - } else { - " {\n" - }; - let body_end = if language == LanguageType::Python { - "" - } else { - "}" - }; - output.push_str(body_start); + // Add all fields + for field in &self.fields { + output.push_str(&format!( + " {}{}\n", + field.source.as_deref().unwrap_or(""), + rules.field_sep + )); + } + output.push_str(rules.function_body_end_marker); - for method in &self.methods { - if !rules.is_test_function(&method.attributes) { - let method_formatted = method.format(strategy, language)?; - if !method_formatted.is_empty() { - output.push_str(" "); - output.push_str(&method_formatted.replace("\n", "\n ")); - output.push('\n'); - } + // Add public methods + for method in &self.methods { + if method.visibility == Visibility::Public + && !rules.is_test_function(&method.attributes) + { + let method_formatted = method.format(strategy, language)?; + if !method_formatted.is_empty() { + output.push_str(" "); + output.push_str(&method_formatted.replace("\n", "\n ")); + output.push('\n'); } } - output.push_str(body_end); } } } diff --git a/src/parser/formatter/python.rs b/src/parser/formatter/python.rs index 1648b5b..c1d419b 100644 --- a/src/parser/formatter/python.rs +++ b/src/parser/formatter/python.rs @@ -225,7 +225,7 @@ impl PythonFormatter for FileUnit { #[cfg(test)] mod tests { - use crate::*; + use crate::{parser::FieldUnit, *}; // Helper to create a test function fn create_test_function(name: &str, is_public: bool, has_test_attr: bool) -> FunctionUnit { @@ -381,17 +381,37 @@ mod tests { #[test] fn test_class_formatter_summary() { // Public class - let public_class = create_test_class("PublicClass", true); + let mut public_class = create_test_class("PublicClass", true); + + // Add a field to the class + let field = FieldUnit { + name: "field".to_string(), + doc: Some("Field documentation".to_string()), + attributes: vec![], + source: Some("field = None".to_string()), + }; + public_class.fields.push(field); + let formatted = public_class .format(&BankStrategy::Summary, LanguageType::Python) .unwrap(); - // Summary only shows head for classes - assert!(formatted.contains("class PublicClass: ...")); - assert!(!formatted.contains("publicclass_method")); // Methods shouldn't be in summary - assert!(!formatted.contains("privateclass_method")); + + assert!( + formatted.contains("class PublicClass:"), + "Should include class definition" + ); + assert!(formatted.contains("field = None"), "Should include fields"); + assert!( + formatted.contains("def publicclass_method"), + "Should include public methods" + ); + assert!( + !formatted.contains("def _publicclass_private_method"), + "Should not include private methods" + ); // Private class - let private_class = create_test_class("PrivateClass", false); + let private_class = create_test_class("_PrivateClass", false); let formatted = private_class .format(&BankStrategy::Summary, LanguageType::Python) .unwrap(); diff --git a/src/parser/formatter/rules.rs b/src/parser/formatter/rules.rs index 25721e2..63d9e0e 100644 --- a/src/parser/formatter/rules.rs +++ b/src/parser/formatter/rules.rs @@ -4,6 +4,7 @@ use crate::parser::LanguageType; #[allow(dead_code)] pub struct FormatterRules { pub summary_ellipsis: &'static str, + pub field_sep: &'static str, pub function_body_start_marker: &'static str, pub function_body_end_marker: &'static str, pub doc_marker: &'static str, @@ -13,6 +14,7 @@ pub struct FormatterRules { const RUST_RULES: FormatterRules = FormatterRules { summary_ellipsis: " { ... }", + field_sep: ",", function_body_start_marker: "{", function_body_end_marker: "}", doc_marker: "///", @@ -22,6 +24,7 @@ const RUST_RULES: FormatterRules = FormatterRules { const PYTHON_RULES: FormatterRules = FormatterRules { summary_ellipsis: ": ...", + field_sep: "", function_body_start_marker: ":", function_body_end_marker: "", doc_marker: "#", @@ -31,6 +34,7 @@ const PYTHON_RULES: FormatterRules = FormatterRules { const TS_RULES: FormatterRules = FormatterRules { summary_ellipsis: " { ... }", + field_sep: ",", function_body_start_marker: "{", function_body_end_marker: "}", doc_marker: "//", @@ -40,6 +44,7 @@ const TS_RULES: FormatterRules = FormatterRules { const C_RULES: FormatterRules = FormatterRules { summary_ellipsis: " { ... }", + field_sep: ",", function_body_start_marker: "{", function_body_end_marker: "}", doc_marker: "//", @@ -47,8 +52,19 @@ const C_RULES: FormatterRules = FormatterRules { test_module_markers: &["test_"], }; +const GO_RULES: FormatterRules = FormatterRules { + summary_ellipsis: " { ... }", + field_sep: ",", + function_body_start_marker: "{", + function_body_end_marker: "}", + doc_marker: "//", + test_markers: &["test_"], + test_module_markers: &["test_"], +}; + const UNKNOWN_RULES: FormatterRules = FormatterRules { summary_ellipsis: "...", + field_sep: "", function_body_start_marker: "", function_body_end_marker: "", doc_marker: "//", @@ -64,6 +80,7 @@ impl FormatterRules { LanguageType::Python => PYTHON_RULES, LanguageType::TypeScript => TS_RULES, LanguageType::Cpp => C_RULES, + LanguageType::Go => GO_RULES, LanguageType::Unknown => UNKNOWN_RULES, } } diff --git a/src/parser/formatter/rust.rs b/src/parser/formatter/rust.rs index c5dd215..9ab878e 100644 --- a/src/parser/formatter/rust.rs +++ b/src/parser/formatter/rust.rs @@ -227,11 +227,44 @@ mod tests { #[test] fn test_struct_formatter_summary() { // Public struct - let public_struct = create_test_struct("PublicStruct", true); + let mut public_struct = create_test_struct("PublicStruct", true); + + // Add a field to the struct + let field = FieldUnit { + name: "field".to_string(), + doc: Some("Field documentation".to_string()), + attributes: vec![], + source: Some("pub field: i32".to_string()), + }; + public_struct.fields.push(field); + let formatted = public_struct .format(&BankStrategy::Summary, LanguageType::Rust) .unwrap(); + assert!(formatted.contains("struct PublicStruct")); + assert!( + formatted.contains("pub field: i32"), + "Summary should include fields" + ); + assert!( + formatted.contains("fn publicstruct_method"), + "Summary should include public methods" + ); + assert!( + !formatted.contains("fn publicstruct_private_method"), + "Summary should not include private methods" + ); + + // Private struct should be skipped + let private_struct = create_test_struct("PrivateStruct", false); + let formatted = private_struct + .format(&BankStrategy::Summary, LanguageType::Rust) + .unwrap(); + assert!( + formatted.is_empty(), + "Private structs should be skipped in summary mode" + ); } #[test] @@ -294,9 +327,11 @@ mod tests { .format(&BankStrategy::NoTests, LanguageType::Rust) .unwrap(); - // Should include both public and private methods - assert!(formatted.contains("fn teststruct_method()")); // public method - assert!(formatted.contains("fn teststruct_private_method()")); // private method + // Should now just return the source for NoTests mode + assert!(formatted.contains("struct TestStruct { field: i32 }")); + // Should not contain methods as we're just using the source + assert!(!formatted.contains("fn teststruct_method()")); + assert!(!formatted.contains("fn teststruct_private_method()")); } #[test] @@ -500,7 +535,9 @@ mod tests { assert!(formatted.contains("struct PublicStruct")); assert!(formatted.contains("struct PrivateStruct")); assert!(formatted.contains("use std::collections::HashMap;")); - assert!(formatted.contains("fn publicstruct_private_method()")); + + // We now just display struct source in NoTests, not individual methods anymore + assert!(!formatted.contains("fn publicstruct_private_method()")); } #[test] @@ -510,11 +547,12 @@ mod tests { .format(&BankStrategy::Summary, LanguageType::Rust) .unwrap(); - // Summary for enums should show the full source (including variants) + // Summary for enums now follows the same pattern as structs assert!(formatted.contains("/// Docs for PublicEnum")); assert!(formatted.contains("pub enum PublicEnum")); - assert!(formatted.contains("VariantA,")); - assert!(formatted.contains("VariantB(String),")); + // No fields/variants in the enum + assert!(!formatted.contains("VariantA,")); + assert!(!formatted.contains("VariantB(String),")); let private_enum = create_test_enum("PrivateEnum", false); let formatted = private_enum diff --git a/src/parser/lang/go.rs b/src/parser/lang/go.rs new file mode 100644 index 0000000..178d87c --- /dev/null +++ b/src/parser/lang/go.rs @@ -0,0 +1,884 @@ +use super::GoParser; +use crate::{ + DeclareKind, DeclareStatements, Error, FieldUnit, FileUnit, FunctionUnit, ImplUnit, + LanguageParser, ModuleUnit, Result, StructUnit, TraitUnit, Visibility, +}; +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::path::Path; +use tree_sitter::{Node, Parser}; + +impl LanguageParser for GoParser { + fn parse_file(&mut self, file_path: &Path) -> Result { + // Read the file + let source_code = fs::read_to_string(file_path).map_err(Error::Io)?; + + // Parse the file + let tree = self + .parse(source_code.as_bytes(), None) + .ok_or_else(|| Error::TreeSitter("Failed to parse source code".to_string()))?; + let root_node = tree.root_node(); + + // Create a new file unit + let mut file_unit = FileUnit::new(file_path.to_path_buf()); + file_unit.source = Some(source_code.clone()); + + // Maps to collect methods by receiver type + let mut methods_by_type: std::collections::HashMap> = + std::collections::HashMap::new(); + + // Process top-level declarations + let mut cursor = root_node.walk(); + for child in root_node.children(&mut cursor) { + match child.kind() { + "package_clause" => { + let package_doc = extract_documentation(child, &source_code); + if let Some(package_name) = + get_child_node_text(child, "package_identifier", &source_code) + { + let module = ModuleUnit { + name: package_name, + visibility: Visibility::Public, // Packages are public + doc: package_doc, + source: get_node_text(child, &source_code), + attributes: Vec::new(), + ..Default::default() + }; + file_unit.modules.push(module); + } + } + "import_declaration" => { + // Handle single and block imports + let mut import_cursor = child.walk(); + for import_spec in child.children(&mut import_cursor) { + if import_spec.kind() == "import_spec" + || import_spec.kind() == "interpreted_string_literal" + || import_spec.kind() == "raw_string_literal" + { + if let Some(import_text) = get_node_text(import_spec, &source_code) { + file_unit.declares.push(DeclareStatements { + source: import_text, + kind: DeclareKind::Use, + }); + } + } else if import_spec.kind() == "import_spec_list" { + let mut list_cursor = import_spec.walk(); + for inner_spec in import_spec.children(&mut list_cursor) { + if inner_spec.kind() == "import_spec" { + if let Some(import_text) = + get_node_text(inner_spec, &source_code) + { + file_unit.declares.push(DeclareStatements { + source: import_text, + kind: DeclareKind::Use, + }); + } + } + } + } + } + } + "function_declaration" => { + if let Ok(func) = self.parse_function(child, &source_code) { + file_unit.functions.push(func); + } + } + "method_declaration" => { + if let Ok((receiver_type, method)) = self.parse_method(child, &source_code) { + methods_by_type + .entry(receiver_type) + .or_default() + .push(method); + } + } + "type_declaration" => { + let mut type_decl_cursor = child.walk(); + for type_spec_node in child.children(&mut type_decl_cursor) { + if type_spec_node.kind() == "type_spec" { + let mut type_spec_cursor = type_spec_node.walk(); + if let Some(type_def_node) = type_spec_node + .children(&mut type_spec_cursor) + .find(|n| n.kind() == "struct_type" || n.kind() == "interface_type") + { + if type_def_node.kind() == "struct_type" { + if let Ok(struct_item) = + self.parse_struct(type_spec_node, &source_code) + { + file_unit.structs.push(struct_item); + } + } else if type_def_node.kind() == "interface_type" { + if let Ok(interface_item) = + self.parse_interface(type_spec_node, &source_code) + { + file_unit.traits.push(interface_item); + } + } + } + } + } + } + "const_declaration" | "var_declaration" => { + let mut decl_cursor = child.walk(); + for spec_node in child.children(&mut decl_cursor) { + if spec_node.kind() == "const_spec" || spec_node.kind() == "var_spec" { + if let Some(declare_text) = get_node_text(spec_node, &source_code) { + let kind_str = if child.kind() == "const_declaration" { + "const" + } else { + "var" + }; + file_unit.declares.push(DeclareStatements { + source: declare_text, + kind: DeclareKind::Other(kind_str.to_string()), + }); + } + } else if spec_node.kind() == "var_spec_list" + || spec_node.kind() == "const_spec_list" + { + let mut list_cursor = spec_node.walk(); + for inner_spec_node in spec_node.children(&mut list_cursor) { + if inner_spec_node.kind() == "const_spec" + || inner_spec_node.kind() == "var_spec" + { + if let Some(declare_text) = + get_node_text(inner_spec_node, &source_code) + { + let kind_str = if child.kind() == "const_declaration" { + "const" + } else { + "var" + }; + file_unit.declares.push(DeclareStatements { + source: declare_text, + kind: DeclareKind::Other(kind_str.to_string()), + }); + } + } + } + } + } + } + "comment" => { + // Ignore comments at top level for now + } + _ => { + // Ignore other top-level node: {} + } + } + } + + // Add methods to their respective structs + for struct_item in &mut file_unit.structs { + if let Some(methods) = methods_by_type.remove(&struct_item.name) { + struct_item.methods.extend(methods.clone()); // Add methods to struct + + // Also create an ImplUnit for each struct with methods + let impl_unit = ImplUnit { + doc: None, // Could try to find doc for the impl block if needed + head: format!("methods for {}", struct_item.name), + source: None, // Source for the whole impl block is tricky + attributes: Vec::new(), + methods, // Moves methods into the impl unit + }; + file_unit.impls.push(impl_unit); + } + } + + // For any methods whose receiver types weren't found as structs, + // still create impl units (e.g., methods on built-in types or type aliases) + for (receiver_type, methods) in methods_by_type { + let impl_unit = ImplUnit { + doc: None, + head: format!("methods for {}", receiver_type), + source: None, + attributes: Vec::new(), + methods, + }; + file_unit.impls.push(impl_unit); + } + + Ok(file_unit) + } +} + +impl GoParser { + pub fn try_new() -> Result { + let mut parser = Parser::new(); + let language = tree_sitter_go::LANGUAGE; + parser + .set_language(&language.into()) + .map_err(|e| Error::TreeSitter(e.to_string()))?; + Ok(Self { parser }) + } + + // Helper function to determine visibility (in Go, uppercase first letter means exported/public) + fn determine_visibility(&self, name: &str) -> Visibility { + if !name.is_empty() && name.chars().next().unwrap().is_uppercase() { + Visibility::Public + } else { + Visibility::Private + } + } + + // Parse function and extract its details + fn parse_function(&self, node: Node, source_code: &str) -> Result { + let documentation = extract_documentation(node, source_code); + let name = get_child_node_text(node, "identifier", source_code) + .unwrap_or_else(|| "unknown".to_string()); + + let visibility = self.determine_visibility(&name); + let source = get_node_text(node, source_code); + let mut signature = None; + let mut body = None; + + // Extract signature (everything before the body block) + if let Some(body_node) = node.child_by_field_name("body") { + let sig_end = body_node.start_byte(); + let sig_start = node.start_byte(); + if sig_end > sig_start { + signature = Some(source_code[sig_start..sig_end].trim().to_string()); + } + body = get_node_text(body_node, source_code); + } else { + // Fallback for function declarations without body (e.g. in interfaces - though handled separately) + signature = source.clone(); + } + + Ok(FunctionUnit { + name, + visibility, + doc: documentation, + source, + signature, + body, + attributes: Vec::new(), // Go doesn't have attributes like Rust + }) + } + + // Parse struct and extract its details + // Node passed here should be the `type_spec` node + fn parse_struct(&self, type_spec_node: Node, source_code: &str) -> Result { + // Documentation should be associated with the type_spec node or its parent type_declaration + let documentation = + extract_documentation(type_spec_node, source_code).or_else(|| -> Option { + type_spec_node + .parent() + .and_then(|p| extract_documentation(p, source_code)) + }); + let name = get_child_node_text(type_spec_node, "type_identifier", source_code) + .unwrap_or_else(|| "unknown".to_string()); + let visibility = self.determine_visibility(&name); + let source = get_node_text( + type_spec_node.parent().unwrap_or(type_spec_node), + source_code, + ); + let head = format!("type {} struct", name); + + let mut fields = Vec::new(); + + if let Some(struct_type) = type_spec_node + .children(&mut type_spec_node.walk()) + .find(|child| child.kind() == "struct_type") + { + if let Some(field_list) = struct_type + .children(&mut struct_type.walk()) + .find(|child| child.kind() == "field_declaration_list") + { + let mut list_cursor = field_list.walk(); + for field_decl in field_list.children(&mut list_cursor) { + if field_decl.kind() == "field_declaration" { + let field_documentation = extract_documentation(field_decl, source_code); + let field_source = get_node_text(field_decl, source_code); + let mut field_names = Vec::new(); + let mut decl_cursor = field_decl.walk(); + for child in field_decl.children(&mut decl_cursor) { + if child.kind() == "identifier" || child.kind() == "field_identifier" { + if let Some(field_name) = get_node_text(child, source_code) { + field_names.push(field_name); + } + } else if child.kind().ends_with("_type") + || child.kind() == "qualified_type" + { + // Stop collecting names when type is reached + break; + } + } + for field_name in field_names { + fields.push(FieldUnit { + name: field_name, + doc: field_documentation.clone(), + attributes: Vec::new(), + source: field_source.clone(), + }); + } + } + } + } + } + + Ok(StructUnit { + name, + head, + visibility, + doc: documentation, + source, + attributes: Vec::new(), + fields, + methods: Vec::new(), + }) + } + + // Parse interface (similar to trait in Rust) + // Node passed here should be the `type_spec` node + fn parse_interface(&self, type_spec_node: Node, source_code: &str) -> Result { + // Documentation should be associated with the type_spec node or its parent type_declaration + let documentation = + extract_documentation(type_spec_node, source_code).or_else(|| -> Option { + type_spec_node + .parent() + .and_then(|p| extract_documentation(p, source_code)) + }); + let name = get_child_node_text(type_spec_node, "type_identifier", source_code) + .unwrap_or_else(|| "unknown".to_string()); + let visibility = self.determine_visibility(&name); + let source = get_node_text( + type_spec_node.parent().unwrap_or(type_spec_node), + source_code, + ); + + let mut methods = Vec::new(); + + if let Some(interface_type) = type_spec_node + .children(&mut type_spec_node.walk()) + .find(|child| child.kind() == "interface_type") + { + let mut interface_cursor = interface_type.walk(); + for child in interface_type.children(&mut interface_cursor) { + if child.kind() == "method_elem" { + let method_spec = child; // Keep variable name for consistency + let method_doc = extract_documentation(method_spec, source_code); + let method_source = get_node_text(method_spec, source_code); + // Method name is typically the first identifier within method_spec + let method_name = get_child_node_text(method_spec, "identifier", source_code) + .or_else(|| { + get_child_node_text(method_spec, "field_identifier", source_code) + }) + .unwrap_or_else(|| "unknown_interface_method".to_string()); + let visibility = self.determine_visibility(&method_name); // Interface methods are implicitly public + // Interface methods only have signatures, no bodies + let signature = method_source.clone(); + + methods.push(FunctionUnit { + name: method_name, + visibility, // Could force Public, but determine_visibility works + doc: method_doc, + source: method_source, + signature, + body: None, // Interface methods don't have bodies + attributes: Vec::new(), + }); + } + } + } + + Ok(TraitUnit { + name, + visibility, + doc: documentation, + source, + attributes: Vec::new(), + methods, + }) + } + + // Parse method (like impl in Rust) + // Node is `method_declaration` + fn parse_method(&self, node: Node, source_code: &str) -> Result<(String, FunctionUnit)> { + let documentation = extract_documentation(node, source_code); + let source = get_node_text(node, source_code); + + // Get method name (field identifier) + let method_name = get_child_node_text(node, "field_identifier", source_code) + .unwrap_or_else(|| "unknown".to_string()); + + // Get receiver type (struct type) + let receiver_type = if let Some(parameter_list) = node.child_by_field_name("receiver") { + // The receiver is a parameter_list containing one parameter_declaration + if let Some(parameter) = parameter_list + .children(&mut parameter_list.walk()) + .find(|child| child.kind() == "parameter_declaration") + { + // Extract type from parameter declaration + if let Some(type_node) = parameter.child_by_field_name("type") { + get_node_text(type_node, source_code) + .map(|s| s.trim_start_matches('*').to_string()) // Remove leading * for pointer receivers + .unwrap_or_else(|| "unknown".to_string()) + } else { + "unknown".to_string() + } + } else { + "unknown".to_string() + } + } else { + "unknown".to_string() + }; + + let visibility = self.determine_visibility(&method_name); + let mut signature = None; + let mut body = None; + + // Extract signature (everything before the body block) + if let Some(body_node) = node.child_by_field_name("body") { + let sig_end = body_node.start_byte(); + let sig_start = node.start_byte(); + if sig_end > sig_start { + signature = Some(source_code[sig_start..sig_end].trim().to_string()); + } + body = get_node_text(body_node, source_code); + } else { + signature = source.clone(); + } + + let function = FunctionUnit { + name: method_name, + visibility, + doc: documentation, + source, + signature, + body, + attributes: Vec::new(), + }; + + Ok((receiver_type, function)) + } +} + +// Helper function to get the text of a node +fn get_node_text(node: Node, source_code: &str) -> Option { + node.utf8_text(source_code.as_bytes()) + .ok() + .map(String::from) +} + +// Helper function to get the text of the first child node of a specific kind +fn get_child_node_text<'a>(node: Node<'a>, kind: &str, source_code: &'a str) -> Option { + // First try to find it directly as a child using field name if common (e.g., 'name') + if kind == "identifier" || kind == "package_identifier" || kind == "field_identifier" { + if let Some(name_node) = node.child_by_field_name("name") { + // Check if the node kind matches the expected identifier type + if name_node.kind() == kind { + return name_node + .utf8_text(source_code.as_bytes()) + .ok() + .map(String::from); + } + } + } + + // Then try finding by specific node kind + if let Some(child) = node + .children(&mut node.walk()) + .find(|child| child.kind() == kind) + { + return child + .utf8_text(source_code.as_bytes()) + .ok() + .map(String::from); + } + + // Fallback: Look for any specific identifier kind child if specific kind not found + if kind == "identifier" || kind == "package_identifier" || kind == "field_identifier" { + if let Some(ident_child) = node + .children(&mut node.walk()) + .find(|child| child.kind() == kind) + { + return ident_child + .utf8_text(source_code.as_bytes()) + .ok() + .map(String::from); + } + } + // Generic identifier fallback + if let Some(ident_child) = node + .children(&mut node.walk()) + .find(|child| child.kind() == "identifier") + { + return ident_child + .utf8_text(source_code.as_bytes()) + .ok() + .map(String::from); + } + + None +} + +// Extract documentation from comments preceding a node +fn extract_documentation(node: Node, source_code: &str) -> Option { + // Attempt to find a preceding comment block associated with the node. + // Go documentation comments are typically immediately before the declaration. + let mut prev_sibling = node.prev_sibling(); + while let Some(sibling) = prev_sibling { + if sibling.kind() == "comment" { + // Check if the comment is "close" enough (on the preceding line(s)) + if node.start_position().row == sibling.end_position().row + 1 + || node.start_position().row == sibling.start_position().row + 1 + { + // Found a relevant comment block + let doc_text = get_node_text(sibling, source_code)?; // Use ? to propagate None + // Basic cleaning: remove comment markers and trim whitespace + let cleaned_doc = doc_text + .trim_start_matches("//") + .trim_start_matches("/*") + .trim_end_matches("*/") + .trim() + .to_string(); + // If multiple comment lines form a block, they should be concatenated. + // Tree-sitter often gives the whole block as one node. + // If not, more complex logic might be needed to combine multi-line comments. + return Some(cleaned_doc); + } else { + // Comment is not immediately preceding, stop searching backwards + break; + } + } else if !sibling.is_extra() { + // Reached a non-comment, non-whitespace node, stop searching + break; + } + prev_sibling = sibling.prev_sibling(); + } + + None // No documentation comment found immediately preceding the node +} + +impl Deref for GoParser { + type Target = Parser; + + fn deref(&self) -> &Self::Target { + &self.parser + } +} + +impl DerefMut for GoParser { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.parser + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn parse_fixture(file_name: &str) -> Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR should be set during tests"); + let path = PathBuf::from(manifest_dir).join("fixtures").join(file_name); + let mut parser = GoParser::try_new()?; + parser.parse_file(&path) + } + + #[test] + fn test_parse_go_package() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + assert_eq!( + file_unit.modules.len(), + 1, + "Should parse one package module" + ); + assert_eq!(file_unit.modules[0].name, "example"); + assert!( + file_unit.modules[0].doc.is_some(), + "Package doc comment missing" + ); + assert!( + file_unit.modules[0] + .doc + .as_ref() + .unwrap() + .contains("sample Go file") + ); + } + + #[test] + fn test_parse_go_imports() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + // Count only imports + let import_count = file_unit + .declares + .iter() + .filter(|d| d.kind == DeclareKind::Use) + .count(); + assert_eq!( + import_count, 7, + "Expected exactly 7 imports, found {}", + import_count + ); // Check exact count + // Check specific imports + assert!( + file_unit + .declares + .iter() + .any(|d| d.kind == DeclareKind::Use && d.source.contains("\"fmt\"")) + ); + assert!( + file_unit + .declares + .iter() + .any(|d| d.kind == DeclareKind::Use && d.source.contains("\"strings\"")) + ); + assert!( + file_unit + .declares + .iter() + .any(|d| d.kind == DeclareKind::Use && d.source.contains("\"os\"")) + ); + // Check const/var declarations + let const_count = file_unit + .declares + .iter() + .filter(|d| matches!(&d.kind, DeclareKind::Other(s) if s == "const")) + .count(); + assert!( + const_count >= 3, + "Expected at least 3 const declarations, found {}", + const_count + ); + let var_count = file_unit + .declares + .iter() + .filter(|d| matches!(&d.kind, DeclareKind::Other(s) if s == "var")) + .count(); + assert!( + var_count >= 1, + "Expected at least 1 var declaration, found {}", + var_count + ); + } + + #[test] + fn test_parse_go_functions() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + // Check top-level functions + let new_person_func = file_unit.functions.iter().find(|f| f.name == "NewPerson"); + assert!(new_person_func.is_some(), "NewPerson function not found"); + let new_person_func = new_person_func.unwrap(); + assert_eq!(new_person_func.visibility, Visibility::Public); + assert!(new_person_func.doc.is_some(), "NewPerson doc missing"); + assert!( + new_person_func + .doc + .as_ref() + .unwrap() + .contains("creates a new Person instance") + ); + assert!(new_person_func.signature.is_some()); + assert!(new_person_func.body.is_some()); + + let upper_case_func = file_unit.functions.iter().find(|f| f.name == "UpperCase"); + assert!(upper_case_func.is_some(), "UpperCase function not found"); + let upper_case_func = upper_case_func.unwrap(); + assert_eq!(upper_case_func.visibility, Visibility::Public); + assert!(upper_case_func.doc.is_some(), "UpperCase doc missing"); + assert!( + upper_case_func + .doc + .as_ref() + .unwrap() + .contains("converts a string to uppercase") + ); + assert!(upper_case_func.signature.is_some()); + assert!(upper_case_func.body.is_some()); + } + + #[test] + fn test_parse_go_structs() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + + let person_struct = file_unit.structs.iter().find(|s| s.name == "Person"); + assert!(person_struct.is_some(), "Person struct not found"); + let person_struct = person_struct.unwrap(); + assert_eq!(person_struct.visibility, Visibility::Public); + assert!(person_struct.doc.is_some(), "Person doc missing"); + assert!( + person_struct + .doc + .as_ref() + .unwrap() + .contains("represents a person") + ); + assert_eq!(person_struct.fields.len(), 3, "Person should have 3 fields"); + // Check field names + assert!(person_struct.fields.iter().any(|f| f.name == "Name")); + assert!(person_struct.fields.iter().any(|f| f.name == "Age")); + assert!(person_struct.fields.iter().any(|f| f.name == "address")); + // Check field documentation + let name_field = person_struct + .fields + .iter() + .find(|f| f.name == "Name") + .unwrap(); + assert!(name_field.doc.is_some(), "Name field doc missing"); + assert!(name_field.doc.as_ref().unwrap().contains("person's name")); + + let age_field = person_struct + .fields + .iter() + .find(|f| f.name == "Age") + .unwrap(); + assert!(age_field.doc.is_some(), "Age field doc missing"); + assert!(age_field.doc.as_ref().unwrap().contains("person's age")); + + let address_field = person_struct + .fields + .iter() + .find(|f| f.name == "address") + .unwrap(); + assert!(address_field.doc.is_some(), "address field doc missing"); + assert!( + address_field + .doc + .as_ref() + .unwrap() + .contains("unexported field") + ); + + let greeter_impl_struct = file_unit.structs.iter().find(|s| s.name == "GreeterImpl"); + assert!( + greeter_impl_struct.is_some(), + "GreeterImpl struct not found" + ); + let greeter_impl_struct = greeter_impl_struct.unwrap(); + assert_eq!(greeter_impl_struct.visibility, Visibility::Public); + assert!(greeter_impl_struct.doc.is_some(), "GreeterImpl doc missing"); + assert!( + greeter_impl_struct + .doc + .as_ref() + .unwrap() + .contains("implements the Greeter interface") + ); + assert_eq!( + greeter_impl_struct.fields.len(), + 1, + "GreeterImpl should have 1 field" + ); + assert_eq!(greeter_impl_struct.fields[0].name, "greeting"); + + // Check associated methods (parsed into impls) + let greeter_impl_methods = file_unit + .impls + .iter() + .find(|imp| imp.head == "methods for GreeterImpl"); + assert!( + greeter_impl_methods.is_some(), + "Impl block for GreeterImpl not found" + ); + assert_eq!( + greeter_impl_methods.unwrap().methods.len(), + 1, + "GreeterImpl should have 1 method" + ); + assert_eq!(greeter_impl_methods.unwrap().methods[0].name, "Greet"); + } + + #[test] + fn test_parse_go_interfaces() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + + let greeter_interface = file_unit.traits.iter().find(|t| t.name == "Greeter"); + assert!(greeter_interface.is_some(), "Greeter interface not found"); + let greeter_interface = greeter_interface.unwrap(); + assert_eq!(greeter_interface.visibility, Visibility::Public); + assert!(greeter_interface.doc.is_some(), "Greeter doc missing"); + assert!( + greeter_interface + .doc + .as_ref() + .unwrap() + .contains("defines an interface") + ); + assert_eq!( + greeter_interface.methods.len(), + 1, + "Greeter interface should have 1 method" + ); + assert_eq!(greeter_interface.methods[0].name, "Greet"); + assert!( + greeter_interface.methods[0].doc.is_some(), + "Greet method doc missing" + ); + assert!( + greeter_interface.methods[0] + .doc + .as_ref() + .unwrap() + .contains("returns a greeting message") + ); + assert!(greeter_interface.methods[0].signature.is_some()); + assert!(greeter_interface.methods[0].body.is_none()); + } + + #[test] + fn test_parse_go_methods() { + let file_unit = parse_fixture("sample.go").expect("Failed to parse Go file"); + + // Find the ImplUnit for Person methods + let person_impl = file_unit + .impls + .iter() + .find(|imp| imp.head == "methods for Person"); + assert!(person_impl.is_some(), "Impl block for Person not found"); + let person_impl = person_impl.unwrap(); + + // Check method count + assert_eq!(person_impl.methods.len(), 3, "Person should have 3 methods"); + + // Check SetAddress method + let set_address = person_impl.methods.iter().find(|m| m.name == "SetAddress"); + assert!(set_address.is_some(), "SetAddress method not found"); + let set_address = set_address.unwrap(); + assert_eq!(set_address.visibility, Visibility::Public); + assert!(set_address.doc.is_some(), "SetAddress doc missing"); + assert!( + set_address + .doc + .as_ref() + .unwrap() + .contains("sets the person's address") + ); + assert!(set_address.signature.is_some()); + assert!(set_address.body.is_some()); + + // Check GetAddress method + let get_address = person_impl.methods.iter().find(|m| m.name == "GetAddress"); + assert!(get_address.is_some(), "GetAddress method not found"); + let get_address = get_address.unwrap(); + assert_eq!(get_address.visibility, Visibility::Public); + assert!(get_address.doc.is_some(), "GetAddress doc missing"); + assert!( + get_address + .doc + .as_ref() + .unwrap() + .contains("returns the person's address") + ); + assert!(get_address.signature.is_some()); + assert!(get_address.body.is_some()); + + // Check String method + let string_method = person_impl.methods.iter().find(|m| m.name == "String"); + assert!(string_method.is_some(), "String method not found"); + let string_method = string_method.unwrap(); + assert_eq!(string_method.visibility, Visibility::Public); + assert!(string_method.doc.is_some(), "String method doc missing"); + assert!( + string_method + .doc + .as_ref() + .unwrap() + .contains("implements the Stringer interface") + ); + assert!(string_method.signature.is_some()); + assert!(string_method.body.is_some()); + } +} diff --git a/src/parser/lang/mod.rs b/src/parser/lang/mod.rs index 6b44680..c13c65a 100644 --- a/src/parser/lang/mod.rs +++ b/src/parser/lang/mod.rs @@ -1,6 +1,7 @@ use tree_sitter::Parser; mod cpp; +mod go; mod python; mod rust; mod ts; @@ -20,3 +21,7 @@ pub struct CppParser { pub struct TypeScriptParser { parser: Parser, } + +pub struct GoParser { + parser: Parser, +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5adc7fa..6ff3dbd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6,7 +6,7 @@ use crate::Result; use std::path::{Path, PathBuf}; pub use formatter::Formatter; -pub use lang::{CppParser, PythonParser, RustParser, TypeScriptParser}; +pub use lang::{CppParser, GoParser, PythonParser, RustParser, TypeScriptParser}; /// Represents visibility levels for code elements. /// @@ -72,6 +72,9 @@ pub enum Visibility { /// // Check C files /// assert!(matches!(LanguageType::Cpp, LanguageType::Cpp)); /// +/// // Check Go files +/// assert!(matches!(LanguageType::Go, LanguageType::Go)); +/// /// // Handle unknown types /// assert!(matches!(LanguageType::Unknown, LanguageType::Unknown)); /// ``` @@ -85,6 +88,8 @@ pub enum LanguageType { TypeScript, /// C/C++ language Cpp, + /// Go language + Go, /// Unknown language (used for unsupported extensions) Unknown, } @@ -401,6 +406,7 @@ impl Visibility { (_, LanguageType::Python) => "", (_, LanguageType::TypeScript) => "", (_, LanguageType::Cpp) => "", + (_, LanguageType::Go) => "", (_, LanguageType::Unknown) => "", } }