use std::collections::HashMap;
use std::path::Path;
use sqry_core::graph::unified::edge::FfiConvention;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
use sqry_core::graph::{
GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
};
use tree_sitter::{Node, Tree};
use super::local_scopes;
use super::type_extractor::{extract_all_type_names_from_annotation, extract_type_string};
const DEFAULT_MAX_SCOPE_DEPTH: usize = 6;
const FILE_MODULE_NAME: &str = "<file_module>";
#[derive(Debug, Clone, Copy)]
pub struct CSharpGraphBuilder {
max_scope_depth: usize,
}
impl Default for CSharpGraphBuilder {
fn default() -> Self {
Self {
max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
}
}
}
impl CSharpGraphBuilder {
#[must_use]
pub fn new(max_scope_depth: usize) -> Self {
Self { max_scope_depth }
}
}
impl GraphBuilder for CSharpGraphBuilder {
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::CSharp);
let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
GraphBuilderError::ParseError {
span: Span::default(),
reason: e,
}
})?;
let mut node_map = HashMap::new();
for context in ast_graph.contexts() {
let qualified_name = &context.qualified_name;
let span = Span::from_bytes(context.span.0, context.span.1);
let node_id = match context.kind {
ContextKind::Function { is_async } => {
helper.add_function_with_signature(
qualified_name,
Some(span),
is_async,
false,
None, context.return_type.as_deref(),
)
}
ContextKind::Method {
is_async,
is_static,
} => {
helper.add_method_with_signature(
qualified_name,
Some(span),
is_async,
is_static,
None, context.return_type.as_deref(),
)
}
ContextKind::Class => helper.add_class(qualified_name, Some(span)),
ContextKind::Interface => helper.add_interface(qualified_name, Some(span)),
};
node_map.insert(qualified_name.clone(), node_id);
}
let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
let mut namespace_stack = Vec::new();
let mut class_stack = Vec::new();
let root = tree.root_node();
walk_tree_for_edges(
root,
content,
&ast_graph,
&mut helper,
&mut node_map,
&mut namespace_stack,
&mut class_stack,
&mut scope_tree,
)?;
Ok(())
}
fn language(&self) -> Language {
Language::CSharp
}
fn detect_cross_language_edges(
&self,
_snapshot: &GraphSnapshot,
) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
Ok(vec![])
}
}
#[derive(Debug, Clone)]
enum ContextKind {
Function { is_async: bool },
Method { is_async: bool, is_static: bool },
Class,
Interface,
}
#[derive(Debug, Clone)]
struct CallContext {
qualified_name: String,
span: (usize, usize),
kind: ContextKind,
class_name: Option<String>,
return_type: Option<String>,
}
struct ASTGraph {
contexts: Vec<CallContext>,
node_to_context: HashMap<usize, usize>,
}
impl ASTGraph {
fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
let mut contexts = Vec::new();
let mut node_to_context = HashMap::new();
let mut scope_stack: Vec<String> = Vec::new();
let mut class_stack: Vec<String> = Vec::new();
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.map_err(|e| format!("Failed to load recursion limits: {e}"))?;
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.map_err(|e| format!("Failed to create recursion guard: {e}"))?;
let mut walk_context = WalkContext {
content,
contexts: &mut contexts,
node_to_context: &mut node_to_context,
scope_stack: &mut scope_stack,
class_stack: &mut class_stack,
max_depth,
guard: &mut guard,
};
walk_ast(tree.root_node(), &mut walk_context)?;
Ok(Self {
contexts,
node_to_context,
})
}
fn contexts(&self) -> &[CallContext] {
&self.contexts
}
fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
self.node_to_context
.get(&node_id)
.and_then(|idx| self.contexts.get(*idx))
}
}
#[allow(clippy::too_many_lines)] struct WalkContext<'a> {
content: &'a [u8],
contexts: &'a mut Vec<CallContext>,
node_to_context: &'a mut HashMap<usize, usize>,
scope_stack: &'a mut Vec<String>,
class_stack: &'a mut Vec<String>,
max_depth: usize,
guard: &'a mut sqry_core::query::security::RecursionGuard,
}
#[allow(clippy::too_many_lines)]
fn walk_ast(node: Node, context: &mut WalkContext<'_>) -> Result<(), String> {
context
.guard
.enter()
.map_err(|e| format!("Recursion limit exceeded: {e}"))?;
if context.scope_stack.len() > context.max_depth {
context.guard.exit();
return Ok(());
}
match node.kind() {
"class_declaration" => {
let name_node = node
.child_by_field_name("name")
.ok_or_else(|| "class_declaration missing name".to_string())?;
let class_name = name_node
.utf8_text(context.content)
.map_err(|_| "failed to read class name".to_string())?;
let qualified_class = if context.scope_stack.is_empty() {
class_name.to_string()
} else {
format!("{}.{}", context.scope_stack.join("."), class_name)
};
context.class_stack.push(qualified_class.clone());
context.scope_stack.push(class_name.to_string());
let _context_idx = context.contexts.len();
context.contexts.push(CallContext {
qualified_name: qualified_class.clone(),
span: (node.start_byte(), node.end_byte()),
kind: ContextKind::Class,
class_name: Some(qualified_class),
return_type: None,
});
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_ast(child, context)?;
}
}
context.class_stack.pop();
context.scope_stack.pop();
}
"interface_declaration" => {
let name_node = node
.child_by_field_name("name")
.ok_or_else(|| "interface_declaration missing name".to_string())?;
let interface_name = name_node
.utf8_text(context.content)
.map_err(|_| "failed to read interface name".to_string())?;
let qualified_interface = if context.scope_stack.is_empty() {
interface_name.to_string()
} else {
format!("{}.{}", context.scope_stack.join("."), interface_name)
};
context.class_stack.push(qualified_interface.clone());
context.scope_stack.push(interface_name.to_string());
let _context_idx = context.contexts.len();
context.contexts.push(CallContext {
qualified_name: qualified_interface.clone(),
span: (node.start_byte(), node.end_byte()),
kind: ContextKind::Interface,
class_name: Some(qualified_interface),
return_type: None,
});
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_ast(child, context)?;
}
}
context.class_stack.pop();
context.scope_stack.pop();
}
"method_declaration" | "constructor_declaration" | "local_function_statement" => {
let name_node = node
.child_by_field_name("name")
.ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
let func_name = name_node
.utf8_text(context.content)
.map_err(|_| "failed to read function name".to_string())?;
let is_async = has_modifier(node, context.content, "async");
let is_static = has_modifier(node, context.content, "static");
let return_type = node
.child_by_field_name("return_type")
.or_else(|| node.child_by_field_name("returns"))
.or_else(|| node.child_by_field_name("type"))
.and_then(|type_node| type_node.utf8_text(context.content).ok())
.map(std::string::ToString::to_string);
let qualified_func = if context.scope_stack.is_empty() {
func_name.to_string()
} else {
format!("{}.{}", context.scope_stack.join("."), func_name)
};
let is_method = !context.class_stack.is_empty();
let class_name = context.class_stack.last().cloned();
let kind = if is_method {
ContextKind::Method {
is_async,
is_static,
}
} else {
ContextKind::Function { is_async }
};
let context_idx = context.contexts.len();
context.contexts.push(CallContext {
qualified_name: qualified_func.clone(),
span: (node.start_byte(), node.end_byte()),
kind,
class_name,
return_type,
});
if let Some(body) = node.child_by_field_name("body") {
associate_descendants(body, context_idx, context.node_to_context);
}
context.scope_stack.push(func_name.to_string());
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_ast(child, context)?;
}
}
context.scope_stack.pop();
}
"namespace_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(namespace_name) = name_node.utf8_text(context.content)
{
context.scope_stack.push(namespace_name.to_string());
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_ast(child, context)?;
}
}
context.scope_stack.pop();
}
}
_ => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_ast(child, context)?;
}
}
}
context.guard.exit();
Ok(())
}
fn associate_descendants(
node: Node,
context_idx: usize,
node_to_context: &mut HashMap<usize, usize>,
) {
node_to_context.insert(node.id(), context_idx);
let mut stack = vec![node];
while let Some(current) = stack.pop() {
node_to_context.insert(current.id(), context_idx);
let mut cursor = current.walk();
for child in current.children(&mut cursor) {
stack.push(child);
}
}
}
#[allow(clippy::too_many_lines)] fn walk_tree_for_edges(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
namespace_stack: &mut Vec<String>,
class_stack: &mut Vec<String>,
scope_tree: &mut local_scopes::CSharpScopeTree,
) -> GraphResult<()> {
match node.kind() {
"namespace_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(namespace_name) = name_node.utf8_text(content)
{
namespace_stack.push(namespace_name.to_string());
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_tree_for_edges(
child,
content,
ast_graph,
helper,
node_map,
namespace_stack,
class_stack,
scope_tree,
)?;
}
}
namespace_stack.pop();
return Ok(());
}
}
"class_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(class_name) = name_node.utf8_text(content)
{
let qualified_class =
build_qualified_name(namespace_stack, class_stack, class_name);
class_stack.push(class_name.to_string());
process_class_declaration(
node,
content,
helper,
node_map,
&qualified_class,
namespace_stack,
);
process_type_parameter_declarations(node, content, &qualified_class, helper);
if should_export(node, content)
&& let Some(class_id) = node_map.get(&qualified_class)
{
export_from_file_module(helper, *class_id);
}
if let Some(body) = node.child_by_field_name("body") {
process_class_member_exports(body, content, &qualified_class, helper, node_map);
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_tree_for_edges(
child,
content,
ast_graph,
helper,
node_map,
namespace_stack,
class_stack,
scope_tree,
)?;
}
}
class_stack.pop();
return Ok(());
}
}
"interface_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(interface_name) = name_node.utf8_text(content)
{
let qualified_interface =
build_qualified_name(namespace_stack, class_stack, interface_name);
process_interface_declaration(
node,
content,
helper,
node_map,
&qualified_interface,
namespace_stack,
);
process_type_parameter_declarations(node, content, &qualified_interface, helper);
if should_export(node, content)
&& let Some(interface_id) = node_map.get(&qualified_interface)
{
export_from_file_module(helper, *interface_id);
}
if let Some(body) = node.child_by_field_name("body") {
process_interface_member_exports(
body,
content,
&qualified_interface,
helper,
node_map,
);
class_stack.push(interface_name.to_string());
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_tree_for_edges(
child,
content,
ast_graph,
helper,
node_map,
namespace_stack,
class_stack,
scope_tree,
)?;
}
class_stack.pop();
}
return Ok(());
}
}
"invocation_expression" => {
process_invocation(node, content, ast_graph, helper, node_map);
}
"object_creation_expression" => {
process_object_creation(node, content, ast_graph, helper, node_map);
}
"using_directive" => {
process_using_directive(node, content, helper);
}
"method_declaration" => {
process_pinvoke_method(node, content, helper, node_map, namespace_stack);
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(method_name) = name_node.utf8_text(content)
{
let mut scope_parts = namespace_stack.clone();
scope_parts.extend(class_stack.iter().cloned());
let qualified_name = if scope_parts.is_empty() {
method_name.to_string()
} else {
format!("{}.{}", scope_parts.join("."), method_name)
};
process_method_parameters(node, &qualified_name, content, helper);
process_method_return_type(node, &qualified_name, content, helper);
process_type_parameter_declarations(node, content, &qualified_name, helper);
}
}
"local_declaration_statement" => {
process_local_variables(node, content, helper, class_stack);
}
"field_declaration" => {
process_field_declaration(node, content, helper, class_stack);
}
"property_declaration" => {
process_property_declaration(node, content, helper, class_stack);
}
"identifier" => {
local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_tree_for_edges(
child,
content,
ast_graph,
helper,
node_map,
namespace_stack,
class_stack,
scope_tree,
)?;
}
Ok(())
}
fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
let mut parts = Vec::new();
parts.extend(namespace_stack.iter().cloned());
parts.extend(class_stack.iter().cloned());
parts.push(name.to_string());
parts.join(".")
}
fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
if type_name.contains('.') {
return type_name.to_string();
}
if namespace_stack.is_empty() {
return type_name.to_string();
}
format!("{}.{}", namespace_stack.join("."), type_name)
}
fn process_invocation(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(function_node) = node.child_by_field_name("function") else {
return;
};
let Ok(callee_text) = function_node.utf8_text(content) else {
return;
};
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let callee_qualified = if callee_text.contains('.') {
callee_text.to_string()
} else if let Some(class_name) = &call_context.class_name {
format!("{class_name}.{callee_text}")
} else {
callee_text.to_string()
};
let caller_function_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let target_function_id = *node_map
.entry(callee_qualified.clone())
.or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
let argument_count = count_call_arguments(node);
let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
helper.add_call_edge_full_with_span(
caller_function_id,
target_function_id,
argument_count,
false,
vec![call_span],
);
}
fn process_object_creation(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(type_node) = node.child_by_field_name("type") else {
return;
};
let Ok(type_name) = type_node.utf8_text(content) else {
return;
};
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let callee_qualified = format!("{type_name}.ctor");
let caller_function_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let target_function_id = *node_map
.entry(callee_qualified.clone())
.or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
let argument_count = count_call_arguments(node);
let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
helper.add_call_edge_full_with_span(
caller_function_id,
target_function_id,
argument_count,
false,
vec![call_span],
);
}
fn count_call_arguments(call_node: Node<'_>) -> u8 {
let args_node = call_node
.child_by_field_name("arguments")
.or_else(|| call_node.child_by_field_name("argument_list"))
.or_else(|| {
let mut cursor = call_node.walk();
call_node
.children(&mut cursor)
.find(|child| child.kind() == "argument_list")
});
let Some(args_node) = args_node else {
return 255;
};
let count = args_node.named_child_count();
if count <= 254 {
u8::try_from(count).unwrap_or(u8::MAX)
} else {
u8::MAX
}
}
fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
let is_static = node
.children(&mut node.walk())
.any(|child| child.kind() == "static");
let has_equals = node
.children(&mut node.walk())
.any(|child| child.kind() == "=");
let (alias, imported_name) = if has_equals {
extract_aliased_using(node, content)
} else {
(None, extract_simple_using_target(node, content))
};
let Some(imported_name) = imported_name else {
return;
};
let module_id = helper.add_module("<file>", None);
let span = Span::from_bytes(node.start_byte(), node.end_byte());
let import_name = if is_static {
format!("static {imported_name}")
} else {
imported_name.clone()
};
let imported_id = helper.add_import(&import_name, Some(span));
match (alias.as_deref(), is_static) {
(Some(alias_str), _) => {
helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
}
(None, true) => {
helper.add_import_edge_full(module_id, imported_id, None, true);
}
(None, false) => {
helper.add_import_edge(module_id, imported_id);
}
}
}
fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
let mut alias: Option<String> = None;
let mut target: Option<String> = None;
let mut past_equals = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let kind = child.kind();
if kind == "=" {
past_equals = true;
continue;
}
if kind == "using" || kind == "static" || kind == ";" {
continue;
}
if past_equals {
if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
target = child
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string);
}
} else if kind == "identifier" && alias.is_none() {
alias = child
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string);
}
}
(alias, target)
}
fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let kind = child.kind();
if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
continue;
}
if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
return child
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string);
}
}
node.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(std::string::ToString::to_string)
}
fn process_class_declaration(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
qualified_class_name: &str,
namespace_stack: &[String],
) {
let class_id = *node_map
.entry(qualified_class_name.to_string())
.or_insert_with(|| helper.add_class(qualified_class_name, None));
let mut cursor = node.walk();
let base_list = node
.children(&mut cursor)
.find(|child| child.kind() == "base_list");
let Some(base_list) = base_list else {
return;
};
let mut first_base_class = true;
let mut base_cursor = base_list.walk();
for base_child in base_list.children(&mut base_cursor) {
let base_type_name = match base_child.kind() {
"identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string),
"generic_name" => {
base_child
.child_by_field_name("name")
.or_else(|| base_child.child(0))
.and_then(|n| n.utf8_text(content).ok())
.map(std::string::ToString::to_string)
}
_ => None,
};
let Some(base_name) = base_type_name else {
continue;
};
let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
let is_interface = is_interface_name(&base_name);
if is_interface {
let interface_id = *node_map
.entry(qualified_base_name.clone())
.or_insert_with(|| helper.add_interface(&qualified_base_name, None));
helper.add_implements_edge(class_id, interface_id);
} else if first_base_class {
let parent_id = *node_map
.entry(qualified_base_name.clone())
.or_insert_with(|| helper.add_class(&qualified_base_name, None));
helper.add_inherits_edge(class_id, parent_id);
first_base_class = false;
}
}
}
fn process_interface_declaration(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
qualified_interface_name: &str,
namespace_stack: &[String],
) {
let interface_id = *node_map
.entry(qualified_interface_name.to_string())
.or_insert_with(|| helper.add_interface(qualified_interface_name, None));
let mut cursor = node.walk();
let base_list = node
.children(&mut cursor)
.find(|child| child.kind() == "base_list");
let Some(base_list) = base_list else {
return;
};
let mut base_cursor = base_list.walk();
for base_child in base_list.children(&mut base_cursor) {
let parent_name = match base_child.kind() {
"identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string),
"generic_name" => base_child
.child_by_field_name("name")
.or_else(|| base_child.child(0))
.and_then(|n| n.utf8_text(content).ok())
.map(std::string::ToString::to_string),
_ => None,
};
let Some(parent_name) = parent_name else {
continue;
};
let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
let parent_id = *node_map
.entry(qualified_parent_name.clone())
.or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
helper.add_inherits_edge(interface_id, parent_id);
}
}
fn is_interface_name(name: &str) -> bool {
let chars: Vec<char> = name.chars().collect();
if chars.len() >= 2 {
chars[0] == 'I' && chars[1].is_ascii_uppercase()
} else {
false
}
}
fn is_public(node: Node, content: &[u8]) -> bool {
has_visibility_modifier(node, content, "public")
}
fn is_internal(node: Node, content: &[u8]) -> bool {
has_visibility_modifier(node, content, "internal")
}
fn is_private(node: Node, content: &[u8]) -> bool {
has_visibility_modifier(node, content, "private")
}
#[allow(dead_code)] fn is_protected(node: Node, content: &[u8]) -> bool {
has_visibility_modifier(node, content, "protected")
}
fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
node.children(&mut node.walk())
.any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
}
fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
node.children(&mut node.walk())
.any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
}
fn should_export(node: Node, content: &[u8]) -> bool {
is_public(node, content) || is_internal(node, content)
}
fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
let module_id = helper.add_module(FILE_MODULE_NAME, None);
helper.add_export_edge(module_id, exported);
}
fn process_class_member_exports(
body_node: Node,
content: &[u8],
class_qualified_name: &str,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
match child.kind() {
"method_declaration" | "constructor_declaration" => {
if should_export(child, content)
&& let Some(name_node) = child.child_by_field_name("name")
&& let Ok(method_name) = name_node.utf8_text(content)
{
let qualified_name = format!("{class_qualified_name}.{method_name}");
if let Some(method_id) = node_map.get(&qualified_name) {
export_from_file_module(helper, *method_id);
}
} else if should_export(child, content) && child.kind() == "constructor_declaration"
{
let class_name = class_qualified_name
.rsplit('.')
.next()
.unwrap_or(class_qualified_name);
let qualified_name = format!("{class_qualified_name}.{class_name}");
if let Some(method_id) = node_map.get(&qualified_name) {
export_from_file_module(helper, *method_id);
}
}
}
"field_declaration" | "property_declaration" => {
if should_export(child, content) {
let is_property = child.kind() == "property_declaration";
let is_const = !is_property && has_modifier(child, content, "const");
let is_readonly = !is_property && has_modifier(child, content, "readonly");
let is_static = is_const || has_modifier(child, content, "static");
let visibility = extract_field_visibility(child, content);
let get_only = is_property && is_get_only_property(child);
let emit_constant = is_const || is_readonly || get_only;
let mut field_cursor = child.walk();
for field_child in child.children(&mut field_cursor) {
if field_child.kind() == "variable_declarator"
&& let Some(name_node) = field_child.child_by_field_name("name")
&& let Ok(field_name) = name_node.utf8_text(content)
{
let qualified_name = format!("{class_qualified_name}.{field_name}");
let span =
Span::from_bytes(field_child.start_byte(), field_child.end_byte());
let field_id = if emit_constant {
helper.add_constant_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
} else {
helper.add_property_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
};
export_from_file_module(helper, field_id);
} else if field_child.kind() == "identifier"
&& let Ok(prop_name) = field_child.utf8_text(content)
{
let qualified_name = format!("{class_qualified_name}.{prop_name}");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let prop_id = if emit_constant {
helper.add_constant_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
} else {
helper.add_property_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
};
export_from_file_module(helper, prop_id);
}
}
}
}
_ => {}
}
}
}
fn process_interface_member_exports(
body_node: Node,
content: &[u8],
interface_qualified_name: &str,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
if child.kind() == "method_declaration"
&& !is_private(child, content)
&& let Some(name_node) = child.child_by_field_name("name")
&& let Ok(method_name) = name_node.utf8_text(content)
{
let qualified_name = format!("{interface_qualified_name}.{method_name}");
if let Some(method_id) = node_map.get(&qualified_name) {
export_from_file_module(helper, *method_id);
}
}
}
}
fn process_pinvoke_method(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
namespace_stack: &[String],
) {
let has_extern = node
.children(&mut node.walk())
.any(|child| child.kind() == "extern");
if !has_extern {
return;
}
let mut cursor = node.walk();
let attribute_list = node
.children(&mut cursor)
.find(|child| child.kind() == "attribute_list");
let Some(attribute_list) = attribute_list else {
return;
};
let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
let Some(dll_name) = dll_name else {
return;
};
let method_name = node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(std::string::ToString::to_string);
let Some(method_name) = method_name else {
return;
};
let qualified_method = if namespace_stack.is_empty() {
method_name.clone()
} else {
format!("{}.{}", namespace_stack.join("."), method_name)
};
let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
let method_id = *node_map
.entry(qualified_method.clone())
.or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
let ffi_func_id = *node_map
.entry(ffi_func_name.clone())
.or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
let convention = match calling_convention.as_deref() {
Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
_ => FfiConvention::Stdcall,
};
helper.add_ffi_edge(method_id, ffi_func_id, convention);
}
fn extract_dllimport_info(
attribute_list: Node,
content: &[u8],
) -> (Option<String>, Option<String>) {
let mut dll_name = None;
let mut calling_convention = None;
let mut list_cursor = attribute_list.walk();
for attr_child in attribute_list.children(&mut list_cursor) {
if attr_child.kind() != "attribute" {
continue;
}
let attr_name = attr_child
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok());
let is_dllimport = attr_name.is_some_and(|name| {
name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
});
if !is_dllimport {
continue;
}
let mut attr_cursor = attr_child.walk();
let arg_list = attr_child
.children(&mut attr_cursor)
.find(|child| child.kind() == "attribute_argument_list");
let Some(arg_list) = arg_list else {
continue;
};
let mut arg_cursor = arg_list.walk();
for arg in arg_list.children(&mut arg_cursor) {
if arg.kind() != "attribute_argument" {
continue;
}
if let Some(name_node) = arg.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(content)
{
if name == "CallingConvention"
&& let Some(expr) = arg.child_by_field_name("expression")
&& let Ok(value) = expr.utf8_text(content)
{
calling_convention = Some(value.to_string());
}
continue;
}
if dll_name.is_none() {
let expr = arg.child_by_field_name("expression").or_else(|| {
let mut c = arg.walk();
arg.children(&mut c)
.find(|child| child.kind() == "string_literal")
});
if let Some(expr) = expr
&& let Ok(text) = expr.utf8_text(content)
{
let trimmed = text.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('@') && trimmed.len() > 2)
{
let start = if trimmed.starts_with('@') { 2 } else { 1 };
dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
} else {
dll_name = Some(trimmed.to_string());
}
}
}
}
}
(dll_name, calling_convention)
}
fn process_local_variables(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
_class_stack: &[String],
) {
let mut cursor = node.walk();
let var_decl = node
.children(&mut cursor)
.find(|child| child.kind() == "variable_declaration");
let Some(var_decl) = var_decl else {
return;
};
let type_node = var_decl.child_by_field_name("type");
let Some(type_node) = type_node else {
return;
};
let type_text = extract_type_string(type_node, content);
let Some(type_text) = type_text else {
return;
};
let all_type_names = extract_all_type_names_from_annotation(type_node, content);
let mut var_cursor = var_decl.walk();
for child in var_decl.children(&mut var_cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
&& let Ok(var_name) = name_node.utf8_text(content)
{
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let var_id = helper.add_variable(var_name, Some(span));
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
var_id,
type_id,
Some(TypeOfContext::Variable),
None,
Some(var_name),
);
for type_name in &all_type_names {
let ref_type_id = helper.add_type(type_name, None);
helper.add_reference_edge(var_id, ref_type_id);
}
}
}
}
fn process_field_declaration(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
class_stack: &[String],
) {
let decl_node = node
.children(&mut node.walk())
.find(|child| child.kind() == "variable_declaration");
let Some(decl_node) = decl_node else {
return;
};
let type_node = decl_node.child_by_field_name("type");
let Some(type_node) = type_node else {
return;
};
let type_text = extract_type_string(type_node, content);
let Some(type_text) = type_text else {
return;
};
let all_type_names = extract_all_type_names_from_annotation(type_node, content);
let class_name = class_stack.last().map_or("", String::as_str);
let mut var_cursor = decl_node.walk();
for child in decl_node.children(&mut var_cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
&& let Ok(field_name) = name_node.utf8_text(content)
{
let qualified_name = if class_name.is_empty() {
field_name.to_string()
} else {
format!("{class_name}.{field_name}")
};
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let is_const = has_modifier(node, content, "const");
let is_readonly = has_modifier(node, content, "readonly");
let is_static = is_const || has_modifier(node, content, "static");
let visibility = extract_field_visibility(node, content);
let field_id = if is_const || is_readonly {
helper.add_constant_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
} else {
helper.add_property_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
};
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
field_id,
type_id,
Some(TypeOfContext::Field),
None,
Some(field_name),
);
for type_name in &all_type_names {
let ref_type_id = helper.add_type(type_name, None);
helper.add_reference_edge(field_id, ref_type_id);
}
}
}
}
fn process_property_declaration(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
class_stack: &[String],
) {
let type_node = node.child_by_field_name("type");
let Some(type_node) = type_node else {
return;
};
let type_text = extract_type_string(type_node, content);
let Some(type_text) = type_text else {
return;
};
let all_type_names = extract_all_type_names_from_annotation(type_node, content);
let name_node = node.child_by_field_name("name");
let Some(name_node) = name_node else {
return;
};
let Ok(prop_name) = name_node.utf8_text(content) else {
return;
};
let class_name = class_stack.last().map_or("", String::as_str);
let qualified_name = if class_name.is_empty() {
prop_name.to_string()
} else {
format!("{class_name}.{prop_name}")
};
let span = Span::from_bytes(node.start_byte(), node.end_byte());
let is_static = has_modifier(node, content, "static");
let visibility = extract_field_visibility(node, content);
let get_only = is_get_only_property(node);
let prop_id = if get_only {
helper.add_constant_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
} else {
helper.add_property_with_static_and_visibility(
&qualified_name,
Some(span),
is_static,
Some(visibility),
)
};
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
prop_id,
type_id,
Some(TypeOfContext::Field),
None,
Some(prop_name),
);
for type_name in &all_type_names {
let ref_type_id = helper.add_type(type_name, None);
helper.add_reference_edge(prop_id, ref_type_id);
}
}
fn extract_field_visibility(node: Node, content: &[u8]) -> &'static str {
let has_protected = has_modifier(node, content, "protected");
let has_internal = has_modifier(node, content, "internal");
if has_protected && has_internal {
"protected internal"
} else if has_modifier(node, content, "public") {
"public"
} else if has_protected {
"protected"
} else if has_internal {
"internal"
} else if has_modifier(node, content, "private") {
"private"
} else {
"private"
}
}
fn is_get_only_property(node: Node) -> bool {
if node
.children(&mut node.walk())
.any(|child| child.kind() == "arrow_expression_clause")
{
return false;
}
let Some(accessor_list) = node
.children(&mut node.walk())
.find(|child| child.kind() == "accessor_list")
else {
return false;
};
let mut has_auto_get = false;
let mut has_set_like = false;
for accessor in accessor_list.children(&mut accessor_list.walk()) {
if accessor.kind() != "accessor_declaration" {
continue;
}
let mut keyword: Option<&str> = None;
let mut has_body = false;
for tok in accessor.children(&mut accessor.walk()) {
match tok.kind() {
"get" | "set" | "init" => keyword = Some(tok.kind()),
"block" | "arrow_expression_clause" => has_body = true,
_ => {}
}
}
match keyword {
Some("get") => {
if has_body {
return false;
}
has_auto_get = true;
}
Some("set" | "init") => has_set_like = true,
_ => {}
}
}
has_auto_get && !has_set_like
}
fn process_method_parameters(
node: Node,
_method_name: &str,
content: &[u8],
helper: &mut GraphBuildHelper,
) {
let Some(param_list) = node.child_by_field_name("parameter_list") else {
return;
};
let mut cursor = param_list.walk();
let mut param_index: u16 = 0;
for child in param_list.children(&mut cursor) {
if !child.is_named() {
continue;
}
{
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Ok(param_name) = name_node.utf8_text(content) else {
continue;
};
let Some(type_node) = child.child_by_field_name("type") else {
continue;
};
let Some(type_text) = extract_type_string(type_node, content) else {
continue;
};
let all_type_names = extract_all_type_names_from_annotation(type_node, content);
let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
let param_id = helper.add_variable(param_name, Some(param_span));
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
param_id,
type_id,
Some(TypeOfContext::Parameter),
Some(param_index),
Some(param_name),
);
for type_name in &all_type_names {
let ref_type_id = helper.add_type(type_name, None);
helper.add_reference_edge(param_id, ref_type_id);
}
param_index += 1;
}
}
}
fn process_method_return_type(
node: Node,
method_name: &str,
content: &[u8],
helper: &mut GraphBuildHelper,
) {
let return_type_node = node
.child_by_field_name("return_type")
.or_else(|| node.child_by_field_name("returns"))
.or_else(|| node.child_by_field_name("type"));
let Some(return_type_node) = return_type_node else {
return;
};
if let Ok(type_text) = return_type_node.utf8_text(content)
&& type_text.trim() == "void"
{
return;
}
let Some(type_text) = extract_type_string(return_type_node, content) else {
return;
};
let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
let method_id = helper.add_method(method_name, Some(method_span), false, false);
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
method_id,
type_id,
Some(TypeOfContext::Return),
Some(0), Some(method_name),
);
for type_name in &all_type_names {
let ref_type_id = helper.add_type(type_name, None);
helper.add_reference_edge(method_id, ref_type_id);
}
}
fn process_type_parameter_declarations(
decl_node: Node,
content: &[u8],
parent_qualified_name: &str,
helper: &mut GraphBuildHelper,
) {
let mut decl_cursor = decl_node.walk();
let Some(params_node) = decl_node
.children(&mut decl_cursor)
.find(|c| c.kind() == "type_parameter_list")
else {
return;
};
let mut param_ids: HashMap<String, sqry_core::graph::unified::node::NodeId> = HashMap::new();
let mut params_cursor = params_node.walk();
for param_node in params_node.children(&mut params_cursor) {
if param_node.kind() != "type_parameter" {
continue;
}
let Some(name_node) = param_node.child_by_field_name("name") else {
continue;
};
let Ok(param_name) = name_node.utf8_text(content) else {
continue;
};
let qualified_param = format!("{parent_qualified_name}.{param_name}");
let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
let param_id = helper.add_type(&qualified_param, Some(span));
param_ids.insert(param_name.to_string(), param_id);
}
let mut clause_cursor = decl_node.walk();
for clause_node in decl_node.children(&mut clause_cursor) {
if clause_node.kind() != "type_parameter_constraints_clause" {
continue;
}
emit_type_parameter_constraint_clause(clause_node, content, ¶m_ids, helper);
}
}
fn emit_type_parameter_constraint_clause(
clause_node: Node,
content: &[u8],
param_ids: &HashMap<String, sqry_core::graph::unified::node::NodeId>,
helper: &mut GraphBuildHelper,
) {
let mut cursor = clause_node.walk();
let mut named_children = clause_node
.children(&mut cursor)
.filter(tree_sitter::Node::is_named);
let Some(param_id_node) = named_children.next() else {
return;
};
if param_id_node.kind() != "identifier" {
return;
}
let Ok(param_name) = param_id_node.utf8_text(content) else {
return;
};
let Some(¶m_id) = param_ids.get(param_name) else {
return;
};
for constraint_node in named_children {
if constraint_node.kind() != "type_parameter_constraint" {
continue;
}
let Some(constraint_target_name) = extract_constraint_target_name(constraint_node, content)
else {
continue;
};
let constraint_id = helper.add_type(&constraint_target_name, None);
helper.add_typeof_edge_with_context(
param_id,
constraint_id,
Some(TypeOfContext::Constraint),
None,
None,
);
}
}
fn extract_constraint_target_name(constraint_node: Node, content: &[u8]) -> Option<String> {
let mut cursor = constraint_node.walk();
let mut had_named_constructor = false;
let mut keyword_token: Option<&'static str> = None;
for child in constraint_node.children(&mut cursor) {
match child.kind() {
"constructor_constraint" => {
had_named_constructor = true;
}
"class" if !child.is_named() => {
keyword_token = Some("class");
}
"struct" if !child.is_named() => {
keyword_token = Some("struct");
}
"unmanaged" if !child.is_named() => {
keyword_token = Some("unmanaged");
}
"notnull" if !child.is_named() => {
keyword_token = Some("notnull");
}
_ => {}
}
}
if had_named_constructor {
return Some("new()".to_string());
}
if let Some(kw) = keyword_token {
return Some(kw.to_string());
}
let type_node = constraint_node.child_by_field_name("type")?;
Some(extract_constraint_base_type_name(type_node, content))
}
fn extract_constraint_base_type_name(type_node: Node, content: &[u8]) -> String {
match type_node.kind() {
"generic_name" => {
let mut cursor = type_node.walk();
for child in type_node.children(&mut cursor) {
if matches!(child.kind(), "identifier" | "qualified_name") {
return extract_constraint_base_type_name(child, content);
}
}
type_node.utf8_text(content).unwrap_or_default().to_string()
}
"qualified_name" => {
let mut cursor = type_node.walk();
let children: Vec<_> = type_node
.children(&mut cursor)
.filter(tree_sitter::Node::is_named)
.collect();
if let Some(last) = children.last()
&& last.kind() == "generic_name"
{
let prefix_segments: Vec<String> = children
.iter()
.take(children.len() - 1)
.filter_map(|c| c.utf8_text(content).ok().map(str::to_string))
.collect();
let leaf_base = extract_constraint_base_type_name(*last, content);
if prefix_segments.is_empty() {
return leaf_base;
}
return format!("{}.{}", prefix_segments.join("."), leaf_base);
}
type_node.utf8_text(content).unwrap_or_default().to_string()
}
"nullable_type" => {
let mut cursor = type_node.walk();
for child in type_node.children(&mut cursor) {
if child.is_named() {
return extract_constraint_base_type_name(child, content);
}
}
type_node.utf8_text(content).unwrap_or_default().to_string()
}
_ => type_node.utf8_text(content).unwrap_or_default().to_string(),
}
}