use crate::declarations::{
CppRecoveredExportClassIndex, cpp_member_fq, extract_function_declarator, node_text,
recovered_callable_body_at, recovered_class_body_at,
};
use crate::graph::CppGraphSource;
use crate::graph::resolver::{
VisibilityIndex, cpp_include_closure_reaches, cpp_name_component_nodes, declarator_name_node,
lexical_component_tiers, qualified_owner_components,
};
use crate::graph_support::CppSource;
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, SignatureMetadata};
use brokk_bifrost_core::analyzer::query_token::QueryToken;
use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
pub use brokk_bifrost_core::analyzer::usages::common::same_node as cpp_same_node;
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::sync::Arc;
use tree_sitter::Node;
use tree_sitter::{Parser, Tree};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppCallableUnitRole {
DeclarationOnly,
Definition,
Both,
Unknown,
}
pub use brokk_bifrost_core::analyzer::cpp_facts::CppOccurrenceRole;
pub struct CppOccurrenceClassifier {
tree: Tree,
source: String,
recovered_export_classes: CppRecoveredExportClassIndex,
}
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| {
let recovered_export_classes =
CppRecoveredExportClassIndex::build(tree.root_node(), source);
Self {
tree,
source: source.to_owned(),
recovered_export_classes,
}
})
}
pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
cpp_occurrence_role_for_range(
&self.recovered_export_classes,
self.tree.root_node(),
&self.source,
candidate,
range,
)
}
}
pub fn cpp_callable_unit_role(
index: &dyn CodeUnitIndex,
callable: &CodeUnit,
) -> CppCallableUnitRole {
cpp_callable_unit_role_from_metadata(callable, index.signature_metadata(callable))
}
fn cpp_callable_unit_role_from_metadata(
callable: &CodeUnit,
metadata: impl IntoIterator<Item = SignatureMetadata>,
) -> CppCallableUnitRole {
if !callable.is_callable() {
return CppCallableUnitRole::Unknown;
}
let mut declaration = false;
let mut definition = false;
for metadata in metadata {
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> {
cpp_callable_linkage_from_metadata(index.signature_metadata(callable))
}
pub fn cpp_callable_linkage_from_metadata(
metadata: impl IntoIterator<Item = SignatureMetadata>,
) -> Option<CallableLinkage> {
let mut external = false;
for metadata in metadata {
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.fq_name() == right.fq_name()
&& left.signature() == right.signature()
&& (left.source() == right.source()
|| (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_callable_definitions_share_identity_evidence_with_visibility(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
left: &CodeUnit,
right: &CodeUnit,
header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
) -> bool {
left.fq_name() == right.fq_name()
&& visibility.same_logical_callable(analyzer, left, right)
&& (left.source() == right.source()
|| (matches!(
visibility.callable_linkage(left),
Some(CallableLinkage::External)
) && matches!(
visibility.callable_linkage(right),
Some(CallableLinkage::External)
) && (header_body_related(left.source(), right.source())
|| visibility.member_declaration_bridges_to_body(
analyzer,
left,
right,
&header_body_related,
)
|| visibility.member_declaration_bridges_to_body(
analyzer,
right,
left,
&header_body_related,
))))
}
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_conversion_operator_target_type(mut node: Node<'_>) -> bool {
while let Some(parent) = node.parent() {
if parent.kind() == "operator_cast" {
return true;
}
if matches!(
parent.kind(),
"function_declarator" | "declaration" | "function_definition" | "translation_unit"
) {
return false;
}
node = parent;
}
false
}
pub fn cpp_is_recovered_macro_character_token_type(node: Node<'_>) -> bool {
if node.kind() != "type_identifier" {
return false;
}
let Some(parameter) = node.parent() else {
return false;
};
if parameter.kind() != "parameter_declaration"
|| parameter.child_by_field_name("type") != Some(node)
|| parameter.child_by_field_name("declarator").is_some()
|| parameter
.parent()
.is_none_or(|parent| parent.kind() != "parameter_list")
{
return false;
}
parameter
.prev_named_sibling()
.is_some_and(cpp_is_recovered_character_quote)
&& parameter
.next_named_sibling()
.is_some_and(cpp_is_recovered_character_quote)
}
fn cpp_is_recovered_character_quote(node: Node<'_>) -> bool {
node.is_error()
&& node.child_count() == 1
&& node
.child(0)
.is_some_and(|quote| !quote.is_named() && quote.kind() == "'")
}
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"
)
})
})
}
pub fn cpp_header_body_files_are_related(
source: &dyn CppSource,
token: QueryToken<'_>,
left: &ProjectFile,
right: &ProjectFile,
) -> bool {
match (
cpp_source_path_is_header(left),
cpp_source_path_is_header(right),
) {
(true, false) => cpp_include_closure_reaches(source, token, right, left),
(false, true) => cpp_include_closure_reaches(source, token, left, right),
(true, true) => {
cpp_include_closure_reaches(source, token, left, right)
|| cpp_include_closure_reaches(source, token, right, left)
}
(false, false) => false,
}
}
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" | "hin" | "hh" | "hpp" | "hxx")
)
}
pub(crate) fn cpp_occurrence_role_for_node(
node: Node<'_>,
source: &str,
candidate: &CodeUnit,
) -> CppOccurrenceRole {
if !candidate.is_callable() && !candidate.is_class() {
return CppOccurrenceRole::Both;
}
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 cpp_node_declares_alias(node, source, candidate) {
return CppOccurrenceRole::Both;
}
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
}
}
pub fn cpp_occurrence_role_for_range(
recovered_export_classes: &CppRecoveredExportClassIndex,
root: Node<'_>,
source: &str,
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()
&& subtree_contains(node, |descendant| {
descendant.kind() == "function_definition"
&& descendant.child_by_field_name("body").is_some()
})
{
return CppOccurrenceRole::Definition;
}
if candidate.is_callable()
&& !matches!(
node.kind(),
"declaration" | "field_declaration" | "function_definition"
)
&& let Some(has_body) = recovered_callable_body_at(source, range)
{
return if has_body {
CppOccurrenceRole::Definition
} else {
CppOccurrenceRole::DeclarationOnly
};
}
if candidate.is_class()
&& node.kind() == "function_definition"
&& node.child_by_field_name("body").is_some()
{
return CppOccurrenceRole::Definition;
}
if candidate.is_class()
&& let Some(has_body) = recovered_class_body_at(
recovered_export_classes,
root,
source,
candidate.identifier(),
range,
)
{
return if has_body {
CppOccurrenceRole::Definition
} else {
CppOccurrenceRole::DeclarationOnly
};
}
cpp_occurrence_role_for_node(node, source, candidate)
}
fn cpp_node_declares_alias(node: Node<'_>, source: &str, candidate: &CodeUnit) -> bool {
match node.kind() {
"alias_declaration" => {
node.child_by_field_name("name")
.and_then(|name| name.utf8_text(source.as_bytes()).ok())
== Some(candidate.identifier())
}
"type_definition" => {
let mut cursor = node.walk();
node.children_by_field_name("declarator", &mut cursor)
.any(|declarator| {
crate::declarations::extract_typedef_declarator_name(declarator, source)
.as_deref()
== Some(candidate.identifier())
})
}
_ => false,
}
}
pub fn cpp_range_is_pure_virtual_declaration(root: Node<'_>, source: &str, range: &Range) -> bool {
let Some(node) = cpp_declaration_node_for_range(root, range) else {
return false;
};
let mut current = Some(node);
while let Some(node) = current {
match node.kind() {
"field_declaration" => {
return node
.child_by_field_name("default_value")
.is_some_and(|value| {
value.kind() == "number_literal" && node_text(value, source) == "0"
})
&& node
.child_by_field_name("declarator")
.and_then(extract_function_declarator)
.is_some();
}
"field_declaration_list" | "function_definition" | "translation_unit" => return false,
_ => current = node.parent(),
}
}
false
}
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(Default)]
pub struct CppReconciledGroup {
pub by_name: HashMap<String, Arc<CppReconciledDefinitionIndex>>,
pub canonical_of: HashMap<CodeUnit, CodeUnit>,
}
pub fn cpp_reconcile_group_key_for_fq(
fq: &brokk_bifrost_core::analyzer::fq_name::FqName,
) -> Option<CppReconcileGroupKey> {
let interner = segment_interner();
let (member_identifier, _) = interner.resolve(fq.last()?);
if member_identifier.is_empty() {
return None;
}
let owner_terminal = fq
.segments()
.len()
.checked_sub(2)
.map(|index| interner.resolve(fq.segments()[index]).0.to_string());
Some(CppReconcileGroupKey {
member_identifier: member_identifier.to_string(),
owner_terminal,
})
}
#[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_from_units(
candidates: impl IntoIterator<Item = CodeUnit>,
keep_going: &dyn Fn() -> bool,
) -> Option<CppReconcileCandidates> {
let mut candidates = candidates.into_iter().collect::<Vec<_>>();
candidates.sort();
candidates.dedup();
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);
}
if !keep_going() {
return None;
}
Some(CppReconcileCandidates {
by_owner_terminal,
all,
})
}
const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
pub fn cpp_reconcile_group(
cpp: &dyn CppSource,
token: QueryToken<'_>,
key: &CppReconcileGroupKey,
candidates: &CppReconcileCandidates,
keep_going: &dyn Fn() -> bool,
on_candidate: &dyn Fn(),
) -> Option<CppReconciledGroup> {
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.stored_callable_unit_role(unit)
};
if !matches!(
role,
CppCallableUnitRole::Definition | CppCallableUnitRole::Both
) {
continue;
}
let Some(reconciled) =
cpp_reconcile_definition_identity(cpp, token, unit, &mut using_by_file, keep_going)
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());
}
let canonical_of = groups
.values()
.flat_map(|index| {
index
.provisional_of
.iter()
.map(|(canonical, physical)| (physical.clone(), canonical.clone()))
})
.collect();
let group = CppReconciledGroup {
canonical_of,
by_name: groups
.into_iter()
.map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
.collect(),
};
keep_going().then_some(group)
}
fn cpp_reconcile_definition_identity(
cpp: &dyn CppSource,
token: QueryToken<'_>,
unit: &CodeUnit,
using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
keep_going: &dyn Fn() -> bool,
) -> Option<ReconciledIdentity> {
let interner = segment_interner();
let mut provisional_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() {
provisional_owner_segments.push(text);
}
}
SegmentKind::Member => member = Some(text),
_ => return None,
}
}
let member = member?;
let single_owner = cpp_single_owner_context(cpp, token, unit, keep_going)?;
let owner_segments = single_owner.as_ref().map_or_else(
|| provisional_owner_segments,
|context| vec![context.owner.as_str()],
);
if owner_segments.is_empty() {
return None;
}
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();
if let Some(context) = &single_owner {
if context.global {
return keep_going().then(|| {
reconcile_out_of_line_member_identity(&owner_segments, member, &[""], &class_table)
})?;
}
for tier in
lexical_component_tiers(&[], false, &context.lexical).take(context.lexical.len())
{
if !keep_going() {
return None;
}
let namespace = tier.join("::");
if let Some(identity) = reconcile_out_of_line_member_identity(
&owner_segments,
member,
&[namespace.as_str()],
&class_table,
) {
return keep_going().then_some(identity);
}
}
}
if !keep_going() {
return None;
}
let using = using_by_file
.entry(unit.source().clone())
.or_insert_with(|| {
Arc::new(
cpp.declaration_source_facts(token, unit.source())
.map(|source| {
source
.facts
.using_namespaces
.iter()
.map(|(_, name)| name.clone())
.collect()
})
.unwrap_or_default(),
)
})
.clone();
let mut namespace_candidates: Vec<&str> = vec![""];
namespace_candidates.extend(using.iter().map(String::as_str));
reconcile_out_of_line_member_identity(
&owner_segments,
member,
&namespace_candidates,
&class_table,
)
}
#[derive(PartialEq, Eq)]
struct CppSingleOwnerContext {
owner: String,
global: bool,
lexical: Vec<String>,
}
fn cpp_single_owner_context(
cpp: &dyn CppSource,
token: QueryToken<'_>,
unit: &CodeUnit,
keep_going: &dyn Fn() -> bool,
) -> Option<Option<CppSingleOwnerContext>> {
let facts = cpp.declaration_source_facts(token, unit.source())?;
let mut has_single = false;
for fact in facts.for_unit(unit) {
if !keep_going() {
return None;
}
has_single |= fact.written_owner.len() == 1;
}
if !has_single {
return keep_going().then_some(None);
}
let syntax = cpp.prepared_syntax(token, unit.source())?;
if !keep_going() {
return None;
}
let scopes = cpp.orphaned_namespace_scopes(token, unit.source());
let root = syntax.tree().root_node();
let source = syntax.source();
let mut agreed = None;
for fact in facts.for_unit(unit) {
if !keep_going() {
return None;
}
let declaration = facts.source.declaration(fact.declaration);
let occurrence = facts.source.occurrence(declaration.occurrence).range;
let name_range = facts.source.occurrence(declaration.name?).range;
let mut node =
root.descendant_for_byte_range(occurrence.start_byte, occurrence.end_byte)?;
let selected = loop {
if !keep_going()
|| node.start_byte() != occurrence.start_byte
|| node.end_byte() != occurrence.end_byte
{
return None;
}
if matches!(
node.kind(),
"function_definition" | "function_declarator" | "declaration" | "field_declaration"
) {
break node;
}
node = node.parent()?;
};
let callable = if selected.kind() == "function_declarator" {
selected
} else {
selected
.child_by_field_name("declarator")
.and_then(extract_function_declarator)?
};
let name = declarator_name_node(callable)?;
let terminal = cpp_name_component_nodes(name)?.pop()?;
if name.is_missing()
|| name.has_error()
|| terminal.is_missing()
|| terminal.start_byte() != name_range.start_byte
|| terminal.end_byte() != name_range.end_byte
|| node_text(terminal, source) != unit.identifier()
{
return None;
}
let qualified = qualified_owner_components(name, source)?;
if qualified.names.len() != 1 || qualified.names != fact.written_owner {
return None;
}
let proof = CppSingleOwnerContext {
owner: qualified.names.into_iter().next()?,
global: qualified.global,
lexical: scopes.enclosing_namespace_components(selected, source),
};
if !keep_going() || agreed.as_ref().is_some_and(|prior| prior != &proof) {
return None;
}
agreed = Some(proof);
}
if !keep_going() {
return None;
}
Some(Some(agreed?))
}
#[cfg(test)]
mod tests {
#[test]
fn range_for_binder_grammar() {
use crate::grammar_test_support::{SOURCE, at, field, nodes, tree};
let parsed = tree(SOURCE);
let cases = [
(
314..348,
"structured_binding_declarator",
324..336,
None,
339..344,
&[(325..328, "key"), (330..335, "value")][..],
),
(
350..376,
"reference_declarator",
360..365,
None,
368..372,
&[(361..365, "same")][..],
),
(
378..407,
"pointer_declarator",
388..392,
Some(389..392),
395..403,
&[(389..392, "ptr")][..],
),
(
409..452,
"attributed_declarator",
419..440,
None,
443..448,
&[(419..423, "item")][..],
),
(
454..480,
"reference_declarator",
464..467,
None,
470..476,
&[(465..467, "\u{3bb}")][..],
),
];
for (span, kind, declarator_span, inner_field, iterable_span, binders) in cases {
let range = at(parsed.root_node(), "for_range_loop", span);
field(range, "declarator", kind, declarator_span);
field(range, "right", "identifier", iterable_span.clone());
let declarator = range.child_by_field_name("declarator").unwrap();
assert_eq!(
declarator
.child_by_field_name("declarator")
.map(|node| node.byte_range()),
inner_field
);
for (binder_span, text) in binders {
let binder = at(range, "identifier", binder_span.clone());
assert_eq!(binder.utf8_text(SOURCE.as_bytes()).unwrap(), *text);
assert!(crate::identity::cpp_is_range_for_binding_name(binder));
}
for identifier in nodes(range)
.into_iter()
.filter(|node| node.kind() == "identifier")
{
let expected = binders
.iter()
.any(|(span, _)| identifier.byte_range() == *span);
assert_eq!(
crate::identity::cpp_is_range_for_binding_name(identifier),
expected,
"identifier {:?}",
identifier.byte_range()
);
}
assert!(!crate::identity::cpp_is_range_for_binding_name(at(
range,
"identifier",
iterable_span
)));
}
}
use super::*;
use brokk_bifrost_test_macros::hardwon;
#[test]
fn physical_identity_cohorts_keep_recorded_owner_and_member_boundaries() {
use brokk_bifrost_core::analyzer::fq_name::FqName;
let mut fq = FqName::new();
for (text, kind) in [
("ns", SegmentKind::Package),
("Owner$Literal", SegmentKind::Type),
("operator std::string", SegmentKind::Member),
] {
fq.push(segment_interner().intern(text, kind));
}
let key = cpp_reconcile_group_key_for_fq(&fq).unwrap();
assert_eq!(key.owner_terminal.as_deref(), Some("Owner$Literal"));
assert_eq!(key.member_identifier, "operator std::string");
}
#[test]
#[hardwon("#2488: terminal cancellation must not publish identity cohorts")]
fn candidate_cohorts_recheck_terminal_cancellation_before_publish() {
use std::cell::Cell;
let unit = CodeUnit::new(
ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
"",
"Owner.run",
);
let cancel_after_last_candidate = {
let active = Cell::new(true);
move || active.replace(false)
};
assert!(
cpp_reconcile_candidates_from_units(
std::iter::once(unit.clone()),
&cancel_after_last_candidate,
)
.is_none(),
"a cancellation observed after the last sub-256 candidate must prevent publication"
);
let candidates = (0..=CANDIDATE_BUCKETING_POLL_STRIDE)
.map(|index| {
CodeUnit::new(
ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
"",
format!("Owner{index}.run"),
)
})
.collect::<Vec<_>>();
let cancel_at_bucket_boundary = {
let active = Cell::new(true);
move || active.replace(false)
};
assert!(
cpp_reconcile_candidates_from_units(candidates, &cancel_at_bucket_boundary).is_none(),
"a cancellation at the bucket boundary must prevent a partial cohort from publishing"
);
let cancelled = || false;
assert!(
cpp_reconcile_candidates_from_units(Vec::<CodeUnit>::new(), &cancelled).is_none(),
"an empty cancelled cohort must not publish"
);
}
fn parse_cpp(source: &str) -> Tree {
crate::diagnostics::parse_cpp_tree(source)
}
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)
}
fn is_conversion_target(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_conversion_operator_target_type(node)
}
fn is_recovered_macro_character_type(
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");
cpp_is_recovered_macro_character_token_type(node)
}
#[test]
#[hardwon(
"b114d87215ca: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
fn recovered_macro_character_tokens_are_not_type_references() {
let source = concat!(
"struct I {};\n",
"#define STRING_TOKEN_(name, ...)\n",
"struct Schema {\n",
" STRING_TOKEN_(MaxItems, 'm', 'I')\n",
" void ordinary(I value);\n",
" void malformed(I value, @);\n",
" void use() { I value; consume('I'); }\n",
"};\n",
);
let tree = parse_cpp(source);
let recovered_m = source.find("'m'").expect("recovered m") + 1;
let recovered_i = source.find("'I'").expect("recovered I") + 1;
for (label, start, text) in [
("lowercase character token", recovered_m, "m"),
("uppercase character token", recovered_i, "I"),
] {
assert!(
is_recovered_macro_character_type(&tree, source, start, text),
"{label} must match the exact recovery role"
);
}
let macro_first_argument = source.find("MaxItems").expect("macro first argument");
let ordinary = source.find("ordinary(I").expect("ordinary parameter") + "ordinary(".len();
let malformed =
source.find("malformed(I").expect("malformed parameter") + "malformed(".len();
let local = source
.find("I value; consume")
.expect("local type reference");
let expression_character = source.rfind("'I'").expect("expression character") + 1;
for (label, start, text) in [
("unquoted macro argument", macro_first_argument, "MaxItems"),
("ordinary parameter type", ordinary, "I"),
("parameter beside another error", malformed, "I"),
("local type reference", local, "I"),
("expression character literal", expression_character, "I"),
] {
assert!(
!is_recovered_macro_character_type(&tree, source, start, text),
"{label} must remain outside the recovery role"
);
}
}
#[test]
#[hardwon(
"0733cadab142: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
fn conversion_operator_target_components_are_identity_syntax_only() {
let source = concat!(
"namespace other { struct Target {}; template<class T> struct Box {}; }\n",
"using other::Target;\n",
"struct Source {\n",
" operator Target() const;\n",
" operator other::Target() const { return other::Target{}; }\n",
" template<class T> operator other::Box<T>() const { return {}; }\n",
" operator other::Target const&() const;\n",
" operator other::Target*() const;\n",
" other::Target ordinary() const {\n",
" return reinterpret_cast<other::Target&>(*this);\n",
" }\n",
" other::Target operator+() const { return {}; }\n",
"};\n",
);
let tree = parse_cpp(source);
let bare = source.find("operator Target").expect("bare target") + "operator ".len();
let qualified = source
.find("operator other::Target()")
.expect("qualified target")
+ "operator ".len();
let template = source
.find("operator other::Box<T>")
.expect("template target")
+ "operator ".len();
let cv_reference = source
.find("operator other::Target const&")
.expect("cv-reference target")
+ "operator other::".len();
let pointer = source
.find("operator other::Target*")
.expect("pointer target")
+ "operator other::".len();
for (label, start, text) in [
("bare target", bare, "Target"),
("qualified target scope", qualified, "other"),
(
"qualified target name",
qualified + "other::".len(),
"Target",
),
("template target scope", template, "other"),
("template target name", template + "other::".len(), "Box"),
(
"template target argument",
template + "other::Box<".len(),
"T",
),
("cv-reference target", cv_reference, "Target"),
("pointer target", pointer, "Target"),
] {
assert!(
is_conversion_target(&tree, source, start, text),
"the {label} at byte {start} belongs to the conversion identity"
);
}
let ordinary_return = source
.find("other::Target ordinary")
.expect("ordinary return");
let body_cast = source
.find("reinterpret_cast<other::Target")
.expect("body cast")
+ "reinterpret_cast<".len();
let overloaded_return = source
.find("other::Target operator+")
.expect("overloaded operator return");
for (label, start) in [
("ordinary return type", ordinary_return),
("body cast target", body_cast),
("overloaded-operator return type", overloaded_return),
] {
assert!(
!is_conversion_target(&tree, source, start, "other"),
"the {label} at byte {start} stays a reference"
);
}
}
#[test]
#[hardwon(
"246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
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]
#[hardwon(
"246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
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]
#[hardwon(
"246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
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"
);
}
}
fn class_roles(source: &str, name: &str) -> Vec<CppOccurrenceRole> {
let tree = parse_cpp(source);
let file = ProjectFile::new(std::env::temp_dir(), "occurrence-role.hpp");
let parsed = crate::adapter::parse_cpp_file(&file, source, &tree);
let unit = parsed
.declarations()
.iter()
.find(|unit| unit.is_class() && unit.fq_name() == name)
.unwrap_or_else(|| panic!("missing class {name}: {parsed:#?}"));
let ranges = parsed.declaration_ranges(unit);
assert!(!ranges.is_empty(), "{name} must have a declaration range");
ranges
.iter()
.map(|range| {
cpp_occurrence_role_for_range(
&CppRecoveredExportClassIndex::build(tree.root_node(), source),
tree.root_node(),
source,
unit,
range,
)
})
.collect()
}
#[test]
#[hardwon(
"ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
fn recovered_export_macro_classes_are_definitions_without_an_ordinary_class() {
let source = concat!(
"namespace api {\n",
"class PROJECT_PUBLIC_API(2, 0) Name final {\n",
" public:\n",
" Name();\n",
"};\n",
"class PROJECT_PUBLIC_API(2, 0) Other : public Name {\n",
" public:\n",
" Other();\n",
"};\n",
"} // namespace api\n",
);
for name in ["api.Name", "api.Other"] {
assert_eq!(
class_roles(source, name),
vec![CppOccurrenceRole::Definition],
"{name} is a complete recovered definition"
);
}
}
#[test]
#[hardwon(
"ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
)]
fn ordinary_class_roles_keep_their_plain_specifier_reading() {
assert_eq!(
class_roles("class Plain;\n", "Plain"),
vec![CppOccurrenceRole::DeclarationOnly],
"a forward declaration stays a declaration"
);
assert_eq!(
class_roles("class Plain { };\n", "Plain"),
vec![CppOccurrenceRole::Definition],
"a complete class stays a definition"
);
}
#[test]
fn partial_declaration_range_grammar_boundary() {
for prefix in ["", "// offset\n"] {
let source = format!("{prefix}void first(); void later() {{}}");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.unwrap();
let tree = parser.parse(&source, None).unwrap();
assert!(!tree.root_node().has_error());
let declaration = tree
.root_node()
.named_child(if prefix.is_empty() { 0 } else { 1 })
.unwrap();
assert_eq!(declaration.kind(), "declaration");
assert_eq!(declaration.byte_range(), prefix.len()..prefix.len() + 13);
let mut range = brokk_bifrost_core::analyzer::tree_walk::node_range(declaration);
range.end_byte -= 1; assert!(super::node_for_exact_range(tree.root_node(), &range).is_none());
let selected = super::cpp_declaration_node_for_range(tree.root_node(), &range).unwrap();
assert_eq!(
(selected.kind(), selected.byte_range()),
("declaration", prefix.len()..prefix.len() + 13)
);
}
}
}