use tree_sitter::Node;
use crate::core::{Kind, Symbol};
use crate::lang::{Ctx, LanguagePlugin, extract_with};
const LANGUAGE: &str = "go";
pub(crate) struct Go;
impl LanguagePlugin for Go {
fn language(&self) -> &'static str {
LANGUAGE
}
fn extensions(&self) -> &[&str] {
&["go"]
}
fn extract(&self, file: &str, source: &str) -> Vec<Symbol> {
extract_with(
LANGUAGE,
tree_sitter_go::LANGUAGE.into(),
file,
source,
|ctx, root, out| walk(ctx, root, None, out),
)
}
}
fn walk(ctx: &Ctx, node: Node, parent: Option<&str>, out: &mut Vec<Symbol>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_declaration" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Function, child, parent);
}
}
"method_declaration" => {
if let Some(name) = ctx.field_text(child, "name") {
let recv = child
.child_by_field_name("receiver")
.and_then(|r| type_identifier(ctx, r));
push(ctx, out, &name, Kind::Method, child, recv.as_deref());
}
}
"type_spec" => {
if let Some(name) = ctx.field_text(child, "name") {
match child.child_by_field_name("type").map(|t| t.kind()) {
Some("struct_type") => {
push(ctx, out, &name, Kind::Struct, child, parent);
}
Some("interface_type") => {
push(ctx, out, &name, Kind::Trait, child, parent);
walk(ctx, child, Some(&name), out);
}
_ => {}
}
}
}
"const_declaration" if node.kind() == "source_file" => {
constants(ctx, child, out);
}
"method_spec" | "method_elem" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Method, child, parent);
}
}
_ => walk(ctx, child, parent, out),
}
}
}
fn constants(ctx: &Ctx, decl: Node, out: &mut Vec<Symbol>) {
let mut specs = decl.walk();
for spec in decl.named_children(&mut specs) {
if spec.kind() != "const_spec" {
continue;
}
let mut names = spec.walk();
for ident in spec.children_by_field_name("name", &mut names) {
if ident.kind() == "identifier"
&& let Some(name) = ctx.node_text(ident)
&& name != "_"
{
push(ctx, out, &name, Kind::Constant, spec, None);
}
}
}
}
fn push(ctx: &Ctx, out: &mut Vec<Symbol>, name: &str, kind: Kind, node: Node, p: Option<&str>) {
let mut s = ctx.symbol(name, kind, node, p);
s.visibility = Some(if name.chars().next().is_some_and(char::is_uppercase) {
"public"
} else {
"private"
});
out.push(s);
}
fn type_identifier(ctx: &Ctx, node: Node) -> Option<String> {
if node.kind() == "type_identifier" {
return ctx.node_text(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(name) = type_identifier(ctx, child) {
return Some(name);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::testing::find;
fn extract(source: &str) -> Vec<Symbol> {
Go.extract("test.go", source)
}
#[test]
fn extracts_funcs_types_and_methods() {
let src = r#"
package widget
type Widget struct {
Size int
}
type Renderer interface {
Render() string
}
func (w *Widget) Resize(n int) {
w.Size = n
}
func Build() *Widget {
return &Widget{}
}
"#;
let syms = extract(src);
assert_eq!(find(&syms, "Widget").kind, Kind::Struct);
assert_eq!(find(&syms, "Renderer").kind, Kind::Trait);
let build = find(&syms, "Build");
assert_eq!(build.kind, Kind::Function);
assert_eq!(build.parent, None);
let resize = find(&syms, "Resize");
assert_eq!(resize.kind, Kind::Method);
assert_eq!(resize.parent.as_deref(), Some("Widget"));
let render = find(&syms, "Render");
assert_eq!(render.kind, Kind::Method);
assert_eq!(render.parent.as_deref(), Some("Renderer"));
assert_eq!(build.language, "go");
}
#[test]
fn package_level_consts_are_constants() {
let src = r#"
package widget
const MaxRetries = 3
const (
StateIdle State = iota
stateBusy
_
)
const Low, High = 1, 9
var DefaultWidget = Widget{}
func Build() {
const localLimit = 5
}
"#;
let syms = extract(src);
let max = find(&syms, "MaxRetries");
assert_eq!(max.kind, Kind::Constant);
assert_eq!(max.parent, None);
assert_eq!(max.visibility, Some("public"));
let idle = find(&syms, "StateIdle");
assert_eq!(idle.kind, Kind::Constant);
let busy = find(&syms, "stateBusy");
assert_eq!(busy.line, idle.line + 1);
assert_eq!(busy.visibility, Some("private"));
assert_eq!(find(&syms, "Low").kind, Kind::Constant);
assert_eq!(find(&syms, "High").kind, Kind::Constant);
for absent in ["_", "DefaultWidget", "localLimit"] {
assert!(!syms.iter().any(|s| s.name == absent), "{absent}: {syms:?}");
}
}
#[test]
fn empty_and_unparseable_yield_no_symbols() {
assert!(extract("").is_empty());
assert!(extract("package x\n").is_empty());
}
#[test]
fn capitalization_is_visibility() {
let src = "package x\n\nfunc Exported() {}\nfunc internal() {}\n";
let syms = extract(src);
assert_eq!(find(&syms, "Exported").visibility, Some("public"));
assert_eq!(find(&syms, "internal").visibility, Some("private"));
}
}