use anyhow::Result;
use serde::Serialize;
use std::collections::HashMap;
use tree_sitter::Language;
use crate::core::graph::Edge;
use crate::core::parser::SupportedLanguage;
use crate::languages::{make_edge, resolve_scope_id, LanguagePlugin};
pub struct GoPlugin;
impl LanguagePlugin for GoPlugin {
fn language(&self) -> SupportedLanguage {
SupportedLanguage::Go
}
fn extensions(&self) -> &[&str] {
&["go"]
}
fn ts_language(&self) -> Language {
tree_sitter_go::language()
}
fn symbol_query_source(&self) -> &str {
include_str!("../queries/go/symbols.scm")
}
fn edge_query_source(&self) -> &str {
include_str!("../queries/go/edges.scm")
}
fn infer_symbol_kind(&self, node_kind: &str) -> &str {
match node_kind {
"function_declaration" => "function",
"method_declaration" => "method",
"type_spec" => {
"struct"
}
"const_spec" => "const",
_ => "function",
}
}
fn scope_node_types(&self) -> &[&str] {
&["function_declaration", "method_declaration", "func_literal"]
}
fn class_body_node_types(&self) -> &[&str] {
&["field_declaration_list"]
}
fn class_decl_node_types(&self) -> &[&str] {
&["type_declaration"]
}
fn extract_metadata(
&self,
node: &tree_sitter::Node,
source: &str,
kind: &str,
) -> Result<String> {
extract_metadata(node, source, kind)
}
fn extract_edge(
&self,
pattern_index: usize,
captures: &HashMap<String, (String, u32)>,
file_path: &str,
enclosing_scope_id: Option<&str>,
) -> Vec<Edge> {
extract_go_edge(pattern_index, captures, file_path, enclosing_scope_id)
}
fn extract_docstring(&self, node: &tree_sitter::Node, source: &str) -> Option<String> {
let mut lines = Vec::new();
let mut current = node.prev_sibling();
while let Some(prev) = current {
if prev.kind() == "comment" {
if let Ok(text) = prev.utf8_text(source.as_bytes()) {
let cleaned = text.trim().trim_start_matches("//").trim();
lines.push(cleaned.to_string());
}
current = prev.prev_sibling();
} else {
break;
}
}
if lines.is_empty() {
return None;
}
lines.reverse();
Some(lines.join("\n"))
}
fn generic_name_stopwords(&self) -> &[&str] {
&[
"String", "Error", "Close", "Read", "Write", "New", "Init", "Run",
]
}
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct GoMetadata {
pub exported: bool,
pub receiver: Option<String>,
pub is_pointer_receiver: bool,
pub return_types: Vec<String>,
pub parameters: Vec<GoParameterInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_kind: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct GoParameterInfo {
pub name: String,
#[serde(rename = "type")]
pub type_annotation: Option<String>,
}
pub fn extract_metadata(node: &tree_sitter::Node, source: &str, kind: &str) -> Result<String> {
let mut meta = GoMetadata::default();
let name = extract_name_text(node, source);
if let Some(ref n) = name {
meta.exported = n.starts_with(|c: char| c.is_uppercase());
}
match kind {
"method" => {
extract_receiver_info(node, source, &mut meta);
extract_parameters_from_func(node, source, &mut meta);
extract_return_types(node, source, &mut meta);
}
"function" => {
extract_parameters_from_func(node, source, &mut meta);
extract_return_types(node, source, &mut meta);
}
"struct" => {
if let Some(type_child) = find_type_child(node) {
match type_child.kind() {
"struct_type" => meta.type_kind = Some("struct".to_string()),
"interface_type" => meta.type_kind = Some("interface".to_string()),
_ => meta.type_kind = Some("type".to_string()),
}
}
}
_ => {}
}
let json = serde_json::to_string(&meta)?;
Ok(json)
}
fn extract_name_text(node: &tree_sitter::Node, source: &str) -> Option<String> {
node.child_by_field_name("name")
.and_then(|n| n.utf8_text(source.as_bytes()).ok())
.map(|s| s.to_string())
}
fn find_type_child<'a>(node: &'a tree_sitter::Node<'a>) -> Option<tree_sitter::Node<'a>> {
node.child_by_field_name("type")
}
fn extract_receiver_info(node: &tree_sitter::Node, source: &str, meta: &mut GoMetadata) {
if let Some(receiver_node) = node.child_by_field_name("receiver") {
let mut cursor = receiver_node.walk();
for child in receiver_node.children(&mut cursor) {
if child.kind() == "parameter_declaration" {
if let Some(type_node) = child.child_by_field_name("type") {
if let Ok(type_text) = type_node.utf8_text(source.as_bytes()) {
let type_text = type_text.trim();
if let Some(stripped) = type_text.strip_prefix('*') {
meta.receiver = Some(stripped.to_string());
meta.is_pointer_receiver = true;
} else {
meta.receiver = Some(type_text.to_string());
meta.is_pointer_receiver = false;
}
}
}
}
}
}
}
fn extract_parameters_from_func(node: &tree_sitter::Node, source: &str, meta: &mut GoMetadata) {
if let Some(params_node) = node.child_by_field_name("parameters") {
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
if child.kind() == "parameter_declaration" {
let type_text = child
.child_by_field_name("type")
.and_then(|n| n.utf8_text(source.as_bytes()).ok())
.map(|s| s.trim().to_string());
let mut names = Vec::new();
let mut inner_cursor = child.walk();
for inner in child.children(&mut inner_cursor) {
if inner.kind() == "identifier" {
if let Ok(name) = inner.utf8_text(source.as_bytes()) {
names.push(name.to_string());
}
}
}
if names.is_empty() {
meta.parameters.push(GoParameterInfo {
name: String::new(),
type_annotation: type_text,
});
} else {
for name in names {
meta.parameters.push(GoParameterInfo {
name,
type_annotation: type_text.clone(),
});
}
}
}
}
}
}
fn extract_return_types(node: &tree_sitter::Node, source: &str, meta: &mut GoMetadata) {
if let Some(result_node) = node.child_by_field_name("result") {
match result_node.kind() {
"type_identifier" | "pointer_type" | "slice_type" | "map_type" | "channel_type"
| "qualified_type" | "array_type" | "interface_type" | "struct_type"
| "function_type" => {
if let Ok(text) = result_node.utf8_text(source.as_bytes()) {
meta.return_types.push(text.trim().to_string());
}
}
"parameter_list" => {
let mut cursor = result_node.walk();
for child in result_node.children(&mut cursor) {
if child.kind() == "parameter_declaration" {
if let Some(type_node) = child.child_by_field_name("type") {
if let Ok(text) = type_node.utf8_text(source.as_bytes()) {
meta.return_types.push(text.trim().to_string());
}
}
} else if child.kind() == "type_identifier"
|| child.kind() == "pointer_type"
|| child.kind() == "slice_type"
|| child.kind() == "qualified_type"
{
if let Ok(text) = child.utf8_text(source.as_bytes()) {
meta.return_types.push(text.trim().to_string());
}
}
}
}
_ => {
if let Ok(text) = result_node.utf8_text(source.as_bytes()) {
let trimmed = text.trim();
if !trimmed.is_empty() {
meta.return_types.push(trimmed.to_string());
}
}
}
}
}
}
fn extract_go_edge(
pattern: usize,
captures: &HashMap<String, (String, u32)>,
file_path: &str,
enclosing_scope_id: Option<&str>,
) -> Vec<Edge> {
let mut edges = Vec::new();
let from_fn = resolve_scope_id(enclosing_scope_id, file_path, "function");
let from_cls = resolve_scope_id(enclosing_scope_id, file_path, "class");
match pattern {
0 => {
if let Some((source_path, line)) = captures.get("source") {
let clean = source_path.trim_matches('"');
edges.push(make_edge(
format!("{file_path}::__module__::function"),
clean,
"imports",
file_path,
*line,
));
}
}
1 => {
if let Some((callee, line)) = captures.get("callee") {
edges.push(make_edge(
from_fn.clone(),
callee,
"calls",
file_path,
*line,
));
}
}
2 => {
if let (Some((object, line)), Some((method, _))) =
(captures.get("object"), captures.get("method"))
{
edges.push(make_edge(
from_fn.clone(),
format!("{object}.{method}"),
"calls",
file_path,
*line,
));
}
}
3 => {
if let Some((base_type, line)) = captures.get("base_type") {
edges.push(make_edge(
from_cls.clone(),
base_type,
"extends",
file_path,
*line,
));
}
}
_ => {}
}
edges
}