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, clippy::too_many_lines)]
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 is_static_kw = false;
let mut is_const = false;
let mut is_constexpr = false;
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 let Ok(text) = child.utf8_text(content) {
let trimmed = text.trim();
if trimmed == "const" {
is_const = true;
} else if trimmed == "constexpr" {
is_constexpr = true;
}
if field_type_text.is_none() {
field_type_text = Some(text.to_string());
}
}
}
"storage_class_specifier" => {
if let Ok(text) = child.utf8_text(content) {
let trimmed = text.trim();
if trimmed == "static" {
is_static_kw = true;
} else if trimmed == "constexpr" {
is_constexpr = true;
}
}
}
"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);
let is_constant = is_const || is_constexpr;
for field_name in field_names {
let field_qualified = format!("{class_qualified_name}.{field_name}");
let span = span_from_node(node);
let field_id = if is_constant {
helper.add_constant_with_name_static_and_visibility(
&field_name,
&field_qualified,
Some(span),
is_static_kw,
Some(visibility),
)
} else {
helper.add_property_with_name_static_and_visibility(
&field_name,
&field_qualified,
Some(span),
is_static_kw,
Some(visibility),
)
};
let type_id = helper.add_type(&base_type, None);
helper.add_typeof_edge_with_context(
field_id,
type_id,
Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
None,
Some(&field_name),
);
helper.add_reference_edge(field_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, clippy::too_many_lines)]
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" => {
let mut handled_nested = false;
let mut inner_cursor = child.walk();
for inner in child.children(&mut inner_cursor) {
let kind = inner.kind();
if !matches!(
kind,
"class_specifier" | "struct_specifier" | "union_specifier"
) {
continue;
}
let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
if let Some(name_node) = inner.child_by_field_name("name") {
if let Ok(inner_name) = name_node.utf8_text(content) {
let inner_name = inner_name.trim();
let nested_qualified = format!("{class_qualified_name}::{inner_name}");
if let Some(body) = inner.child_by_field_name("body") {
walk_class_body(
body,
content,
&nested_qualified,
is_struct_or_union,
ast_graph,
helper,
seen_includes,
namespace_stack,
class_stack,
ffi_registry,
pure_virtual_registry,
budget,
)?;
handled_nested = true;
}
}
} else if let Some(body) = inner.child_by_field_name("body") {
let mut anon_cursor = body.walk();
for anon_child in body.children(&mut anon_cursor) {
if anon_child.kind() == "field_declaration" {
process_field_declaration(
anon_child,
content,
class_qualified_name,
current_visibility,
helper,
)?;
}
}
handled_nested = true;
}
}
let _ = handled_nested;
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, assert_has_node_with_kind_exact,
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'"
);
}
}
use sqry_core::graph::unified::build::staging::StagingOp;
use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
fn cpp_find_added_node<'a>(
staging: &'a StagingGraph,
canonical_name: &str,
) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
staging.operations().iter().find_map(|op| {
if let StagingOp::AddNode { entry, .. } = op
&& staging.resolve_node_canonical_name(entry) == Some(canonical_name)
{
Some(entry)
} else {
None
}
})
}
fn cpp_find_added_node_id(
staging: &StagingGraph,
canonical_name: &str,
kind: NodeKind,
) -> Option<sqry_core::graph::unified::NodeId> {
staging.operations().iter().find_map(|op| match op {
StagingOp::AddNode {
entry,
expected_id: Some(id),
} if entry.kind == kind
&& staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
{
Some(*id)
}
_ => None,
})
}
fn build_cpp(source: &str) -> StagingGraph {
let tree = parse_cpp(source);
let mut staging = StagingGraph::new();
let builder = CppGraphBuilder::new();
builder
.build_graph(
&tree,
source.as_bytes(),
Path::new("test.cpp"),
&mut staging,
)
.expect("build_graph must succeed for the test fixture");
staging
}
#[test]
fn test_struct_field_emits_property_with_field_context() {
let source = "struct Point { int x; int y; };";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
let entry =
cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
assert!(!entry.is_static, "instance field is_static must be false");
let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
assert_eq!(
vis,
Some("public"),
"struct default visibility must be 'public'"
);
assert!(entry.end_line > 0, "field end_line must be set (got 0)");
assert!(
entry.end_line > entry.start_line
|| (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
"field span must be non-empty: [{}:{}..{}:{}]",
entry.start_line,
entry.start_column,
entry.end_line,
entry.end_column,
);
let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
.expect("Point.x Property NodeId");
let edge = staging.operations().iter().find_map(|op| {
if let StagingOp::AddEdge {
source: src,
kind: EdgeKind::TypeOf { context, name, .. },
..
} = op
&& *src == x_id
{
Some((*context, *name))
} else {
None
}
});
let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
assert_eq!(
ctx,
Some(TypeOfContext::Field),
"TypeOf edge context must be Field"
);
let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
assert_eq!(
resolved_name,
Some("x"),
"TypeOf edge name must be the bare field name 'x'"
);
let stale_variable = staging.nodes().any(|n| {
n.entry.kind == NodeKind::Variable
&& matches!(
staging.resolve_node_name(n.entry),
Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
)
});
assert!(
!stale_variable,
"Point fields must not be emitted as NodeKind::Variable"
);
}
#[test]
fn test_class_field_default_visibility_is_private() {
let source = "class Foo { int hidden; };";
let staging = build_cpp(source);
let entry = cpp_find_added_node(&staging, "Foo.hidden")
.expect("Foo.hidden should be staged as a node");
assert_eq!(entry.kind, NodeKind::Property);
let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
assert_eq!(
vis,
Some("private"),
"class default visibility must be 'private'"
);
}
#[test]
fn test_class_field_respects_explicit_access_specifier() {
let source = "class Foo { public: int public_field; protected: int prot_field; };";
let staging = build_cpp(source);
let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
.expect("Foo.public_field should be staged");
assert_eq!(
staging.resolve_local_string(pub_entry.visibility.expect("vis")),
Some("public")
);
let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
.expect("Foo.prot_field should be staged");
assert_eq!(
staging.resolve_local_string(prot_entry.visibility.expect("vis")),
Some("protected")
);
}
#[test]
fn test_const_field_emits_constant() {
let source = "class Foo { const int kMax = 0; };";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
assert_eq!(entry.kind, NodeKind::Constant);
assert!(
!entry.is_static,
"const (non-static) field is_static must be false; only `static` keyword sets is_static"
);
}
#[test]
fn test_constexpr_field_emits_constant() {
let source = "class Foo { constexpr static int kAnswer = 42; };";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
assert_eq!(entry.kind, NodeKind::Constant);
assert!(
entry.is_static,
"static constexpr member must have is_static = true"
);
}
#[test]
fn test_static_field_sets_is_static_true() {
let source = "class Foo { static int counter; };";
let staging = build_cpp(source);
let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
assert_eq!(entry.kind, NodeKind::Property);
assert!(entry.is_static, "static keyword must set is_static = true");
}
#[test]
fn test_bitfield_emits_property() {
let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
}
#[test]
fn test_anonymous_union_member_fields_emit_property() {
let source = r"
class Variant {
public:
int tag;
union {
int as_int;
float as_float;
};
};
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
let as_int = cpp_find_added_node(&staging, "Variant.as_int")
.expect("Variant.as_int should be staged");
let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
assert_eq!(
vis,
Some("public"),
"anonymous-union members must inherit OUTER access (`public:` here)"
);
let bogus = staging.nodes().any(|n| {
staging
.resolve_node_name(n.entry)
.is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
});
assert!(
!bogus,
"anonymous union must not produce a synthetic qualifier"
);
let stale_variable = staging.nodes().any(|n| {
n.entry.kind == NodeKind::Variable
&& matches!(
staging.resolve_node_name(n.entry),
Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
)
});
assert!(
!stale_variable,
"anonymous-union members + outer fields must not stay as Variable"
);
}
#[test]
fn test_templated_class_field_emits_property() {
let source = r"
template<class T>
struct Box {
T value;
};
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
assert_eq!(entry.kind, NodeKind::Property);
assert!(!entry.is_static);
}
#[test]
fn test_outer_class_field_with_nested_class_present() {
let source = r"
class Outer {
public:
int outer_value;
class Inner {
public:
int x;
};
};
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
let legacy_hits: Vec<_> = staging
.nodes()
.filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
.collect();
assert!(
legacy_hits.is_empty(),
"legacy `Outer::outer_value` lookup must return 0 hits"
);
for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
let hits: Vec<_> = staging
.nodes()
.filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
.collect();
assert!(
hits.is_empty(),
"nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
);
}
}
#[test]
fn test_outer_class_with_nested_struct_emits_inner_field() {
let source = r"
class Outer {
private:
struct Inner {
int y;
};
};
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
.expect("Outer::Inner.y should be staged");
let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
assert_eq!(
vis,
Some("public"),
"nested struct field default visibility must be 'public' \
regardless of OUTER access state"
);
}
#[test]
fn test_legacy_double_colon_field_lookup_returns_zero() {
let source = r"
class Foo {
public:
int bar;
static int baz;
const int qux = 0;
};
struct Quux {
int corge;
};
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
let hits: Vec<_> = staging
.nodes()
.filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
.collect();
assert!(
hits.is_empty(),
"legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
hits.len(),
hits.iter()
.map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
.collect::<Vec<_>>()
);
}
}
#[test]
fn test_namespaced_class_field_qualified_name() {
let source = r"
namespace demo {
class Service {
public:
int counter;
};
}
";
let staging = build_cpp(source);
assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
}
}