use crate::adapter::parse_cpp_file_with_callable_observer;
use crate::declarations::{
CppCallableObserver, CppComparableNode, CppComparableSlot, CppParameterType,
cpp_active_template_type_parameter, cpp_callable_parameter_names,
cpp_callable_parameter_type_identities, cpp_callable_return_type_identity,
cpp_comparable_parameter_shapes, cpp_macro_swallowed_access_label, node_text,
};
use crate::graph::resolver::cpp_name_for;
use crate::structural::is_cpp_preprocessor_conditional;
use brokk_bifrost_core::analyzer::ProjectFile;
use brokk_bifrost_core::analyzer::model::{
CallableArity, CodeUnit, CodeUnitType, CppTemplateMetadata, SignatureMetadata,
StructuredTypeIdentity, StructuredTypeNodeView,
};
use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, collect_parse_errors};
use brokk_bifrost_core::hash::HashMap;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use tree_sitter::{Node, Parser};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CppExternalDeclarationLimits {
pub max_records: usize,
}
impl Default for CppExternalDeclarationLimits {
fn default() -> Self {
Self {
max_records: 250_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppExternalDeclarationCompleteness {
Complete,
Partial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppExternalMemberKind {
Function,
Field,
Macro,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppExternalVisibility {
Public,
Protected,
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppExternalTypeKind {
Class,
Namespace,
TypeAlias,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CppExternalOwner {
Type(String),
Namespace(String),
}
impl CppExternalOwner {
pub fn name(&self) -> &str {
match self {
Self::Type(name) | Self::Namespace(name) => name,
}
}
pub fn type_name(&self) -> Option<&str> {
match self {
Self::Type(name) => Some(name),
Self::Namespace(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CppExternalType {
pub name: String,
pub source_name: String,
pub kind: CppExternalTypeKind,
pub underlying_type: Option<StructuredTypeIdentity>,
pub template_metadata: Option<CppTemplateMetadata>,
pub visibility: CppExternalVisibility,
pub source_path: PathBuf,
pub direct_bases: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CppExternalMember {
pub owner: Option<CppExternalOwner>,
pub name: String,
pub qualified_name: String,
pub kind: CppExternalMemberKind,
pub visibility: CppExternalVisibility,
pub is_constructor: bool,
pub signature: Option<String>,
pub parameter_types: Option<Vec<CppParameterType>>,
pub parameter_shapes: Option<Vec<CppComparableSlot>>,
pub callable_arity: Option<CallableArity>,
pub parameter_names: Option<Vec<Option<String>>>,
pub explicitness: Option<CppCallableExplicitness>,
pub return_type: Option<StructuredTypeIdentity>,
pub returns_forwarding_reference: bool,
pub source_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ObservedCallable {
metadata: SignatureMetadata,
parameter_types: Option<Vec<CppParameterType>>,
parameter_names: Option<Vec<Option<String>>>,
parameter_shapes: Option<Vec<CppComparableSlot>>,
explicitness: Option<CppCallableExplicitness>,
return_type: Option<StructuredTypeIdentity>,
returns_forwarding_reference: bool,
}
#[derive(Default)]
struct ExternalCallableCollector {
by_occurrence: RefCell<HashMap<(CodeUnit, usize), Option<ObservedCallable>>>,
}
impl CppCallableObserver for ExternalCallableCollector {
fn observe(
&self,
unit: &CodeUnit,
declaration_start: usize,
metadata: &SignatureMetadata,
declarator: Node<'_>,
source: &str,
ancestry: &ParentIndex<'_>,
) {
let parameter_types = cpp_callable_parameter_type_identities(declarator, source, ancestry);
let parameter_names = declarator
.child_by_field_name("parameters")
.and_then(|parameters| cpp_callable_parameter_names(parameters, source));
let parameter_shapes = cpp_comparable_parameter_shapes(declarator, source, ancestry);
let return_type = metadata
.return_type_identity()
.cloned()
.or_else(|| cpp_callable_return_type_identity(declarator, source, ancestry));
let returns_forwarding_reference = cpp_returns_forwarding_reference(
declarator,
source,
ancestry,
parameter_types.as_deref(),
parameter_shapes.as_deref(),
return_type.as_ref(),
);
let observed = ObservedCallable {
metadata: metadata.clone(),
parameter_types,
parameter_names,
parameter_shapes,
explicitness: cpp_callable_explicitness(declarator),
return_type,
returns_forwarding_reference,
};
let mut by_occurrence = self.by_occurrence.borrow_mut();
let key = (unit.clone(), declaration_start);
if let Some(existing) = by_occurrence.get_mut(&key) {
if existing.as_ref() != Some(&observed) {
*existing = None;
}
} else {
by_occurrence.insert(key, Some(observed));
}
}
fn observe_duplicate(
&self,
unit: &CodeUnit,
declaration_start: usize,
declarator: Node<'_>,
source: &str,
ancestry: &ParentIndex<'_>,
metadata: &dyn Fn() -> SignatureMetadata,
) {
self.observe(
unit,
declaration_start,
&metadata(),
declarator,
source,
ancestry,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CppCallableExplicitness {
Implicit,
Explicit,
Conditional,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CppExternalDeclarationDiagnostic {
pub code: &'static str,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CppExternalDeclarationSet {
pub types: Vec<CppExternalType>,
pub members: Vec<CppExternalMember>,
pub completeness: CppExternalDeclarationCompleteness,
pub syntax_complete: bool,
pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
}
pub fn external_include_paths(source: &str) -> Vec<(PathBuf, bool)> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
let tree = parser.parse(source, None).expect("uncancelled C++ parse");
external_include_paths_from_root(source, tree.root_node())
}
pub fn external_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<(PathBuf, bool)> {
let mut paths = Vec::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.kind() == "preproc_include"
&& !has_conditional_preprocessor_ancestor(node)
&& let Some(path) = node.child_by_field_name("path")
&& let Some((path, quoted)) = match path.kind() {
"string_literal" => node_text(path, source)
.strip_prefix('"')
.and_then(|path| path.strip_suffix('"'))
.filter(|path| !path.is_empty())
.map(|path| (PathBuf::from(path), true)),
"system_lib_string" => node_text(path, source)
.strip_prefix('<')
.and_then(|path| path.strip_suffix('>'))
.filter(|path| !path.is_empty())
.map(|path| (PathBuf::from(path), false)),
_ => None,
}
{
paths.push((path, quoted));
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
paths.sort();
paths.dedup();
paths
}
pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
external_include_paths(source)
.into_iter()
.filter_map(|(path, quoted)| (!quoted).then_some(path))
.collect()
}
pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
external_include_paths_from_root(source, root)
.into_iter()
.filter_map(|(path, quoted)| (!quoted).then_some(path))
.collect()
}
fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
while let Some(parent) = node.parent() {
if is_cpp_preprocessor_conditional(parent) {
return true;
}
node = parent;
}
false
}
pub fn extract_external_declarations(
source_set_root: &Path,
source_path: &Path,
source: &str,
limits: CppExternalDeclarationLimits,
) -> CppExternalDeclarationSet {
let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
let tree = parser.parse(source, None).expect("uncancelled C++ parse");
let mut diagnostics = Vec::new();
let mut completeness = CppExternalDeclarationCompleteness::Complete;
let mut parse_errors = Vec::new();
collect_parse_errors(tree.root_node(), &mut parse_errors);
if !parse_errors.is_empty() {
completeness = CppExternalDeclarationCompleteness::Partial;
diagnostics.push(CppExternalDeclarationDiagnostic {
code: "cpp.external.parse_error",
message: format!(
"external header `{}` has parse errors",
source_path.display()
),
});
}
if has_unsupported_preprocessing(tree.root_node()) {
completeness = CppExternalDeclarationCompleteness::Partial;
diagnostics.push(CppExternalDeclarationDiagnostic {
code: "cpp.external.preprocessor_partial",
message: format!(
"external header `{}` has conditional or generated declarations",
source_path.display()
),
});
}
let mut syntax_complete = parse_errors.is_empty();
let callable_collector = ExternalCallableCollector::default();
let parsed = parse_cpp_file_with_callable_observer(&file, source, &tree, &callable_collector);
let mut parent_by_child = HashMap::default();
for (parent, children) in &parsed.children {
for child in children {
parent_by_child
.entry(child.clone())
.and_modify(|owner: &mut CodeUnit| {
if parent.is_class() && !owner.is_class() {
*owner = parent.clone();
}
})
.or_insert_with(|| parent.clone());
}
}
let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
declarations.sort_by_key(|declaration| {
(
declaration.fq_name(),
declaration.kind(),
declaration.signature().map(str::to_owned),
)
});
let mut types = Vec::new();
let mut members = Vec::new();
for declaration in declarations {
if types.len().saturating_add(members.len()) >= limits.max_records {
completeness = CppExternalDeclarationCompleteness::Partial;
syntax_complete = false;
diagnostics.push(CppExternalDeclarationDiagnostic {
code: "cpp.external.record_limit",
message: format!(
"external header `{}` exceeded the declaration record limit",
source_path.display()
),
});
break;
}
match declaration.kind() {
CodeUnitType::Module => types.push(CppExternalType {
name: declaration.fq_name(),
source_name: cpp_name_for(&declaration),
kind: CppExternalTypeKind::Namespace,
underlying_type: None,
template_metadata: None,
visibility: CppExternalVisibility::Public,
source_path: source_path.to_path_buf(),
direct_bases: Vec::new(),
}),
CodeUnitType::Class => types.push(CppExternalType {
name: declaration.fq_name(),
source_name: cpp_name_for(&declaration),
kind: if parsed.type_aliases.contains(&declaration) {
CppExternalTypeKind::TypeAlias
} else {
CppExternalTypeKind::Class
},
underlying_type: parsed
.signature_metadata
.get(&declaration)
.and_then(|records| records.first())
.and_then(|metadata| metadata.underlying_type_identity())
.cloned(),
template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
visibility: parsed
.ranges
.get(&declaration)
.and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
.map(|start| cpp_member_visibility(tree.root_node(), source, start))
.unwrap_or(CppExternalVisibility::Private),
source_path: source_path.to_path_buf(),
direct_bases: parsed
.raw_supertypes
.get(&declaration)
.cloned()
.unwrap_or_default(),
}),
CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
let declaration_start = parsed
.ranges
.get(&declaration)
.and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
let observed = (declaration.kind() == CodeUnitType::Function)
.then(|| {
declaration_start.and_then(|start| {
callable_collector
.by_occurrence
.borrow()
.get(&(declaration.clone(), start))
.cloned()
.flatten()
})
})
.flatten();
let metadata = if declaration.kind() == CodeUnitType::Function {
observed.as_ref().map(|record| &record.metadata)
} else {
parsed
.signature_metadata
.get(&declaration)
.and_then(|records| records.first())
};
let parameter_types = observed
.as_ref()
.and_then(|record| record.parameter_types.clone());
let parameter_names = observed
.as_ref()
.and_then(|record| record.parameter_names.clone());
let parameter_shapes = observed
.as_ref()
.and_then(|record| record.parameter_shapes.clone());
if declaration.kind() == CodeUnitType::Function
&& (parameter_types.is_none()
|| parameter_names.is_none()
|| parameter_shapes.is_none())
{
completeness = CppExternalDeclarationCompleteness::Partial;
syntax_complete = false;
diagnostics.push(CppExternalDeclarationDiagnostic {
code: "cpp.external.formal_roles_unavailable",
message: format!(
"external header `{}` has unavailable formal roles for `{}`",
source_path.display(),
declaration.fq_name()
),
});
}
let return_type = metadata
.and_then(|metadata| metadata.return_type_identity())
.cloned()
.or_else(|| {
observed
.as_ref()
.and_then(|record| record.return_type.clone())
});
let returns_forwarding_reference = observed
.as_ref()
.is_some_and(|record| record.returns_forwarding_reference);
members.push(CppExternalMember {
owner: nearest_declaration_owner(&declaration, &parent_by_child),
name: declaration.terminal_name().to_owned(),
qualified_name: cpp_name_for(&declaration),
kind: match declaration.kind() {
CodeUnitType::Function => CppExternalMemberKind::Function,
CodeUnitType::Field => CppExternalMemberKind::Field,
CodeUnitType::Macro => CppExternalMemberKind::Macro,
_ => unreachable!("the outer match admits exactly member kinds"),
},
visibility: declaration_start
.map(|start| cpp_member_visibility(tree.root_node(), source, start))
.unwrap_or(CppExternalVisibility::Private),
is_constructor: metadata
.is_some_and(|metadata| metadata.callable_is_constructor()),
signature: declaration.signature().map(str::to_owned),
parameter_types,
parameter_shapes,
callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
parameter_names,
explicitness: observed.as_ref().and_then(|record| record.explicitness),
return_type,
returns_forwarding_reference,
source_path: source_path.to_path_buf(),
});
}
CodeUnitType::FileScope => {}
}
}
CppExternalDeclarationSet {
types,
members,
completeness,
syntax_complete,
diagnostics,
}
}
fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
let mut current = root.descendant_for_byte_range(start_byte, start_byte);
while let Some(node) = current {
let Some(parent) = node.parent() else {
break;
};
if parent.kind() == "field_declaration_list" {
let default = match parent.parent().map(|owner| owner.kind()) {
Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
_ => CppExternalVisibility::Private,
};
let mut visibility = default;
let mut cursor = parent.walk();
for child in parent.named_children(&mut cursor) {
if child.start_byte() > start_byte {
break;
}
let access = if child.kind() == "access_specifier" {
Some(node_text(child, source).trim_end_matches(':').trim())
} else {
cpp_macro_swallowed_access_label(child, source).map(|label| label.access)
};
if let Some(access) = access {
visibility = match access {
"public" => CppExternalVisibility::Public,
"protected" => CppExternalVisibility::Protected,
"private" => CppExternalVisibility::Private,
_ => CppExternalVisibility::Private,
};
}
}
return visibility;
}
current = Some(parent);
}
CppExternalVisibility::Public
}
fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
while !matches!(
declarator.kind(),
"declaration" | "field_declaration" | "function_definition"
) {
declarator = declarator.parent()?;
}
if declarator.has_error() {
return None;
}
let mut stack = vec![declarator];
let mut explicit = None;
while let Some(node) = stack.pop() {
if node.kind() == "explicit_function_specifier" {
if explicit.is_some() {
return None;
}
explicit = Some(if node.named_child_count() == 0 {
CppCallableExplicitness::Explicit
} else {
CppCallableExplicitness::Conditional
});
continue;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
}
fn cpp_returns_forwarding_reference<'tree>(
declarator: Node<'tree>,
source: &str,
ancestry: &ParentIndex<'tree>,
parameters: Option<&[CppParameterType]>,
shapes: Option<&[CppComparableSlot]>,
returns: Option<&StructuredTypeIdentity>,
) -> bool {
let (
Some([CppParameterType::Structured(parameter)]),
Some([CppComparableSlot::Shape(shape)]),
Some(returns),
) = (parameters, shapes, returns)
else {
return false;
};
let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
else {
return false;
};
let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
return false;
};
let [parameter_name] = name.path() else {
return false;
};
let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
return false;
};
if !matches!(
shape.node(*inner),
CppComparableNode::Named {
konst: false,
volatil: false,
..
}
) || name.is_absolute()
|| !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
|| !matches!(
returns.view(returns.root_id()),
Some(StructuredTypeNodeView::RvalueReference(_))
)
{
return false;
}
let mut current = declarator;
while let Some(parent) = ancestry.parent(current) {
if matches!(
parent.kind(),
"function_definition" | "declaration" | "field_declaration"
) {
let mut cursor = parent.walk();
if parent.named_children(&mut cursor).any(|child| {
child.kind() == "type_qualifier"
&& matches!(node_text(child, source), "const" | "volatile")
}) {
return false;
}
return parent
.child_by_field_name("type")
.is_some_and(|return_node| {
cpp_forwarded_return_type(return_node, source, parameter_name)
});
}
current = parent;
}
false
}
fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
let mut components = Vec::new();
let mut removals = 0;
let mut stack = vec![node];
while let Some(current) = stack.pop() {
match current.kind() {
"identifier" | "type_identifier" | "namespace_identifier" => {
components.push(node_text(current, source));
}
"dependent_type" => {
if current.named_child_count() != 1 {
return false;
}
stack.push(current.named_child(0).expect("one named child"));
}
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
let Some(name) = current.child_by_field_name("name") else {
return false;
};
stack.push(name);
if let Some(scope) = current.child_by_field_name("scope") {
stack.push(scope);
}
}
"template_type" => {
let (Some(name), Some(arguments)) = (
current.child_by_field_name("name"),
current.child_by_field_name("arguments"),
) else {
return false;
};
if !matches!(name.kind(), "identifier" | "type_identifier")
|| node_text(name, source) != "remove_reference"
|| arguments.named_child_count() != 1
{
return false;
}
let mut argument = arguments.named_child(0).expect("one named argument");
if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
argument = argument.named_child(0).expect("one named type");
}
if !matches!(argument.kind(), "identifier" | "type_identifier")
|| node_text(argument, source) != parameter
{
return false;
}
removals += 1;
stack.push(name);
}
_ => return false,
}
}
(removals == 0 && components == [parameter])
|| (removals == 1
&& (components == ["std", "remove_reference", "type"]
|| components == ["remove_reference", "type"]))
}
fn nearest_declaration_owner(
declaration: &CodeUnit,
parent_by_child: &HashMap<CodeUnit, CodeUnit>,
) -> Option<CppExternalOwner> {
let mut current = declaration;
while let Some(parent) = parent_by_child.get(current) {
match parent.kind() {
CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
_ => {}
}
current = parent;
}
None
}
fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if matches!(
node.kind(),
"preproc_def"
| "preproc_function_def"
| "preproc_if"
| "preproc_ifdef"
| "preproc_ifndef"
| "preproc_elif"
| "preproc_else"
) {
return true;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use brokk_bifrost_core::analyzer::model::CallableArity;
use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
use brokk_bifrost_test_macros::hardwon;
fn extract(source: &str) -> CppExternalDeclarationSet {
let temp = tempfile::tempdir().expect("temp root");
extract_external_declarations(
temp.path(),
Path::new("vector"),
source,
CppExternalDeclarationLimits::default(),
)
}
#[test]
fn extracts_literal_include_paths_with_delimiters() {
let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
assert_eq!(
vec![
(PathBuf::from("local.hpp"), true),
(PathBuf::from("vector"), false),
],
external_include_paths(source)
);
assert_eq!(
vec![PathBuf::from("vector")],
external_angle_include_paths(source)
);
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("tree");
assert_eq!(
vec![PathBuf::from("vector")],
external_angle_include_paths_from_root(source, tree.root_node())
);
}
#[test]
fn extracts_namespaced_template_type_and_owned_members() {
let declarations = extract(
r#"
namespace std {
template <typename T> class vector : public sequence<T> {
public:
vector();
void push_back(const T& value);
T size;
};
}
"#,
);
assert_eq!(
CppExternalDeclarationCompleteness::Complete,
declarations.completeness
);
assert!(
declarations.types.iter().any(|record| {
record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
}),
"{declarations:#?}"
);
assert!(
declarations.members.iter().any(|record| {
record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
&& record.name == "push_back"
&& record.visibility == CppExternalVisibility::Public
}),
"{declarations:#?}"
);
assert!(
declarations.members.iter().any(|record| {
record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
&& record.name == "size"
}),
"{declarations:#?}"
);
}
#[test]
fn preserves_copy_and_move_reference_parameter_shapes() {
let source = r#"
class Widget {
public:
Widget(const Widget&);
Widget(Widget&&);
};
"#;
let declarations = extract(source);
let constructors = declarations
.members
.iter()
.filter(|member| {
member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
&& member.name == "Widget"
})
.collect::<Vec<_>>();
assert_eq!(constructors.len(), 2, "{declarations:#?}");
let shapes = constructors
.iter()
.map(|constructor| {
let parameters = constructor
.parameter_types
.as_ref()
.expect("constructor parameters");
assert_eq!(parameters.len(), 1, "{constructor:#?}");
let CppParameterType::Structured(identity) = ¶meters[0] else {
panic!("constructor parameter shape: {constructor:#?}");
};
(
identity.clone(),
matches!(
identity.view(identity.root_id()),
Some(StructuredTypeNodeView::Reference(_))
),
matches!(
identity.view(identity.root_id()),
Some(StructuredTypeNodeView::RvalueReference(_))
),
)
})
.collect::<Vec<_>>();
assert_ne!(shapes[0].0, shapes[1].0);
assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
assert_eq!(declarations, extract(source));
}
#[test]
fn preserves_callable_arities_for_defaulted_and_required_parameters() {
let declarations = extract(
r#"
namespace std {
template <class C, class Alloc> class basic_string {
public:
basic_string(const C*, const Alloc& = Alloc());
basic_string(const C&, const Alloc&);
};
}
"#,
);
let constructors = declarations
.members
.iter()
.filter(|member| {
member.owner.as_ref().and_then(CppExternalOwner::type_name)
== Some("std.basic_string")
&& member.name == "basic_string"
})
.collect::<Vec<_>>();
assert_eq!(constructors.len(), 2, "{declarations:#?}");
assert!(
constructors
.iter()
.any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
"defaulted constructor arity must be required=1,total=2: {constructors:#?}"
);
assert!(
constructors
.iter()
.any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
"non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
);
}
#[test]
#[hardwon(
"6ec52f232835: quicksilver round 4 survivor review: lexical namespaces stole out-of-line exported member owners"
)]
fn recovered_external_api_manifest() {
let declarations = extract(
r#"
#define EXPORT_MARKER
namespace n { class Widget; int damaged = ; }
class EXPORT_MARKER Widget { public: void first(); };
class EXPORT(2,0) Attributed final {
public:
ATTRIBUTE("old") int value();
void following();
};
namespace a::a {
struct C { void f(); void f()const; void g()&; void g()&&; };
void ::a::a::C::f() {}
void ::a::a::C::f()const {}
void C::g()& {}
void C::g()&& {}
}
DEPRECATED /* between name and arguments */ (
EXPORT_MARKER int old(int value), "hint"
);
EXPORT_MARKER int next();
void after() {}
"#,
);
let mut types = declarations
.types
.iter()
.map(|record| record.name.as_str())
.collect::<Vec<_>>();
types.sort_unstable();
assert_eq!(
types,
["Attributed", "a", "a::a", "a::a.C", "n", "n.Widget"]
);
let mut callables = declarations
.members
.iter()
.filter(|record| record.kind == CppExternalMemberKind::Function)
.map(|record| {
(
record.owner.as_ref().map(CppExternalOwner::name),
record.name.as_str(),
record.signature.as_deref(),
)
})
.collect::<Vec<_>>();
callables.sort_unstable();
let mut expected = vec![
(None, "after", Some("()")),
(None, "next", Some("()")),
(None, "old", Some("(int)")),
(Some("Attributed"), "following", Some("()")),
(Some("Attributed"), "value", Some("()")),
(Some("a::a.C"), "f", Some("()")),
(Some("a::a.C"), "f", Some("() const")),
(Some("a::a.C"), "g", Some("() &")),
(Some("a::a.C"), "g", Some("() &&")),
(Some("n.Widget"), "first", Some("()")),
];
expected.sort_unstable();
assert_eq!(callables, expected);
}
#[test]
fn preserves_callable_parameter_names() {
let declarations = extract(
"struct Owner {};\n\
void connect(int fd, const char *address, unsigned length);\n\
void callbacks(void (*callback)(int argument), int array[extent], int (Owner::*member)(int nested));\n",
);
for (name, expected) in [
("connect", vec![Some("fd"), Some("address"), Some("length")]),
(
"callbacks",
vec![Some("callback"), Some("array"), Some("member")],
),
] {
let candidates = declarations
.members
.iter()
.filter(|member| member.name == name)
.collect::<Vec<_>>();
let [member] = candidates.as_slice() else {
panic!("{name} declaration: {declarations:#?}");
};
assert_eq!(
member.parameter_names,
Some(
expected
.into_iter()
.map(|name| name.map(str::to_owned))
.collect()
),
"{name}: outer parameter slots must agree with the API manifest"
);
}
}
#[test]
#[brokk_bifrost_test_macros::hardwon(
"5e79db920891: quicksilver round 4 survivor tests: unnamed formals inherit nested names"
)]
fn unnamed_callable_parameter_names() {
let declarations = extract("void unnamed(void (*)(int argument), int [extent]);\n");
let [member] = declarations.members.as_slice() else {
panic!("one exported callable: {declarations:#?}");
};
assert_eq!(member.parameter_names, Some(vec![None, None]));
}
#[test]
fn preserves_callable_explicitness_for_implicit_binding() {
for (declaration, expected) in [
("Widget(const char*);", CppCallableExplicitness::Implicit),
(
"explicit Widget(const char*);",
CppCallableExplicitness::Explicit,
),
(
"explicit(true) Widget(const char*);",
CppCallableExplicitness::Conditional,
),
] {
let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
let member = declarations
.members
.first()
.unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
assert_eq!(Some(expected), member.explicitness, "{member:#?}");
}
}
#[test]
fn preserves_callable_return_reference_shapes() {
let declarations = extract(
r#"
namespace std {
class basic_string {
public:
basic_string& operator=(const basic_string&);
basic_string&& operator=(basic_string&&);
void operator=(int);
};
}
"#,
);
let assignments = declarations
.members
.iter()
.filter(|member| member.name == "operator=")
.collect::<Vec<_>>();
assert_eq!(assignments.len(), 3, "{declarations:#?}");
let copy = assignments
.iter()
.find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
.unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
let Some(StructuredTypeNodeView::Reference(inner)) = copy
.return_type
.as_ref()
.and_then(|identity| identity.view(identity.root_id()))
else {
panic!("copy return type: {copy:#?}");
};
let Some(StructuredTypeNodeView::Named(name)) = copy
.return_type
.as_ref()
.and_then(|identity| identity.view(inner))
else {
panic!("copy return target: {copy:#?}");
};
assert_eq!(["basic_string"], name.path());
assert_eq!(["std", "basic_string"], name.lexical_scope());
let move_assignment = declarations
.members
.iter()
.find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
.unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
assert!(matches!(
move_assignment
.return_type
.as_ref()
.and_then(|identity| identity.view(identity.root_id())),
Some(StructuredTypeNodeView::RvalueReference(_))
));
let void_assignment = declarations
.members
.iter()
.find(|member| member.signature.as_deref() == Some("(int)"))
.unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
let Some(StructuredTypeNodeView::Named(name)) = void_assignment
.return_type
.as_ref()
.and_then(|identity| identity.view(identity.root_id()))
else {
panic!("void return type: {void_assignment:#?}");
};
assert_eq!(["void"], name.path());
}
#[test]
fn preserves_type_alias_underlying_structured_identity() {
let declarations = extract(
r#"
namespace std {
template<class C, class Traits, class Alloc> class basic_string;
using string = basic_string<char, char_traits<char>, allocator<char>>;
}
"#,
);
let alias = declarations
.types
.iter()
.find(|record| record.name == "std.string")
.unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
let identity = alias
.underlying_type
.as_ref()
.unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
let Some(StructuredTypeNodeView::Generic { base, arguments }) =
identity.view(identity.root_id())
else {
panic!("generic basic_string alias target: {identity:#?}");
};
assert_eq!(3, arguments.len());
let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
panic!("named basic_string alias base: {identity:#?}");
};
assert_eq!(["basic_string"], name.path());
assert_eq!(["std"], name.lexical_scope());
}
#[test]
fn keeps_same_short_names_under_distinct_owners() {
let declarations = extract(
"namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
);
let mut owners = declarations
.members
.iter()
.filter(|member| member.name == "add")
.filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
.collect::<Vec<_>>();
owners.sort();
assert_eq!(vec!["first.box", "second.box"], owners);
assert!(
declarations
.members
.iter()
.filter(|member| member.name == "add")
.all(|member| member.visibility == CppExternalVisibility::Private)
);
}
#[test]
fn nested_type_visibility_follows_the_enclosing_access_section() {
let declarations = extract(
"class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
);
assert!(declarations.types.iter().any(|record| {
record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
}));
assert!(declarations.types.iter().any(|record| {
record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
}));
assert!(declarations.types.iter().any(|record| {
record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
}));
}
#[test]
fn preprocessor_and_record_limits_make_the_surface_partial() {
let temp = tempfile::tempdir().expect("temp root");
let declarations = extract_external_declarations(
temp.path(),
Path::new("limited.hpp"),
"#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
CppExternalDeclarationLimits { max_records: 1 },
);
assert_eq!(
CppExternalDeclarationCompleteness::Partial,
declarations.completeness
);
assert!(
declarations
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
);
assert!(
declarations
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
);
}
}