use super::*;
impl GoParser {
pub(crate) fn walk_node<'a>(&mut self, node: tree_sitter::Node<'a>) {
match node.kind() {
"source_file" => {
self.walk_children(node);
}
"block" => {
self.scopes.push(std::collections::HashMap::new());
self.walk_children(node);
self.scopes.pop();
}
"import_declaration" => {
self.walk_children(node);
}
"import_spec" => {
self.parse_import_spec(node);
}
"function_declaration" | "method_declaration" => {
self.handle_function(node);
}
"var_spec" | "const_spec" => {
self.handle_var_or_const(node);
}
"var_declaration" | "const_declaration" => {
self.walk_children(node);
}
"short_var_declaration" => {
self.handle_assignment(node);
}
"assignment_statement" => {
self.handle_assignment(node);
}
"return_statement" => {
self.handle_return(node);
}
"expression_statement" => {
self.walk_children(node);
}
"call_expression" => {
self.visit_call_expr(node);
}
_ => {
self.walk_children(node);
}
}
}
fn walk_children<'a>(&mut self, node: tree_sitter::Node<'a>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_named() {
self.walk_node(child);
}
}
}
fn source_text<'a>(&self, node: tree_sitter::Node<'a>) -> Option<String> {
node
.utf8_text(self.source.as_bytes())
.ok()
.map(|text| text.trim().to_string())
}
fn ensure_node(&mut self, raw_name: &str) -> NodeId {
let canonical = self.canonical_import_name(raw_name);
if canonical.is_empty() {
return self
.graph
.add_node(NodeKind::Variable, "<empty>".to_string(), None);
}
if let Some(existing) = self.resolve_in_scope(&canonical) {
return existing;
}
let id = self.graph.add_node(NodeKind::Variable, canonical.clone(), None);
#[allow(clippy::expect_used)]
self.scopes.last_mut().expect("scope stack not empty").insert(canonical, id);
id
}
fn resolve_in_scope(&self, raw_name: &str) -> Option<NodeId> {
self.scopes
.iter()
.rev()
.find_map(|scope| scope.get(raw_name).copied())
}
fn canonical_import_name(&self, text: &str) -> String {
let trimmed = text.trim();
if trimmed.is_empty() {
return String::new();
}
let mut parts = trimmed.split('.');
let first = parts.next().unwrap_or_default();
let rest = parts.collect::<Vec<_>>().join(".");
let package = self.imports.get(first).cloned().unwrap_or_else(|| first.to_string());
if rest.is_empty() {
package
} else {
format!("{package}.{rest}")
}
}
fn normalize_package_name(&self, path: &str) -> String {
path.trim_matches('"')
.trim_matches('`')
.trim()
.to_string()
}
fn parse_import_spec<'a>(&mut self, node: tree_sitter::Node<'a>) {
let Some(path_node) = node.child_by_field_name("path") else {
return;
};
let Some(raw_path) = self.source_text(path_node) else {
return;
};
let Some(name_node) = node.child_by_field_name("name") else {
let package = self.normalize_package_name(&raw_path);
let base = package.rsplit('/').next().unwrap_or(&package).to_string();
self.imports.insert(base.clone(), package);
return;
};
let raw_name = self.source_text(name_node).unwrap_or_default();
let alias = raw_name.trim();
if alias == "." || alias == "_" || alias.is_empty() {
return;
}
let package = self.normalize_package_name(&raw_path);
let base = package.rsplit('/').next().unwrap_or(&package);
let import_alias = alias.to_string();
let import_package = if import_alias == base {
package.clone()
} else {
package
};
self.imports.insert(import_alias, import_package);
}
fn collect_bindings<'a>(&self, node: tree_sitter::Node<'a>) -> Vec<String> {
let mut names = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if !child.is_named() {
continue;
}
match child.kind() {
"identifier" | "field_identifier" | "package_identifier" => {
if let Some(raw) = self.source_text(child) {
let canonical = self.canonical_import_name(&raw);
if !canonical.is_empty() && canonical != "_" {
names.push(canonical);
}
}
}
"expression_list" | "parameter_declaration" | "value_spec" | "var_spec" => {
names.extend(self.collect_bindings(child));
}
_ => {}
}
}
names
}
fn identifier_or_expr_name<'a>(&self, node: tree_sitter::Node<'a>) -> Option<String> {
match node.kind() {
"identifier"
| "field_identifier"
| "package_identifier"
| "type_identifier"
| "qualified_identifier"
| "selector_expression" => {
self.source_text(node).map(|raw| self.canonical_import_name(&raw))
}
"call_expression" => {
node.child_by_field_name("function")
.and_then(|fn_node| self.identifier_or_expr_name(fn_node))
}
_ => self.source_text(node),
}
}
fn mark_network_conn_source_from_text(&mut self, raw: &str, names: &[String]) {
if !raw.contains("net.Conn") {
return;
}
let source_id = self.ensure_node("net.Conn");
for name in names {
if name == "_" {
continue;
}
let lhs_id = self.ensure_node(name);
self.graph
.add_edge(source_id, lhs_id, EdgeKind::Assignment);
}
}
fn materialize_expr<'a>(&mut self, node: tree_sitter::Node<'a>) -> NodeId {
let id = self
.graph
.add_node(NodeKind::Literal, format!("expr:{}:{}", node.start_byte(), node.end_byte()), None);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if !child.is_named() {
continue;
}
let child_id = self.visit_expr(child);
self.graph
.add_edge(child_id, id, EdgeKind::Assignment);
}
id
}
fn visit_expr<'a>(&mut self, node: tree_sitter::Node<'a>) -> NodeId {
match node.kind() {
"identifier" | "field_identifier" | "package_identifier" | "type_identifier" => {
self.source_text(node)
.map(|raw| self.ensure_node(&raw))
.unwrap_or_else(|| {
self.graph
.add_node(NodeKind::Literal, "<unknown>".to_string(), None)
})
}
"qualified_identifier" | "selector_expression" => {
self.source_text(node)
.map(|raw| self.ensure_node(&raw))
.unwrap_or_else(|| self.materialize_expr(node))
}
"call_expression" => self.visit_call_expr(node),
"expression_list" | "argument_list" | "parenthesized_expression"
| "composite_literal" | "binary_expression" | "unary_expression"
| "slice_expression" | "type_assertion_expression"
| "type_conversion_expression" | "type_instantiation_expression"
| "index_expression" => {
self.materialize_expr(node)
}
"func_literal" => {
let fn_id = self.graph.add_node(
NodeKind::Literal,
format!("<func_lit:{}:{}>", node.start_byte(), node.end_byte()),
None,
);
let prev_fn = self.current_function;
self.current_function = Some(fn_id);
self.scopes.push(std::collections::HashMap::new());
if let Some(params) = node.child_by_field_name("parameters") {
let mut cursor = params.walk();
for param in params.children(&mut cursor) {
if !param.is_named() {
continue;
}
let names = self.collect_bindings(param);
let raw = self.source_text(param).unwrap_or_default();
self.mark_network_conn_source_from_text(&raw, &names);
for name in names {
let param_id = self.ensure_node(&name);
self.current_param_index
.entry(fn_id)
.or_default()
.push(param_id);
}
}
}
if let Some(body) = node.child_by_field_name("body") {
self.walk_node(body);
}
self.scopes.pop();
self.current_function = prev_fn;
fn_id
}
"interpreted_string_literal"
| "raw_string_literal"
| "int_lit"
| "float_lit"
| "rune_lit"
| "imaginary_literal" => {
self.graph.add_node(
NodeKind::Literal,
self.source_text(node)
.unwrap_or_else(|| format!("literal:{}:{}", node.start_byte(), node.end_byte())),
None,
)
}
"nil" | "true" | "false" | "iota" => {
self.graph.add_node(NodeKind::Literal, node.kind().to_string(), None)
}
_ => self.materialize_expr(node),
}
}
fn collect_expr_ids<'a>(&mut self, node: Option<tree_sitter::Node<'a>>) -> Vec<NodeId> {
let Some(node) = node else {
return Vec::new();
};
if node.kind() == "expression_list" || node.kind() == "argument_list" {
let mut ids = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_named() {
ids.push(self.visit_expr(child));
}
}
return ids;
}
vec![self.visit_expr(node)]
}
fn visit_call_expr<'a>(&mut self, node: tree_sitter::Node<'a>) -> NodeId {
let Some(function_node) = node.child_by_field_name("function") else {
return self
.graph
.add_node(NodeKind::Call, "<call>".to_string(), None);
};
let mut call_name = self
.identifier_or_expr_name(function_node)
.unwrap_or_else(|| "call".to_string());
call_name = self.canonical_import_name(&call_name);
if function_node.kind() == "selector_expression" {
let field = function_node
.child_by_field_name("field")
.and_then(|field| self.source_text(field))
.unwrap_or_default();
let operand_node = function_node.child_by_field_name("operand");
let operand_name = operand_node
.as_ref()
.and_then(|node| self.source_text(*node))
.map(|name| self.canonical_import_name(&name));
let raw_operand = operand_node
.as_ref()
.and_then(|node| self.identifier_or_expr_name(*node))
.unwrap_or_default();
let inner_function = operand_node
.and_then(|node| node.child_by_field_name("function"))
.and_then(|fn_node| self.source_text(fn_node))
.unwrap_or_default();
if field == "Run" || field == "Output" || field == "CombinedOutput" {
if inner_function == "exec.Command"
|| raw_operand == "exec.Command"
|| raw_operand.ends_with("exec.Command")
|| {
if let Some(ref name) = operand_name {
self.resolve_in_scope(name.as_str())
.is_some_and(|id| self.command_vars.get(&id).copied().unwrap_or(false))
} else {
false
}
}
{
call_name = format!("exec.Command.{field}");
}
}
if field == "Call"
&& (inner_function == "reflect.ValueOf"
|| raw_operand == "reflect.ValueOf"
|| raw_operand.ends_with("reflect.ValueOf")
|| {
if let Some(ref name) = operand_name {
self.resolve_in_scope(&name)
.is_some_and(|id| self.reflect_vars.get(&id).copied().unwrap_or(false))
} else {
false
}
})
{
call_name = "reflect.ValueOf.Call".to_string();
}
if field == "Query"
&& operand_name
.as_deref()
.is_some_and(|value| value.ends_with("URL"))
{
call_name = "URL.Query".to_string();
}
if field == "FormValue" {
call_name = "http.Request.FormValue".to_string();
}
}
let call_id = self.graph.add_node(NodeKind::Call, call_name, None);
let function_id = self.visit_expr(function_node);
if function_node.kind() == "selector_expression" {
if let Some(operand_node) = function_node.child_by_field_name("operand") {
let operand_id = self.visit_expr(operand_node);
self.graph
.add_edge(operand_id, call_id, EdgeKind::Argument);
} else {
self.graph
.add_edge(function_id, call_id, EdgeKind::Call);
}
} else {
self.graph
.add_edge(function_id, call_id, EdgeKind::Call);
}
if let Some(args) = node.child_by_field_name("arguments") {
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !child.is_named() {
continue;
}
let arg_id = self.visit_expr(child);
self.graph.add_edge(arg_id, call_id, EdgeKind::Argument);
}
}
call_id
}
fn mark_command_or_reflect_receiver(&mut self, lhs: &str, rhs_call: &str) {
if let Some(id) = self.resolve_in_scope(lhs) {
if rhs_call == "exec.Command" || rhs_call.ends_with("exec.Command") {
self.command_vars.insert(id, true);
} else if rhs_call == "reflect.ValueOf" || rhs_call.ends_with("reflect.ValueOf") {
self.reflect_vars.insert(id, true);
}
}
}
fn handle_var_or_const<'a>(&mut self, node: tree_sitter::Node<'a>) {
let names = self.collect_bindings(node);
let raw = self.source_text(node).unwrap_or_default();
self.mark_network_conn_source_from_text(&raw, &names);
let rhs_node = node.child_by_field_name("value");
let rhs_ids = self.collect_expr_ids(rhs_node);
let rhs_call = rhs_node
.and_then(|node| {
if node.kind() == "expression_list" || node.kind() == "argument_list" {
let mut cursor = node.walk();
let first = node.children(&mut cursor).find(|c| c.is_named());
first.and_then(|f| f.child_by_field_name("function"))
} else {
node.child_by_field_name("function")
}
})
.and_then(|node| self.identifier_or_expr_name(node))
.unwrap_or_default();
if names.is_empty() {
return;
}
let nil = self
.graph
.add_node(NodeKind::Literal, "<nil>".to_string(), None);
for (index, name) in names.iter().enumerate() {
let lhs_id = self.ensure_node(name);
let source_id = rhs_ids
.get(index)
.copied()
.or_else(|| rhs_ids.first().copied())
.unwrap_or(nil);
self.graph
.add_edge(source_id, lhs_id, EdgeKind::Assignment);
self.mark_command_or_reflect_receiver(name, &rhs_call);
}
}
fn handle_assignment<'a>(&mut self, node: tree_sitter::Node<'a>) {
let lhs_node = node.child_by_field_name("left");
let rhs_node = node.child_by_field_name("right");
let lhs_names = lhs_node
.map(|node| self.collect_bindings(node))
.unwrap_or_default();
let rhs_ids = self.collect_expr_ids(rhs_node);
let rhs_call = rhs_node
.and_then(|node| {
if node.kind() == "expression_list" || node.kind() == "argument_list" {
let mut cursor = node.walk();
let first = node.children(&mut cursor).find(|c| c.is_named());
first.and_then(|f| f.child_by_field_name("function"))
} else {
node.child_by_field_name("function")
}
})
.and_then(|node| self.identifier_or_expr_name(node))
.unwrap_or_default();
let raw = self.source_text(node).unwrap_or_default();
self.mark_network_conn_source_from_text(&raw, &lhs_names);
if lhs_names.is_empty() {
return;
}
let nil = self
.graph
.add_node(NodeKind::Literal, "<nil>".to_string(), None);
for (index, name) in lhs_names.iter().enumerate() {
let lhs_id = self.ensure_node(name);
let source_id = rhs_ids
.get(index)
.copied()
.or_else(|| rhs_ids.first().copied())
.unwrap_or(nil);
self.graph
.add_edge(source_id, lhs_id, EdgeKind::Assignment);
self.mark_command_or_reflect_receiver(name, &rhs_call);
}
}
fn handle_return<'a>(&mut self, node: tree_sitter::Node<'a>) {
let Some(current_fn) = self.current_function else {
return;
};
let mut emitted = false;
if let Some(result) = node.child_by_field_name("result") {
let mut cursor = result.walk();
for child in result.children(&mut cursor) {
if child.is_named() {
let value_id = self.visit_expr(child);
self.graph
.add_edge(value_id, current_fn, EdgeKind::Return);
emitted = true;
}
}
} else {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "return" || !child.is_named() {
continue;
}
let value_id = self.visit_expr(child);
self.graph.add_edge(value_id, current_fn, EdgeKind::Return);
emitted = true;
}
}
if !emitted {
let none = self
.graph
.add_node(NodeKind::Literal, "<nil>".to_string(), None);
self.graph.add_edge(none, current_fn, EdgeKind::Return);
}
}
fn handle_function<'a>(&mut self, node: tree_sitter::Node<'a>) {
let fn_name = node
.child_by_field_name("name")
.and_then(|name| self.source_text(name))
.unwrap_or_else(|| "<anonymous>".to_string());
let fn_id = self.ensure_node(&fn_name);
self.current_function = Some(fn_id);
self.scopes.push(std::collections::HashMap::new());
if let Some(params) = node.child_by_field_name("parameters") {
let mut cursor = params.walk();
for param in params.children(&mut cursor) {
if !param.is_named() {
continue;
}
let names = self.collect_bindings(param);
let raw = self.source_text(param).unwrap_or_default();
self.mark_network_conn_source_from_text(&raw, &names);
for name in names {
let param_id = self.ensure_node(&name);
self.current_param_index
.entry(fn_id)
.or_default()
.push(param_id);
}
}
}
if let Some(body) = node.child_by_field_name("body") {
self.walk_node(body);
}
self.scopes.pop();
self.current_function = None;
}
}