use sqry_core::graph::unified::build::helper::CalleeKindHint;
use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
time::{Duration, Instant},
};
use tree_sitter::{Node, Tree};
const FILE_MODULE_NAME: &str = "<file_module>";
type QualifiedNameMap = HashMap<(String, String), String>;
type FfiRegistry = HashMap<String, (String, FfiConvention)>;
type PureVirtualRegistry = HashSet<String>;
const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
const BUDGET_CHECK_INTERVAL: u32 = 1024;
fn cpp_graph_build_timeout() -> Duration {
let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
.clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
Duration::from_millis(timeout_ms)
}
struct BuildBudget {
file: PathBuf,
phase_timeout: Duration,
started_at: Instant,
checkpoints: u32,
}
impl BuildBudget {
fn new(file: &Path) -> Self {
Self {
file: file.to_path_buf(),
phase_timeout: cpp_graph_build_timeout(),
started_at: Instant::now(),
checkpoints: 0,
}
}
#[cfg(test)]
fn already_expired(file: &Path) -> Self {
Self {
file: file.to_path_buf(),
phase_timeout: Duration::from_secs(1),
started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
checkpoints: BUDGET_CHECK_INTERVAL - 1,
}
}
fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
self.checkpoints = self.checkpoints.wrapping_add(1);
if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
&& self.started_at.elapsed() > self.phase_timeout
{
return Err(GraphBuilderError::BuildTimedOut {
file: self.file.clone(),
phase,
#[allow(clippy::cast_possible_truncation)] timeout_ms: self.phase_timeout.as_millis() as u64,
});
}
Ok(())
}
}
#[allow(dead_code)] trait SpanExt {
fn from_node(node: &tree_sitter::Node) -> Self;
}
impl SpanExt for Span {
fn from_node(node: &tree_sitter::Node) -> Self {
Span::new(
Position::new(node.start_position().row, node.start_position().column),
Position::new(node.end_position().row, node.end_position().column),
)
}
}
#[derive(Debug)]
struct ASTGraph {
contexts: Vec<FunctionContext>,
context_start_index: HashMap<usize, usize>,
#[allow(dead_code)]
field_types: QualifiedNameMap,
#[allow(dead_code)]
type_map: QualifiedNameMap,
#[allow(dead_code)]
namespace_map: HashMap<std::ops::Range<usize>, String>,
}
impl ASTGraph {
fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
let namespace_map = extract_namespace_map(root, content, budget)?;
let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
contexts.sort_by_key(|ctx| ctx.span.0);
let context_start_index = contexts
.iter()
.enumerate()
.map(|(idx, ctx)| (ctx.span.0, idx))
.collect();
let (field_types, type_map) =
extract_field_and_type_info(root, content, &namespace_map, budget)?;
Ok(Self {
contexts,
context_start_index,
field_types,
type_map,
namespace_map,
})
}
fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
if insertion_point == 0 {
return None;
}
let candidate = &self.contexts[insertion_point - 1];
(byte_pos < candidate.span.1).then_some(candidate)
}
fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
self.context_start_index
.get(&start_byte)
.and_then(|idx| self.contexts.get(*idx))
}
}
#[derive(Debug, Clone)]
struct FunctionContext {
qualified_name: String,
span: (usize, usize),
is_static: bool,
#[allow(dead_code)]
is_virtual: bool,
#[allow(dead_code)]
is_inline: bool,
namespace_stack: Vec<String>,
#[allow(dead_code)] class_stack: Vec<String>,
return_type: Option<String>,
}
impl FunctionContext {
#[allow(dead_code)] fn qualified_name(&self) -> &str {
&self.qualified_name
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CppGraphBuilder;
impl CppGraphBuilder {
#[must_use]
pub fn new() -> Self {
Self
}
#[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
#[allow(clippy::trivially_copy_pass_by_ref)] &self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
budget: &mut BuildBudget,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
let mut seen_includes: HashSet<String> = HashSet::new();
let mut namespace_stack: Vec<String> = Vec::new();
let mut class_stack: Vec<String> = Vec::new();
let mut ffi_registry = FfiRegistry::new();
collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
let mut pure_virtual_registry = PureVirtualRegistry::new();
collect_pure_virtual_interfaces(
tree.root_node(),
content,
&mut pure_virtual_registry,
budget,
)?;
walk_tree_for_graph(
tree.root_node(),
content,
&ast_graph,
&mut helper,
&mut seen_includes,
&mut namespace_stack,
&mut class_stack,
&ffi_registry,
&pure_virtual_registry,
budget,
)?;
Ok(())
}
#[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
let mut attributes = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifiers" {
let mut mod_cursor = child.walk();
for modifier in child.children(&mut mod_cursor) {
if let Ok(mod_text) = modifier.utf8_text(content) {
match mod_text {
"template" => attributes.push("template".to_string()),
"sealed" => attributes.push("sealed".to_string()),
"abstract" => attributes.push("abstract".to_string()),
"open" => attributes.push("open".to_string()),
"final" => attributes.push("final".to_string()),
"inner" => attributes.push("inner".to_string()),
"value" => attributes.push("value".to_string()),
_ => {}
}
}
}
}
}
attributes
}
#[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
if let Some(spec) = node.child_by_field_name("declaration_specifiers")
&& let Ok(text) = spec.utf8_text(content)
&& text.contains("virtual")
{
return true;
}
if let Ok(text) = node.utf8_text(content)
&& text.contains("virtual")
{
return true;
}
if let Some(parent) = node.parent()
&& (parent.kind() == "field_declaration" || parent.kind() == "declaration")
&& let Ok(text) = parent.utf8_text(content)
&& text.contains("virtual")
{
return true;
}
false
}
#[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
let mut attributes = Vec::new();
for node_ref in [
node.child_by_field_name("declaration_specifiers"),
node.parent(),
]
.into_iter()
.flatten()
{
if let Ok(text) = node_ref.utf8_text(content) {
for keyword in [
"virtual",
"inline",
"constexpr",
"operator",
"override",
"static",
] {
if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
attributes.push(keyword.to_string());
}
}
}
}
if let Ok(text) = node.utf8_text(content) {
for keyword in [
"virtual",
"inline",
"constexpr",
"operator",
"override",
"static",
] {
if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
attributes.push(keyword.to_string());
}
}
}
attributes
}
}
impl GraphBuilder for CppGraphBuilder {
fn language(&self) -> Language {
Language::Cpp
}
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut budget = BuildBudget::new(file);
self.build_graph_with_budget(tree, content, file, staging, &mut budget)
}
}
fn extract_namespace_map(
node: Node,
content: &[u8],
budget: &mut BuildBudget,
) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
let mut map = HashMap::new();
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.expect("Failed to load recursion limits");
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.expect("Invalid file_ops_depth configuration");
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.expect("Failed to create recursion guard");
extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
match e {
timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
other => GraphBuilderError::ParseError {
span: span_from_node(node),
reason: format!("C++ namespace extraction failed: {other}"),
},
}
})?;
Ok(map)
}
fn extract_namespaces_recursive(
node: Node,
content: &[u8],
current_ns: &str,
map: &mut HashMap<std::ops::Range<usize>, String>,
guard: &mut sqry_core::query::security::RecursionGuard,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:extract_namespace_map")?;
guard.enter().map_err(|e| GraphBuilderError::ParseError {
span: span_from_node(node),
reason: format!("C++ namespace extraction hit recursion limit: {e}"),
})?;
if node.kind() == "namespace_definition" {
let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
extract_identifier(name_node, content)
} else {
String::from("anonymous")
};
let new_ns = if current_ns.is_empty() {
format!("{ns_name}::")
} else {
format!("{current_ns}{ns_name}::")
};
if let Some(body) = node.child_by_field_name("body") {
let range = body.start_byte()..body.end_byte();
map.insert(range, new_ns.clone());
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
}
}
} else {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
}
}
guard.exit();
Ok(())
}
fn extract_identifier(node: Node, content: &[u8]) -> String {
node.utf8_text(content).unwrap_or("").to_string()
}
fn find_namespace_for_offset(
byte_offset: usize,
namespace_map: &HashMap<std::ops::Range<usize>, String>,
) -> String {
let mut matching_ranges: Vec<_> = namespace_map
.iter()
.filter(|(range, _)| range.contains(&byte_offset))
.collect();
matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
matching_ranges
.first()
.map_or("", |(_, ns)| ns.as_str())
.to_string()
}
fn extract_cpp_contexts(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
budget: &mut BuildBudget,
) -> GraphResult<Vec<FunctionContext>> {
let mut contexts = Vec::new();
let mut class_stack = Vec::new();
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.expect("Failed to load recursion limits");
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.expect("Invalid file_ops_depth configuration");
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.expect("Failed to create recursion guard");
extract_contexts_recursive(
node,
content,
namespace_map,
&mut contexts,
&mut class_stack,
&mut guard,
budget,
)
.map_err(|e| match e {
timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
other => GraphBuilderError::ParseError {
span: span_from_node(node),
reason: format!("C++ context extraction failed: {other}"),
},
})?;
Ok(contexts)
}
fn extract_contexts_recursive(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
contexts: &mut Vec<FunctionContext>,
class_stack: &mut Vec<String>,
guard: &mut sqry_core::query::security::RecursionGuard,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:extract_contexts")?;
guard.enter().map_err(|e| GraphBuilderError::ParseError {
span: span_from_node(node),
reason: format!("C++ context extraction hit recursion limit: {e}"),
})?;
match node.kind() {
"class_specifier" | "struct_specifier" => {
if let Some(name_node) = node.child_by_field_name("name") {
let class_name = extract_identifier(name_node, content);
class_stack.push(class_name);
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
extract_contexts_recursive(
child,
content,
namespace_map,
contexts,
class_stack,
guard,
budget,
)?;
}
}
class_stack.pop();
}
}
"function_definition" => {
if let Some(declarator) = node.child_by_field_name("declarator") {
let (func_name, class_prefix) =
extract_function_name_with_class(declarator, content);
let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
let namespace_stack: Vec<String> = if namespace.is_empty() {
Vec::new()
} else {
namespace
.trim_end_matches("::")
.split("::")
.map(String::from)
.collect()
};
let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
class_stack.clone()
} else if let Some(ref prefix) = class_prefix {
vec![prefix.clone()]
} else {
Vec::new()
};
let qualified_name =
build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
let is_static = is_static_function(node, content);
let is_virtual = is_virtual_function(node, content);
let is_inline = is_inline_function(node, content);
let return_type = node
.child_by_field_name("type")
.and_then(|type_node| type_node.utf8_text(content).ok())
.map(std::string::ToString::to_string);
let span = (node.start_byte(), node.end_byte());
contexts.push(FunctionContext {
qualified_name,
span,
is_static,
is_virtual,
is_inline,
namespace_stack,
class_stack: effective_class_stack,
return_type,
});
}
}
_ => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_contexts_recursive(
child,
content,
namespace_map,
contexts,
class_stack,
guard,
budget,
)?;
}
}
}
guard.exit();
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());
for class_name in class_stack {
parts.push(class_name.clone());
}
parts.push(name.to_string());
parts.join("::")
}
fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
match declarator.kind() {
"function_declarator" => {
if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
extract_function_name_with_class(declarator_inner, content)
} else {
(extract_identifier(declarator, content), None)
}
}
"qualified_identifier" => {
let name = if let Some(name_node) = declarator.child_by_field_name("name") {
extract_identifier(name_node, content)
} else {
extract_identifier(declarator, content)
};
let class_prefix = declarator
.child_by_field_name("scope")
.map(|scope_node| extract_identifier(scope_node, content));
(name, class_prefix)
}
"field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
(extract_identifier(declarator, content), None)
}
_ => {
(extract_identifier(declarator, content), None)
}
}
}
#[allow(dead_code)]
fn extract_function_name(declarator: Node, content: &[u8]) -> String {
extract_function_name_with_class(declarator, content).0
}
fn is_static_function(node: Node, content: &[u8]) -> bool {
has_specifier(node, "static", content)
}
fn is_virtual_function(node: Node, content: &[u8]) -> bool {
has_specifier(node, "virtual", content)
}
fn is_inline_function(node: Node, content: &[u8]) -> bool {
has_specifier(node, "inline", content)
}
fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "storage_class_specifier"
|| child.kind() == "type_qualifier"
|| child.kind() == "virtual"
|| child.kind() == "inline")
&& let Ok(text) = child.utf8_text(content)
&& text == specifier
{
return true;
}
}
false
}
fn extract_field_and_type_info(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
budget: &mut BuildBudget,
) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
let mut field_types = HashMap::new();
let mut type_map = HashMap::new();
let mut class_stack = Vec::new();
extract_fields_recursive(
node,
content,
namespace_map,
&mut field_types,
&mut type_map,
&mut class_stack,
budget,
)?;
Ok((field_types, type_map))
}
fn extract_fields_recursive(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
field_types: &mut HashMap<(String, String), String>,
type_map: &mut HashMap<(String, String), String>,
class_stack: &mut Vec<String>,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:extract_fields")?;
match node.kind() {
"class_specifier" | "struct_specifier" => {
if let Some(name_node) = node.child_by_field_name("name") {
let class_name = extract_identifier(name_node, content);
let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
let class_fqn = if class_stack.is_empty() {
if namespace.is_empty() {
class_name.clone()
} else {
format!("{}::{}", namespace.trim_end_matches("::"), class_name)
}
} else {
format!("{}::{}", class_stack.last().unwrap(), class_name)
};
class_stack.push(class_fqn.clone());
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_fields_recursive(
child,
content,
namespace_map,
field_types,
type_map,
class_stack,
budget,
)?;
}
class_stack.pop();
}
}
"field_declaration" => {
if let Some(class_fqn) = class_stack.last() {
extract_field_declaration(
node,
content,
class_fqn,
namespace_map,
field_types,
type_map,
);
}
}
"using_directive" => {
extract_using_directive(node, content, namespace_map, type_map);
}
"using_declaration" => {
extract_using_declaration(node, content, namespace_map, type_map);
}
_ => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_fields_recursive(
child,
content,
namespace_map,
field_types,
type_map,
class_stack,
budget,
)?;
}
}
}
Ok(())
}
fn extract_field_declaration(
node: Node,
content: &[u8],
class_fqn: &str,
namespace_map: &HashMap<std::ops::Range<usize>, String>,
field_types: &mut HashMap<(String, String), String>,
type_map: &HashMap<(String, String), String>,
) {
let mut field_type = None;
let mut field_names = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
field_type = Some(extract_type_name(child, content));
}
"field_identifier" => {
field_names.push(extract_identifier(child, content));
}
"field_declarator"
| "init_declarator"
| "pointer_declarator"
| "reference_declarator"
| "array_declarator" => {
if let Some(name) = extract_field_name(child, content) {
field_names.push(name);
}
}
_ => {}
}
}
if let Some(ftype) = field_type {
let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
for fname in field_names {
field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
}
}
}
fn extract_type_name(type_node: Node, content: &[u8]) -> String {
match type_node.kind() {
"type_identifier" | "primitive_type" => extract_identifier(type_node, content),
"qualified_identifier" => {
extract_identifier(type_node, content)
}
"template_type" => {
if let Some(name) = type_node.child_by_field_name("name") {
extract_identifier(name, content)
} else {
extract_identifier(type_node, content)
}
}
_ => {
extract_identifier(type_node, content)
}
}
}
fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
match declarator.kind() {
"field_declarator" => {
if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
extract_field_name(declarator_inner, content)
} else {
Some(extract_identifier(declarator, content))
}
}
"field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
"pointer_declarator" | "reference_declarator" | "array_declarator" => {
if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
extract_field_name(declarator_inner, content)
} else {
None
}
}
"init_declarator" => {
if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
extract_field_name(declarator_inner, content)
} else {
None
}
}
_ => None,
}
}
fn resolve_type_to_fqn(
type_name: &str,
namespace: &str,
type_map: &HashMap<(String, String), String>,
) -> String {
if type_name.contains("::") {
return type_name.to_string();
}
let namespace_key = namespace.trim_end_matches("::").to_string();
if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
return fqn.clone();
}
if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
return fqn.clone();
}
type_name.to_string()
}
fn extract_using_directive(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
_type_map: &mut HashMap<(String, String), String>,
) {
let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
if let Some(name_node) = node.child_by_field_name("name") {
let _using_ns = extract_identifier(name_node, content);
}
}
fn extract_using_declaration(
node: Node,
content: &[u8],
namespace_map: &HashMap<std::ops::Range<usize>, String>,
type_map: &mut HashMap<(String, String), String>,
) {
let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
let namespace_key = namespace.trim_end_matches("::").to_string();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
let fqn = extract_identifier(child, content);
if let Some(simple_name) = fqn.split("::").last() {
type_map.insert((namespace_key, simple_name.to_string()), fqn);
}
break;
}
}
}
fn resolve_callee_name(
callee_name: &str,
caller_ctx: &FunctionContext,
_ast_graph: &ASTGraph,
) -> String {
if callee_name.starts_with("::") {
return callee_name.trim_start_matches("::").to_string();
}
if callee_name.contains("::") {
if !caller_ctx.namespace_stack.is_empty() {
let namespace_prefix = caller_ctx.namespace_stack.join("::");
return format!("{namespace_prefix}::{callee_name}");
}
return callee_name.to_string();
}
let mut parts = Vec::new();
if !caller_ctx.namespace_stack.is_empty() {
parts.extend(caller_ctx.namespace_stack.iter().cloned());
}
parts.push(callee_name.to_string());
parts.join("::")
}
fn strip_type_qualifiers(type_text: &str) -> String {
let mut result = type_text.trim().to_string();
result = result.replace("const ", "");
result = result.replace("volatile ", "");
result = result.replace("mutable ", "");
result = result.replace("constexpr ", "");
result = result.replace(" const", "");
result = result.replace(" volatile", "");
result = result.replace(" mutable", "");
result = result.replace(" constexpr", "");
result = result.replace(['*', '&'], "");
result = result.trim().to_string();
if let Some(last_part) = result.split("::").last() {
result = last_part.to_string();
}
if let Some(open_bracket) = result.find('<') {
result = result[..open_bracket].to_string();
}
result.trim().to_string()
}
#[allow(clippy::unnecessary_wraps)]
fn process_field_declaration(
node: Node,
content: &[u8],
class_qualified_name: &str,
visibility: &str,
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let mut field_type_text = None;
let mut field_names = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"type_identifier" | "primitive_type" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"qualified_identifier" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"template_type" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"sized_type_specifier" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"type_qualifier" => {
if field_type_text.is_none()
&& let Ok(text) = child.utf8_text(content)
{
field_type_text = Some(text.to_string());
}
}
"auto" => {
field_type_text = Some("auto".to_string());
}
"decltype" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
if let Ok(text) = child.utf8_text(content) {
field_type_text = Some(text.to_string());
}
}
"field_identifier" => {
if let Ok(name) = child.utf8_text(content) {
field_names.push(name.trim().to_string());
}
}
"field_declarator"
| "pointer_declarator"
| "reference_declarator"
| "init_declarator" => {
if let Some(name) = extract_field_name(child, content) {
field_names.push(name);
}
}
_ => {}
}
}
if let Some(type_text) = field_type_text {
let base_type = strip_type_qualifiers(&type_text);
for field_name in field_names {
let field_qualified = format!("{class_qualified_name}::{field_name}");
let span = span_from_node(node);
let var_id = helper.add_node_with_visibility(
&field_qualified,
Some(span),
sqry_core::graph::unified::node::NodeKind::Variable,
Some(visibility),
);
let type_id = helper.add_type(&base_type, None);
helper.add_typeof_edge(var_id, type_id);
helper.add_reference_edge(var_id, type_id);
}
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn process_global_variable_declaration(
node: Node,
content: &[u8],
namespace_stack: &[String],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
if node.kind() != "declaration" {
return Ok(());
}
let mut cursor_check = node.walk();
for child in node.children(&mut cursor_check) {
if child.kind() == "function_declarator" {
return Ok(());
}
}
let mut type_text = None;
let mut var_names = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
if let Ok(text) = child.utf8_text(content) {
type_text = Some(text.to_string());
}
}
"init_declarator" => {
if let Some(declarator) = child.child_by_field_name("declarator")
&& let Some(name) = extract_declarator_name(declarator, content)
{
var_names.push(name);
}
}
"pointer_declarator" | "reference_declarator" => {
if let Some(name) = extract_declarator_name(child, content) {
var_names.push(name);
}
}
"identifier" => {
if let Ok(name) = child.utf8_text(content) {
var_names.push(name.to_string());
}
}
_ => {}
}
}
if let Some(type_text) = type_text {
let base_type = strip_type_qualifiers(&type_text);
for var_name in var_names {
let qualified = if namespace_stack.is_empty() {
var_name.clone()
} else {
format!("{}::{}", namespace_stack.join("::"), var_name)
};
let span = span_from_node(node);
let var_id = helper.add_node_with_visibility(
&qualified,
Some(span),
sqry_core::graph::unified::node::NodeKind::Variable,
Some("public"),
);
let type_id = helper.add_type(&base_type, None);
helper.add_typeof_edge(var_id, type_id);
helper.add_reference_edge(var_id, type_id);
}
}
Ok(())
}
fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
match node.kind() {
"identifier" => {
if let Ok(name) = node.utf8_text(content) {
Some(name.to_string())
} else {
None
}
}
"pointer_declarator" | "reference_declarator" | "array_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
extract_declarator_name(inner, content)
} else {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier"
&& let Ok(name) = child.utf8_text(content)
{
return Some(name.to_string());
}
}
None
}
}
"init_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
extract_declarator_name(inner, content)
} else {
None
}
}
"field_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
extract_declarator_name(inner, content)
} else {
if let Ok(name) = node.utf8_text(content) {
Some(name.to_string())
} else {
None
}
}
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn walk_class_body(
body_node: Node,
content: &[u8],
class_qualified_name: &str,
is_struct: bool,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
seen_includes: &mut HashSet<String>,
namespace_stack: &mut Vec<String>,
class_stack: &mut Vec<String>,
ffi_registry: &FfiRegistry,
pure_virtual_registry: &PureVirtualRegistry,
budget: &mut BuildBudget,
) -> GraphResult<()> {
let mut current_visibility = if is_struct { "public" } else { "private" };
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
budget.checkpoint("cpp:walk_class_body")?;
match child.kind() {
"access_specifier" => {
if let Ok(text) = child.utf8_text(content) {
let spec = text.trim().trim_end_matches(':').trim();
current_visibility = spec;
}
}
"field_declaration" => {
process_field_declaration(
child,
content,
class_qualified_name,
current_visibility,
helper,
)?;
}
"function_definition" => {
if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
let span = span_from_node(child);
helper.add_method_with_signature(
&context.qualified_name,
Some(span),
false, context.is_static,
Some(current_visibility),
context.return_type.as_deref(),
);
}
walk_tree_for_graph(
child,
content,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
_ => {
walk_tree_for_graph(
child,
content,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
seen_includes: &mut HashSet<String>,
namespace_stack: &mut Vec<String>,
class_stack: &mut Vec<String>,
ffi_registry: &FfiRegistry,
pure_virtual_registry: &PureVirtualRegistry,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:walk_tree_for_graph")?;
match node.kind() {
"preproc_include" => {
build_import_edge(node, content, helper, seen_includes)?;
}
"linkage_specification" => {
build_ffi_block_for_staging(node, content, helper, namespace_stack);
}
"namespace_definition" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(ns_name) = name_node.utf8_text(content)
{
namespace_stack.push(ns_name.trim().to_string());
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_tree_for_graph(
child,
content,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
namespace_stack.pop();
return Ok(());
}
}
"class_specifier" | "struct_specifier" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(class_name) = name_node.utf8_text(content)
{
let class_name = class_name.trim();
let span = span_from_node(node);
let is_struct = node.kind() == "struct_specifier";
let qualified_class =
build_qualified_name(namespace_stack, class_stack, class_name);
let visibility = "public";
let class_id = if is_struct {
helper.add_struct_with_visibility(
&qualified_class,
Some(span),
Some(visibility),
)
} else {
helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
};
build_inheritance_and_implements_edges(
node,
content,
&qualified_class,
class_id,
helper,
namespace_stack,
pure_virtual_registry,
)?;
if class_stack.is_empty() {
let module_id = helper.add_module(FILE_MODULE_NAME, None);
helper.add_export_edge(module_id, class_id);
}
class_stack.push(class_name.to_string());
if let Some(body) = node.child_by_field_name("body") {
walk_class_body(
body,
content,
&qualified_class,
is_struct,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
class_stack.pop();
return Ok(());
}
}
"enum_specifier" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(enum_name) = name_node.utf8_text(content)
{
let enum_name = enum_name.trim();
let span = span_from_node(node);
let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
let enum_id = helper.add_enum(&qualified_enum, Some(span));
if class_stack.is_empty() {
let module_id = helper.add_module(FILE_MODULE_NAME, None);
helper.add_export_edge(module_id, enum_id);
}
}
}
"function_definition" => {
if !class_stack.is_empty() {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_tree_for_graph(
child,
content,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
return Ok(());
}
if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
let span = span_from_node(node);
if context.class_stack.is_empty() {
let visibility = if context.is_static {
"private"
} else {
"public"
};
let fn_id = helper.add_function_with_signature(
&context.qualified_name,
Some(span),
false, false, Some(visibility),
context.return_type.as_deref(),
);
if !context.is_static {
let module_id = helper.add_module(FILE_MODULE_NAME, None);
helper.add_export_edge(module_id, fn_id);
}
} else {
helper.add_method_with_signature(
&context.qualified_name,
Some(span),
false, context.is_static,
Some("public"), context.return_type.as_deref(),
);
}
}
}
"call_expression" => {
if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
build_call_for_staging(ast_graph, node, content)
{
let caller_function_id =
helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
let is_unqualified = !callee_qname.contains("::");
if is_unqualified {
if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
let ffi_target_id =
helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
} else {
let target_function_id =
helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
helper.add_call_edge_full_with_span(
caller_function_id,
target_function_id,
argument_count,
false,
vec![span],
);
}
} else {
let target_function_id =
helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
helper.add_call_edge_full_with_span(
caller_function_id,
target_function_id,
argument_count,
false,
vec![span],
);
}
}
}
"declaration" => {
if class_stack.is_empty() {
process_global_variable_declaration(node, content, namespace_stack, helper)?;
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_tree_for_graph(
child,
content,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
}
Ok(())
}
fn build_call_for_staging(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
) -> GraphResult<Option<(String, String, usize, Span)>> {
let call_context = ast_graph.find_enclosing(call_node.start_byte());
let caller_qualified_name = if let Some(ctx) = call_context {
ctx.qualified_name.clone()
} else {
return Ok(None);
};
let Some(function_node) = call_node.child_by_field_name("function") else {
return Ok(None);
};
let callee_text = function_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(call_node),
reason: "failed to read call expression".to_string(),
})?
.trim();
if callee_text.is_empty() {
return Ok(None);
}
let target_qualified_name = if let Some(ctx) = call_context {
resolve_callee_name(callee_text, ctx, ast_graph)
} else {
callee_text.to_string()
};
let span = span_from_node(call_node);
let argument_count = count_arguments(call_node);
Ok(Some((
caller_qualified_name,
target_qualified_name,
argument_count,
span,
)))
}
fn build_import_edge(
include_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
seen_includes: &mut HashSet<String>,
) -> GraphResult<()> {
let path_node = include_node.child_by_field_name("path").or_else(|| {
let mut cursor = include_node.walk();
include_node.children(&mut cursor).find(|child| {
matches!(
child.kind(),
"system_lib_string" | "string_literal" | "string_content"
)
})
});
let Some(path_node) = path_node else {
return Ok(());
};
let include_path = path_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(include_node),
reason: "failed to read include path".to_string(),
})?
.trim();
if include_path.is_empty() {
return Ok(());
}
let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
let cleaned_path = if is_system_include {
include_path.trim_start_matches('<').trim_end_matches('>')
} else {
include_path.trim_start_matches('"').trim_end_matches('"')
};
if cleaned_path.is_empty() {
return Ok(());
}
if !seen_includes.insert(cleaned_path.to_string()) {
return Ok(()); }
let file_module_id = helper.add_module("<file>", None);
let span = span_from_node(include_node);
let import_id = helper.add_import(cleaned_path, Some(span));
helper.add_import_edge(file_module_id, import_id);
Ok(())
}
fn collect_ffi_declarations(
node: Node<'_>,
content: &[u8],
ffi_registry: &mut FfiRegistry,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:collect_ffi_declarations")?;
if node.kind() == "linkage_specification" {
let abi = extract_ffi_abi(node, content);
let convention = abi_to_convention(&abi);
if let Some(body_node) = node.child_by_field_name("body") {
collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ffi_declarations(child, content, ffi_registry, budget)?;
}
Ok(())
}
fn collect_ffi_from_body(
body_node: Node<'_>,
content: &[u8],
abi: &str,
convention: FfiConvention,
ffi_registry: &mut FfiRegistry,
) {
match body_node.kind() {
"declaration_list" => {
let mut cursor = body_node.walk();
for decl in body_node.children(&mut cursor) {
if decl.kind() == "declaration"
&& let Some(fn_name) = extract_ffi_function_name(decl, content)
{
let qualified = format!("extern::{abi}::{fn_name}");
ffi_registry.insert(fn_name, (qualified, convention));
}
}
}
"declaration" => {
if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
let qualified = format!("extern::{abi}::{fn_name}");
ffi_registry.insert(fn_name, (qualified, convention));
}
}
_ => {}
}
}
fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
return extract_function_name_from_declarator(declarator_node, content);
}
None
}
fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
match node.kind() {
"function_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
return extract_function_name_from_declarator(inner, content);
}
}
"identifier" => {
if let Ok(name) = node.utf8_text(content) {
let name = name.trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
"pointer_declarator" | "reference_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
return extract_function_name_from_declarator(inner, content);
}
}
"parenthesized_declarator" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(name) = extract_function_name_from_declarator(child, content) {
return Some(name);
}
}
}
_ => {}
}
None
}
fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
if let Some(value_node) = node.child_by_field_name("value")
&& value_node.kind() == "string_literal"
{
let mut cursor = value_node.walk();
for child in value_node.children(&mut cursor) {
if child.kind() == "string_content"
&& let Ok(text) = child.utf8_text(content)
{
let trimmed = text.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
}
}
"C".to_string()
}
fn abi_to_convention(abi: &str) -> FfiConvention {
match abi.to_lowercase().as_str() {
"system" => FfiConvention::System,
"stdcall" => FfiConvention::Stdcall,
"fastcall" => FfiConvention::Fastcall,
"cdecl" => FfiConvention::Cdecl,
_ => FfiConvention::C, }
}
fn build_ffi_block_for_staging(
node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
namespace_stack: &[String],
) {
let abi = extract_ffi_abi(node, content);
if let Some(body_node) = node.child_by_field_name("body") {
build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
}
}
fn build_ffi_from_body(
body_node: Node<'_>,
content: &[u8],
abi: &str,
helper: &mut GraphBuildHelper,
namespace_stack: &[String],
) {
match body_node.kind() {
"declaration_list" => {
let mut cursor = body_node.walk();
for decl in body_node.children(&mut cursor) {
if decl.kind() == "declaration"
&& let Some(fn_name) = extract_ffi_function_name(decl, content)
{
let span = span_from_node(decl);
let qualified = if namespace_stack.is_empty() {
format!("extern::{abi}::{fn_name}")
} else {
format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
};
helper.add_function(
&qualified,
Some(span),
false, true, );
}
}
}
"declaration" => {
if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
let span = span_from_node(body_node);
let qualified = if namespace_stack.is_empty() {
format!("extern::{abi}::{fn_name}")
} else {
format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
};
helper.add_function(&qualified, Some(span), false, true);
}
}
_ => {}
}
}
fn collect_pure_virtual_interfaces(
node: Node<'_>,
content: &[u8],
registry: &mut PureVirtualRegistry,
budget: &mut BuildBudget,
) -> GraphResult<()> {
budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
if matches!(node.kind(), "class_specifier" | "struct_specifier")
&& let Some(name_node) = node.child_by_field_name("name")
&& let Ok(class_name) = name_node.utf8_text(content)
{
let class_name = class_name.trim();
if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
registry.insert(class_name.to_string());
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_pure_virtual_interfaces(child, content, registry, budget)?;
}
Ok(())
}
fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
if let Some(body) = class_node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
return true;
}
}
}
false
}
fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
let mut has_virtual = false;
let mut has_pure_specifier = false;
let mut cursor = decl_node.walk();
for child in decl_node.children(&mut cursor) {
match child.kind() {
"virtual" => {
has_virtual = true;
}
"number_literal" => {
if let Ok(text) = child.utf8_text(content)
&& text.trim() == "0"
{
has_pure_specifier = true;
}
}
_ => {}
}
}
has_virtual && has_pure_specifier
}
fn build_inheritance_and_implements_edges(
class_node: Node<'_>,
content: &[u8],
_qualified_class_name: &str,
child_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
namespace_stack: &[String],
pure_virtual_registry: &PureVirtualRegistry,
) -> GraphResult<()> {
let mut cursor = class_node.walk();
let base_clause = class_node
.children(&mut cursor)
.find(|child| child.kind() == "base_class_clause");
let Some(base_clause) = base_clause else {
return Ok(()); };
let mut clause_cursor = base_clause.walk();
for child in base_clause.children(&mut clause_cursor) {
match child.kind() {
"type_identifier" => {
let base_name = child
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(child),
reason: "failed to read base class name".to_string(),
})?
.trim();
if !base_name.is_empty() {
let qualified_base = if namespace_stack.is_empty() {
base_name.to_string()
} else {
format!("{}::{}", namespace_stack.join("::"), base_name)
};
if pure_virtual_registry.contains(base_name) {
let interface_id = helper.add_interface(&qualified_base, None);
helper.add_implements_edge(child_id, interface_id);
} else {
let parent_id = helper.add_class(&qualified_base, None);
helper.add_inherits_edge(child_id, parent_id);
}
}
}
"qualified_identifier" => {
let base_name = child
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(child),
reason: "failed to read base class name".to_string(),
})?
.trim();
if !base_name.is_empty() {
let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
if pure_virtual_registry.contains(simple_name) {
let interface_id = helper.add_interface(base_name, None);
helper.add_implements_edge(child_id, interface_id);
} else {
let parent_id = helper.add_class(base_name, None);
helper.add_inherits_edge(child_id, parent_id);
}
}
}
"template_type" => {
if let Some(template_name_node) = child.child_by_field_name("name")
&& let Ok(base_name) = template_name_node.utf8_text(content)
{
let base_name = base_name.trim();
if !base_name.is_empty() {
let qualified_base =
if base_name.contains("::") || namespace_stack.is_empty() {
base_name.to_string()
} else {
format!("{}::{}", namespace_stack.join("::"), base_name)
};
if pure_virtual_registry.contains(base_name) {
let interface_id = helper.add_interface(&qualified_base, None);
helper.add_implements_edge(child_id, interface_id);
} else {
let parent_id = helper.add_class(&qualified_base, None);
helper.add_inherits_edge(child_id, parent_id);
}
}
}
}
_ => {
}
}
}
Ok(())
}
fn span_from_node(node: Node<'_>) -> Span {
let start = node.start_position();
let end = node.end_position();
Span::new(
sqry_core::graph::node::Position::new(start.row, start.column),
sqry_core::graph::node::Position::new(end.row, end.column),
)
}
fn count_arguments(node: Node<'_>) -> usize {
node.child_by_field_name("arguments").map_or(0, |args| {
let mut count = 0;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !matches!(child.kind(), "(" | ")" | ",") {
count += 1;
}
}
count
})
}
#[cfg(test)]
mod tests {
use super::*;
use sqry_core::graph::unified::build::test_helpers::{
assert_has_node, assert_has_node_with_kind, collect_call_edges,
};
use sqry_core::graph::unified::node::NodeKind;
use tree_sitter::Parser;
fn parse_cpp(source: &str) -> Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("Failed to set Cpp language");
parser
.parse(source.as_bytes(), None)
.expect("Failed to parse Cpp source")
}
fn test_budget() -> BuildBudget {
BuildBudget::new(Path::new("test.cpp"))
}
fn extract_namespace_map_for_test(
tree: &Tree,
source: &str,
) -> HashMap<std::ops::Range<usize>, String> {
let mut budget = test_budget();
extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
.expect("namespace extraction should succeed in tests")
}
fn extract_cpp_contexts_for_test(
tree: &Tree,
source: &str,
namespace_map: &HashMap<std::ops::Range<usize>, String>,
) -> Vec<FunctionContext> {
let mut budget = test_budget();
extract_cpp_contexts(
tree.root_node(),
source.as_bytes(),
namespace_map,
&mut budget,
)
.expect("context extraction should succeed in tests")
}
fn extract_field_and_type_info_for_test(
tree: &Tree,
source: &str,
namespace_map: &HashMap<std::ops::Range<usize>, String>,
) -> (QualifiedNameMap, QualifiedNameMap) {
let mut budget = test_budget();
extract_field_and_type_info(
tree.root_node(),
source.as_bytes(),
namespace_map,
&mut budget,
)
.expect("field/type extraction should succeed in tests")
}
#[test]
fn test_build_graph_times_out_with_expired_budget() {
let source = r"
namespace demo {
class Service {
public:
void process() {}
};
}
";
let tree = parse_cpp(source);
let builder = CppGraphBuilder::new();
let mut staging = StagingGraph::new();
let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
let err = builder
.build_graph_with_budget(
&tree,
source.as_bytes(),
Path::new("timeout.cpp"),
&mut staging,
&mut budget,
)
.expect_err("expired budget should force timeout");
match err {
GraphBuilderError::BuildTimedOut {
file,
phase,
timeout_ms,
} => {
assert_eq!(file, PathBuf::from("timeout.cpp"));
assert_eq!(phase, "cpp:extract_namespace_map");
assert_eq!(timeout_ms, 1_000);
}
other => panic!("expected BuildTimedOut, got {other:?}"),
}
}
#[test]
fn test_extract_class() {
let source = "class User { }";
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node_with_kind(&staging, "User", NodeKind::Class);
}
#[test]
fn test_extract_template_class() {
let source = r"
template <typename T>
class Person {
public:
T name;
T age;
};
";
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
}
#[test]
fn test_extract_function() {
let source = r#"
#include <cstdio>
void hello() {
std::printf("Hello");
}
"#;
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
}
#[test]
fn test_extract_virtual_function() {
let source = r"
class Service {
public:
virtual void fetchData() {}
};
";
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node(&staging, "fetchData");
}
#[test]
fn test_extract_call_edge() {
let source = r"
void greet() {}
int main() {
greet();
return 0;
}
";
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node(&staging, "main");
assert_has_node(&staging, "greet");
let calls = collect_call_edges(&staging);
assert!(!calls.is_empty());
}
#[test]
fn test_extract_member_call_edge() {
let source = r"
class Service {
public:
void helper() {}
};
int main() {
Service svc;
svc.helper();
return 0;
}
";
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
let result = builder.build_graph(
&tree,
source.as_bytes(),
Path::new("member.cpp"),
&mut staging,
);
assert!(result.is_ok());
assert_has_node(&staging, "main");
assert_has_node(&staging, "helper");
let calls = collect_call_edges(&staging);
assert!(!calls.is_empty());
}
#[test]
fn test_extract_namespace_map_simple() {
let source = r"
namespace demo {
void func() {}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
assert_eq!(namespace_map.len(), 1);
let (_, ns_prefix) = namespace_map.iter().next().unwrap();
assert_eq!(ns_prefix, "demo::");
}
#[test]
fn test_extract_namespace_map_nested() {
let source = r"
namespace outer {
namespace inner {
void func() {}
}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
assert!(namespace_map.len() >= 2);
let ns_values: Vec<&String> = namespace_map.values().collect();
assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
}
#[test]
fn test_extract_namespace_map_multiple() {
let source = r"
namespace first {
void func1() {}
}
namespace second {
void func2() {}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
assert_eq!(namespace_map.len(), 2);
let ns_values: Vec<&String> = namespace_map.values().collect();
assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
}
#[test]
fn test_find_namespace_for_offset() {
let source = r"
namespace demo {
void func() {}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let func_offset = source.find("func").unwrap();
let ns = find_namespace_for_offset(func_offset, &namespace_map);
assert_eq!(ns, "demo::");
let ns = find_namespace_for_offset(0, &namespace_map);
assert_eq!(ns, "");
}
#[test]
fn test_extract_cpp_contexts_free_function() {
let source = r"
void helper() {}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "helper");
assert!(!contexts[0].is_static);
assert!(!contexts[0].is_virtual);
}
#[test]
fn test_extract_cpp_contexts_namespace_function() {
let source = r"
namespace demo {
void helper() {}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "demo::helper");
assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
}
#[test]
fn test_extract_cpp_contexts_class_method() {
let source = r"
class Service {
public:
void process() {}
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "Service::process");
assert_eq!(contexts[0].class_stack, vec!["Service"]);
}
#[test]
fn test_extract_cpp_contexts_namespace_and_class() {
let source = r"
namespace demo {
class Service {
public:
void process() {}
};
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "demo::Service::process");
assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
assert_eq!(contexts[0].class_stack, vec!["Service"]);
}
#[test]
fn test_extract_cpp_contexts_static_method() {
let source = r"
class Repository {
public:
static void save() {}
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "Repository::save");
assert!(contexts[0].is_static);
}
#[test]
fn test_extract_cpp_contexts_virtual_method() {
let source = r"
class Base {
public:
virtual void render() {}
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "Base::render");
assert!(contexts[0].is_virtual);
}
#[test]
fn test_extract_cpp_contexts_inline_function() {
let source = r"
inline void helper() {}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "helper");
assert!(contexts[0].is_inline);
}
#[test]
fn test_extract_cpp_contexts_out_of_line_definition() {
let source = r"
namespace demo {
class Service {
public:
int process(int v);
};
inline int Service::process(int v) {
return v;
}
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].qualified_name, "demo::Service::process");
assert!(contexts[0].is_inline);
}
#[test]
fn test_extract_field_types_simple() {
let source = r"
class Service {
public:
Repository repo;
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 1);
assert_eq!(
field_types.get(&("Service".to_string(), "repo".to_string())),
Some(&"Repository".to_string())
);
}
#[test]
fn test_extract_field_types_namespace() {
let source = r"
namespace demo {
class Service {
public:
Repository repo;
};
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 1);
assert_eq!(
field_types.get(&("demo::Service".to_string(), "repo".to_string())),
Some(&"Repository".to_string())
);
}
#[test]
fn test_extract_field_types_no_collision() {
let source = r"
class ServiceA {
public:
Repository repo;
};
class ServiceB {
public:
Repository repo;
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 2);
assert_eq!(
field_types.get(&("ServiceA".to_string(), "repo".to_string())),
Some(&"Repository".to_string())
);
assert_eq!(
field_types.get(&("ServiceB".to_string(), "repo".to_string())),
Some(&"Repository".to_string())
);
}
#[test]
fn test_extract_using_declaration() {
let source = r"
using std::vector;
class Service {
public:
vector data;
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 1);
assert_eq!(
field_types.get(&("Service".to_string(), "data".to_string())),
Some(&"std::vector".to_string()),
"Field type should resolve 'vector' to 'std::vector' via using declaration"
);
assert_eq!(
type_map.get(&(String::new(), "vector".to_string())),
Some(&"std::vector".to_string()),
"Using declaration should map 'vector' to 'std::vector' in type_map"
);
}
#[test]
fn test_extract_field_types_pointer() {
let source = r"
class Service {
public:
Repository* repo;
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 1);
assert_eq!(
field_types.get(&("Service".to_string(), "repo".to_string())),
Some(&"Repository".to_string())
);
}
#[test]
fn test_extract_field_types_multiple_declarators() {
let source = r"
class Service {
public:
Repository repo_a, repo_b, repo_c;
};
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert_eq!(field_types.len(), 3);
assert_eq!(
field_types.get(&("Service".to_string(), "repo_a".to_string())),
Some(&"Repository".to_string())
);
assert_eq!(
field_types.get(&("Service".to_string(), "repo_b".to_string())),
Some(&"Repository".to_string())
);
assert_eq!(
field_types.get(&("Service".to_string(), "repo_c".to_string())),
Some(&"Repository".to_string())
);
}
#[test]
fn test_extract_field_types_nested_struct_with_parent_field() {
let source = r"
namespace demo {
struct Outer {
int outer_field;
struct Inner {
int inner_field;
};
Inner nested_instance;
};
}
";
let tree = parse_cpp(source);
let namespace_map = extract_namespace_map_for_test(&tree, source);
let (field_types, _type_map) =
extract_field_and_type_info_for_test(&tree, source, &namespace_map);
assert!(
field_types.len() >= 2,
"Expected at least outer_field and nested_instance"
);
assert_eq!(
field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
Some(&"int".to_string())
);
assert_eq!(
field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
Some(&"Inner".to_string())
);
if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
{
assert_eq!(
field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
Some(&"int".to_string()),
"Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
);
}
}
}