use crate::declarations::{cpp_file_using_namespaces, cpp_member_fq, node_text};
use crate::graph_support::CppSource;
use crate::imports::{IncludeTargetIndex, include_paths, resolve_include_targets_with_index};
use crate::reconcile::{ReconciledIdentity, VisibleClass, reconcile_out_of_line_member_identity};
use brokk_bifrost_core::analyzer::fq_name::{SegmentKind, segment_interner};
use brokk_bifrost_core::analyzer::model::{CallableLinkage, Range};
use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
use brokk_bifrost_core::hash::HashMap;
use brokk_bifrost_core::path_utils::rel_path_string;
use brokk_bifrost_core::profiling;
use std::collections::BTreeSet;
use std::sync::Arc;
use tree_sitter::{Node, Parser, Tree};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppCallableUnitRole {
DeclarationOnly,
Definition,
Both,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppOccurrenceRole {
DeclarationOnly,
Definition,
Both,
Unknown,
}
impl CppOccurrenceRole {
pub fn api_label(self) -> Option<&'static str> {
match self {
Self::DeclarationOnly => Some("declaration"),
Self::Definition => Some("definition"),
Self::Both | Self::Unknown => None,
}
}
}
pub struct CppOccurrenceClassifier {
tree: Tree,
}
impl CppOccurrenceClassifier {
pub fn new(source: &str) -> Option<Self> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.ok()?;
parser.parse(source, None).map(|tree| Self { tree })
}
pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
cpp_occurrence_role_for_range(self.tree.root_node(), candidate, range)
}
}
pub fn cpp_callable_unit_role(
index: &dyn CodeUnitIndex,
callable: &CodeUnit,
) -> CppCallableUnitRole {
if !callable.is_callable() {
return CppCallableUnitRole::Unknown;
}
let mut declaration = false;
let mut definition = false;
for metadata in index.signature_metadata(callable) {
if metadata.is_declaration_only() {
declaration = true;
} else {
definition = true;
}
}
match (declaration, definition) {
(true, false) => CppCallableUnitRole::DeclarationOnly,
(false, true) => CppCallableUnitRole::Definition,
(true, true) => CppCallableUnitRole::Both,
(false, false) => CppCallableUnitRole::Unknown,
}
}
pub fn cpp_indexed_callable_linkage(
index: &dyn CodeUnitIndex,
callable: &CodeUnit,
) -> Option<CallableLinkage> {
let mut external = false;
for metadata in index.signature_metadata(callable) {
match metadata.callable_linkage() {
Some(CallableLinkage::Internal) => return Some(CallableLinkage::Internal),
Some(CallableLinkage::External) => external = true,
None => {}
}
}
external.then_some(CallableLinkage::External)
}
pub fn cpp_callable_definitions_share_identity_evidence(
index: &dyn CodeUnitIndex,
left: &CodeUnit,
right: &CodeUnit,
header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
) -> bool {
left.source() == right.source()
|| (left.fq_name() == right.fq_name()
&& left.signature() == right.signature()
&& matches!(
cpp_indexed_callable_linkage(index, left),
Some(CallableLinkage::External)
)
&& matches!(
cpp_indexed_callable_linkage(index, right),
Some(CallableLinkage::External)
)
&& header_body_related(left.source(), right.source()))
}
pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
let mut current = Some(node);
while let Some(candidate) = current {
let Some(parent) = candidate.parent() else {
return false;
};
if parent.kind() == "for_range_loop" {
return parent
.child_by_field_name("declarator")
.is_some_and(|declarator| {
cpp_range_for_declarator_contains_name(declarator, node)
});
}
current = Some(parent);
}
false
}
pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
cpp_is_declared_constructor_or_destructor_name(node)
|| cpp_is_recovered_constructor_or_destructor_name(node, source)
}
fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
let mut name = node;
if let Some(parent) = name.parent()
&& parent.kind() == "destructor_name"
{
name = parent;
}
while let Some(parent) = name.parent() {
if parent.kind() != "qualified_identifier"
|| parent.child_by_field_name("name") != Some(name)
{
break;
}
name = parent;
}
let Some(declarator) = name.parent() else {
return false;
};
if declarator.kind() != "function_declarator"
|| declarator.child_by_field_name("declarator") != Some(name)
{
return false;
}
let Some(owner) = declarator.parent() else {
return false;
};
matches!(owner.kind(), "declaration" | "function_definition")
&& owner.child_by_field_name("declarator") == Some(declarator)
&& owner.child_by_field_name("type").is_none()
}
fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
if node.kind() != "identifier" {
return false;
}
let Some(call) = node.parent() else {
return false;
};
if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
return false;
}
let mut current = call.parent();
while let Some(ancestor) = current {
if ancestor.kind() == "function_definition" {
return ancestor
.child_by_field_name("declarator")
.is_some_and(|declarator| declarator.kind() == "identifier")
&& cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
}
current = ancestor.parent();
}
false
}
fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
let header_end = definition
.child_by_field_name("body")
.map_or_else(|| definition.end_byte(), |body| body.start_byte());
let mut stack = vec![definition];
while let Some(node) = stack.pop() {
if node.start_byte() >= header_end {
continue;
}
if matches!(
node.kind(),
"identifier" | "type_identifier" | "namespace_identifier"
) && node_text(node, source) == name
{
return true;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
stack.push(child);
}
}
false
}
fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
let mut pending = vec![declarator];
while let Some(candidate) = pending.pop() {
match candidate.kind() {
"identifier" | "field_identifier" => {
if cpp_same_node(candidate, target) {
return true;
}
}
"structured_binding_declarator" => {
let mut cursor = candidate.walk();
if candidate
.named_children(&mut cursor)
.any(|name| cpp_same_node(name, target))
{
return true;
}
}
"pointer_declarator"
| "reference_declarator"
| "array_declarator"
| "attributed_declarator"
| "parenthesized_declarator"
| "function_declarator"
| "init_declarator" => {
if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
pending.push(inner);
}
}
_ => {}
}
}
false
}
fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
node.child_by_field_name("declarator").or_else(|| {
let mut cursor = node.walk();
node.named_children(&mut cursor).find(|child| {
matches!(
child.kind(),
"identifier"
| "field_identifier"
| "structured_binding_declarator"
| "pointer_declarator"
| "reference_declarator"
| "array_declarator"
| "attributed_declarator"
| "parenthesized_declarator"
| "function_declarator"
| "init_declarator"
)
})
})
}
fn cpp_same_node(left: Node<'_>, right: Node<'_>) -> bool {
left.id() == right.id()
&& left.start_byte() == right.start_byte()
&& left.end_byte() == right.end_byte()
}
pub fn cpp_header_body_files_are_related(
left: &ProjectFile,
right: &ProjectFile,
implementation_imports: &[String],
include_targets: &IncludeTargetIndex,
) -> bool {
let (header, implementation) = if cpp_source_path_is_header(left) {
(left, right)
} else if cpp_source_path_is_header(right) {
(right, left)
} else {
return false;
};
if cpp_source_path_is_header(implementation) {
return false;
}
implementation_imports
.iter()
.flat_map(|import| include_paths(std::slice::from_ref(import)))
.any(|include| {
let targets =
resolve_include_targets_with_index(implementation, &include, include_targets);
targets.len() == 1 && targets.first() == Some(header)
})
}
pub fn cpp_header_body_implementation_file<'a>(
left: &'a ProjectFile,
right: &'a ProjectFile,
) -> Option<&'a ProjectFile> {
let implementation = if cpp_source_path_is_header(left) {
right
} else if cpp_source_path_is_header(right) {
left
} else {
return None;
};
(!cpp_source_path_is_header(implementation)).then_some(implementation)
}
pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
let path = rel_path_string(source).to_ascii_lowercase();
matches!(path.rsplit('.').next(), Some("h" | "hh" | "hpp" | "hxx"))
}
pub fn cpp_occurrence_role_for_range(
root: Node<'_>,
candidate: &CodeUnit,
range: &Range,
) -> CppOccurrenceRole {
if !candidate.is_callable() && !candidate.is_class() {
return CppOccurrenceRole::Both;
}
let Some(node) = cpp_declaration_node_for_range(root, range) else {
return CppOccurrenceRole::Unknown;
};
if candidate.is_callable() {
return if subtree_contains(node, |descendant| {
descendant.kind() == "function_definition"
&& descendant.child_by_field_name("body").is_some()
}) {
CppOccurrenceRole::Definition
} else {
CppOccurrenceRole::DeclarationOnly
};
}
if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
return CppOccurrenceRole::Definition;
}
if !subtree_contains(node, |descendant| {
matches!(
descendant.kind(),
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
)
}) {
return CppOccurrenceRole::Both;
}
if subtree_contains(node, |descendant| {
matches!(
descendant.kind(),
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
) && descendant.child_by_field_name("body").is_some()
}) {
CppOccurrenceRole::Definition
} else {
CppOccurrenceRole::DeclarationOnly
}
}
fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
node_for_exact_range(root, range).or_else(|| {
root.descendant_for_byte_range(range.start_byte, range.end_byte)
.and_then(|mut node| {
while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
node = node.parent()?;
}
Some(node)
})
})
}
#[derive(Default)]
pub struct CppReconciledDefinitionIndex {
pub rekeyed: Vec<CodeUnit>,
pub provisional_of: HashMap<CodeUnit, CodeUnit>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CppReconcileGroupKey {
pub member_identifier: String,
pub owner_terminal: Option<String>,
}
pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
let interner = segment_interner();
let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
let (member_identifier, _) = interner.resolve(query_fq.last()?);
if member_identifier.is_empty() {
return None;
}
let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
text.rsplit_once('$')
.map_or(text, |(_, tail)| tail)
.to_string()
});
Some(CppReconcileGroupKey {
member_identifier: member_identifier.to_string(),
owner_terminal,
})
}
pub struct CppReconcileCandidates {
by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
all: Vec<CodeUnit>,
}
impl CppReconcileCandidates {
fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
match &key.owner_terminal {
Some(owner_terminal) => self
.by_owner_terminal
.get(owner_terminal)
.map_or(&[][..], Vec::as_slice),
None => &self.all,
}
}
pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
self.all.iter()
}
pub fn bucketed_len(&self) -> usize {
self.by_owner_terminal.values().map(Vec::len).sum()
}
}
pub fn cpp_reconcile_candidates(
cpp: &dyn CppSource,
member_identifier: &str,
keep_going: &dyn Fn() -> bool,
) -> Option<CppReconcileCandidates> {
let candidates: BTreeSet<CodeUnit> = {
let _lookup =
profiling::scope_with(|| format!("cpp.reconcile.lookup[{member_identifier}]"));
cpp.lookup_candidates_by_identifier(member_identifier)
};
profiling::note_with(|| {
format!(
"cpp.reconcile.candidates[{member_identifier}] n={}",
candidates.len()
)
});
let interner = segment_interner();
let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
let mut all = Vec::new();
for (index, unit) in candidates.into_iter().enumerate() {
if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
return None;
}
if !unit.is_callable() {
continue;
}
let owner_terminal = unit
.fq()
.segments()
.iter()
.filter_map(|&segment| {
let (text, kind) = interner.resolve(segment);
matches!(
kind,
SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
)
.then_some(text)
})
.last();
if let Some(owner_terminal) = owner_terminal {
by_owner_terminal
.entry(owner_terminal.to_string())
.or_default()
.push(unit.clone());
}
all.push(unit);
}
Some(CppReconcileCandidates {
by_owner_terminal,
all,
})
}
const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
pub fn cpp_reconcile_group(
cpp: &dyn CppSource,
key: &CppReconcileGroupKey,
candidates: &CppReconcileCandidates,
keep_going: &dyn Fn() -> bool,
on_candidate: &dyn Fn(),
) -> Option<HashMap<String, Arc<CppReconciledDefinitionIndex>>> {
let _scope = profiling::scope_with(|| {
format!(
"cpp.reconciled.build[{}#{}]",
key.member_identifier,
key.owner_terminal.as_deref().unwrap_or("*")
)
});
let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
for unit in candidates.for_group(key) {
if !keep_going() {
return None;
}
on_candidate();
let _candidate =
profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
let role = {
let _role = profiling::scope("cpp.reconcile.role");
cpp_callable_unit_role(cpp, unit)
};
if !matches!(
role,
CppCallableUnitRole::Definition | CppCallableUnitRole::Both
) {
continue;
}
let Some(reconciled) = cpp_reconcile_definition_identity(cpp, unit, &mut using_by_file)
else {
continue;
};
let canonical_fq = reconciled.fq_name();
if unit.fq_name() == canonical_fq {
continue;
}
let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
let fq = cpp_member_fq(&reconciled.package, &short_name);
let rekeyed = CodeUnit::with_signature_and_fq(
unit.source().clone(),
unit.kind(),
reconciled.package,
short_name,
unit.signature().map(str::to_string),
unit.is_synthetic(),
fq,
);
let index = groups.entry(canonical_fq).or_default();
index.rekeyed.push(rekeyed.clone());
index.provisional_of.insert(rekeyed, unit.clone());
}
Some(
groups
.into_iter()
.map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
.collect(),
)
}
fn cpp_reconcile_definition_identity(
cpp: &dyn CppSource,
unit: &CodeUnit,
using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
) -> Option<ReconciledIdentity> {
let interner = segment_interner();
let mut owner_segments: Vec<&str> = Vec::new();
let mut member: Option<&str> = None;
for &segment in unit.fq().segments() {
let (text, kind) = interner.resolve(segment);
match kind {
SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
if member.is_some() {
return None;
}
if !text.is_empty() {
owner_segments.push(text);
}
}
SegmentKind::Member => member = Some(text),
_ => return None,
}
}
let member = member?;
if owner_segments.len() < 2 {
return None;
}
let using = using_by_file
.entry(unit.source().clone())
.or_insert_with(|| {
Arc::new(
cpp.file_source(unit.source())
.map(|source| cpp_file_using_namespaces(&source))
.unwrap_or_default(),
)
})
.clone();
let mut namespace_candidates: Vec<&str> = vec![""];
namespace_candidates.extend(using.iter().map(String::as_str));
let visible = {
let _visible = profiling::scope_with(|| {
format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
});
cpp.visible_type_units(unit.source())
};
let class_table: Vec<VisibleClass> = visible
.iter()
.filter(|candidate| candidate.is_class())
.map(|candidate| VisibleClass {
package: candidate.package_name(),
nested_short_name: candidate.short_name(),
})
.collect();
reconcile_out_of_line_member_identity(
&owner_segments,
member,
&namespace_candidates,
&class_table,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_cpp(source: &str) -> Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("cpp language");
parser.parse(source, None).expect("cpp tree")
}
fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
let end = start + text.len();
assert_eq!(&source[start..end], text, "the probe must name the token");
let node = tree
.root_node()
.named_descendant_for_byte_range(start, end)
.expect("a node spans the probed range");
assert_eq!(
(node.start_byte(), node.end_byte()),
(start, end),
"the probed range must be exactly one node: {}",
node.to_sexp()
);
cpp_is_constructor_or_destructor_declarator_name(node, source)
}
#[test]
fn declared_constructor_and_destructor_declarator_names_are_not_references() {
let source = concat!(
"class Foo {\n",
"public:\n",
" Foo();\n",
" Foo(const Foo&);\n",
" ~Foo();\n",
" void m();\n",
"};\n",
"Foo::Foo() {}\n",
"Foo::~Foo() {}\n",
"void Foo::m() {}\n",
);
let tree = parse_cpp(source);
for (label, start, text) in [
(
"constructor declaration",
source.find("Foo();").expect("ctor"),
"Foo",
),
(
"copy constructor declaration",
source.find("Foo(const Foo&);").expect("copy ctor"),
"Foo",
),
(
"destructor name",
source.find("~Foo();").expect("dtor"),
"~Foo",
),
(
"identifier inside the destructor name",
source.find("~Foo();").expect("dtor") + "~".len(),
"Foo",
),
(
"out-of-line constructor definition name",
source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
"Foo",
),
(
"out-of-line destructor definition name",
source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
"~Foo",
),
] {
assert!(
is_declarator_name(&tree, source, start, text),
"the {label} at byte {start} is a declaration occurrence"
);
}
for (label, start, text) in [
(
"class name",
source.find("class Foo {").expect("class") + "class ".len(),
"Foo",
),
(
"parameter type",
source.find("const Foo&").expect("parameter type") + "const ".len(),
"Foo",
),
(
"owning scope of an out-of-line constructor",
source.find("Foo::Foo() {}").expect("out-of-line ctor"),
"Foo",
),
(
"owning scope of an out-of-line destructor",
source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
"Foo",
),
(
"out-of-line method name",
source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
"m",
),
] {
assert!(
!is_declarator_name(&tree, source, start, text),
"the {label} at byte {start} stays a reference"
);
}
}
#[test]
fn constructor_call_sites_stay_references() {
let source = concat!(
"struct B { B(int); };\n",
"struct D : B {\n",
" D(int x) : B(x), base_(x) {}\n",
" int base_;\n",
"};\n",
"void g() {\n",
" D* p = new D(1);\n",
" D x(2);\n",
" D(3);\n",
" g();\n",
"}\n",
);
let tree = parse_cpp(source);
let inline_declarator = source.find("D(int x)").expect("inline constructor");
assert!(
is_declarator_name(&tree, source, inline_declarator, "D"),
"an inline constructor definition name is still a declarator"
);
for (label, start, text) in [
(
"base member initializer",
source.find(": B(x)").expect("base initializer") + ": ".len(),
"B",
),
(
"field member initializer",
source.find("base_(x) {}").expect("field initializer"),
"base_",
),
(
"new expression type",
source.find("new D(1)").expect("new expression") + "new ".len(),
"D",
),
(
"direct initialization type",
source.find("D x(2)").expect("direct initialization"),
"D",
),
(
"temporary construction statement",
source.find("D(3)").expect("temporary"),
"D",
),
(
"recursive call in a real body",
source.find("g();").expect("recursive call"),
"g",
),
] {
assert!(
!is_declarator_name(&tree, source, start, text),
"the {label} at byte {start} is a reference"
);
}
}
#[test]
fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
let source = concat!(
"class SAMPLE_EXPORT Properties {\n",
" public:\n",
" Properties();\n",
" DISALLOW_COPY_AND_ASSIGN(Properties);\n",
" int size() const;\n",
" int total() { return size(); }\n",
"};\n",
);
let tree = parse_cpp(source);
let recovered = source.find("Properties();").expect("recovered constructor");
assert!(
is_declarator_name(&tree, source, recovered, "Properties"),
"a constructor declaration the parse read as a call is still a declarator"
);
for (label, start, text) in [
(
"class name in the recovered header",
source
.find("class SAMPLE_EXPORT Properties")
.expect("class")
+ "class SAMPLE_EXPORT ".len(),
"Properties",
),
(
"macro invocation in the recovered body",
source
.find("DISALLOW_COPY_AND_ASSIGN(Properties);")
.expect("macro invocation"),
"DISALLOW_COPY_AND_ASSIGN",
),
(
"call inside a method body the recovery kept",
source.find("return size();").expect("member call") + "return ".len(),
"size",
),
] {
assert!(
!is_declarator_name(&tree, source, start, text),
"the {label} at byte {start} stays a reference"
);
}
}
}