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,
);
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,
);
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,
);
}
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);
}
}
"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 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 = helper.add_variable(&qualified_name, Some(span));
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 = helper.add_variable(&qualified_name, Some(span));
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 field_id = helper.add_variable(&qualified_name, Some(span));
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
field_id,
type_id,
Some(TypeOfContext::Field),
None,
Some(&qualified_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 prop_id = helper.add_variable(&qualified_name, Some(span));
let type_id = helper.add_type(&type_text, None);
helper.add_typeof_edge_with_context(
prop_id,
type_id,
Some(TypeOfContext::Field),
None,
Some(&qualified_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 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);
}
}