1use crate::adapter::parse_cpp_file;
8use crate::declarations::{
9 CppParameterType, cpp_callable_parameter_type_identities, cpp_function_declarator_at, node_text,
10};
11use crate::graph::resolver::cpp_name_for;
12use brokk_bifrost_core::analyzer::ProjectFile;
13use brokk_bifrost_core::analyzer::model::{CodeUnit, CodeUnitType, StructuredTypeIdentity};
14use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, collect_parse_errors};
15use brokk_bifrost_core::hash::HashMap;
16use std::path::{Path, PathBuf};
17use tree_sitter::{Node, Parser};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct CppExternalDeclarationLimits {
21 pub max_records: usize,
22}
23
24impl Default for CppExternalDeclarationLimits {
25 fn default() -> Self {
26 Self {
27 max_records: 250_000,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CppExternalDeclarationCompleteness {
34 Complete,
35 Partial,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum CppExternalMemberKind {
40 Function,
41 Field,
42 Macro,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CppExternalVisibility {
47 Public,
48 Protected,
49 Private,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct CppExternalType {
54 pub name: String,
55 pub source_name: String,
56 pub visibility: CppExternalVisibility,
57 pub source_path: PathBuf,
58 pub direct_bases: Vec<String>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CppExternalMember {
63 pub owner: Option<String>,
64 pub name: String,
65 pub qualified_name: String,
66 pub kind: CppExternalMemberKind,
67 pub visibility: CppExternalVisibility,
68 pub is_constructor: bool,
69 pub signature: Option<String>,
70 pub parameter_types: Option<Vec<CppParameterType>>,
77 pub return_type: Option<StructuredTypeIdentity>,
78 pub source_path: PathBuf,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct CppExternalDeclarationDiagnostic {
83 pub code: &'static str,
84 pub message: String,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct CppExternalDeclarationSet {
89 pub types: Vec<CppExternalType>,
90 pub members: Vec<CppExternalMember>,
91 pub completeness: CppExternalDeclarationCompleteness,
92 pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
93}
94
95pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
101 let mut parser = Parser::new();
102 parser
103 .set_language(&tree_sitter_cpp::LANGUAGE.into())
104 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
105 let tree = parser.parse(source, None).expect("uncancelled C++ parse");
106 external_angle_include_paths_from_root(source, tree.root_node())
107}
108
109pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
115 let mut paths = Vec::new();
116 let mut stack = vec![root];
117 while let Some(node) = stack.pop() {
118 if node.kind() == "preproc_include"
119 && !has_conditional_preprocessor_ancestor(node)
120 && let Some(path) = node.child_by_field_name("path")
121 && path.kind() == "system_lib_string"
122 && let Some(path) = node_text(path, source)
123 .strip_prefix('<')
124 .and_then(|path| path.strip_suffix('>'))
125 .filter(|path| !path.is_empty())
126 {
127 paths.push(PathBuf::from(path));
128 }
129 let mut cursor = node.walk();
130 stack.extend(node.named_children(&mut cursor));
131 }
132 paths.sort();
133 paths.dedup();
134 paths
135}
136
137fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
138 while let Some(parent) = node.parent() {
139 if matches!(
140 parent.kind(),
141 "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif" | "preproc_else"
142 ) {
143 return true;
144 }
145 node = parent;
146 }
147 false
148}
149
150pub fn extract_external_declarations(
156 source_set_root: &Path,
157 source_path: &Path,
158 source: &str,
159 limits: CppExternalDeclarationLimits,
160) -> CppExternalDeclarationSet {
161 let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
162 let mut parser = Parser::new();
163 parser
164 .set_language(&tree_sitter_cpp::LANGUAGE.into())
165 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
166 let tree = parser.parse(source, None).expect("uncancelled C++ parse");
167
168 let mut diagnostics = Vec::new();
169 let mut completeness = CppExternalDeclarationCompleteness::Complete;
170 let mut parse_errors = Vec::new();
171 collect_parse_errors(tree.root_node(), &mut parse_errors);
172 if !parse_errors.is_empty() {
173 completeness = CppExternalDeclarationCompleteness::Partial;
174 diagnostics.push(CppExternalDeclarationDiagnostic {
175 code: "cpp.external.parse_error",
176 message: format!(
177 "external header `{}` has parse errors",
178 source_path.display()
179 ),
180 });
181 }
182 if has_unsupported_preprocessing(tree.root_node()) {
183 completeness = CppExternalDeclarationCompleteness::Partial;
184 diagnostics.push(CppExternalDeclarationDiagnostic {
185 code: "cpp.external.preprocessor_partial",
186 message: format!(
187 "external header `{}` has conditional or generated declarations",
188 source_path.display()
189 ),
190 });
191 }
192
193 let parsed = parse_cpp_file(&file, source, &tree);
194 let mut parent_by_child = HashMap::default();
195 for (parent, children) in &parsed.children {
196 for child in children {
197 parent_by_child.insert(child.clone(), parent.clone());
198 }
199 }
200
201 let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
202 declarations.sort_by_key(|declaration| {
203 (
204 declaration.fq_name(),
205 declaration.kind(),
206 declaration.signature().map(str::to_owned),
207 )
208 });
209
210 let ancestry = ParentIndex::new(tree.root_node());
213 let mut types = Vec::new();
214 let mut members = Vec::new();
215 for declaration in declarations {
216 if types.len().saturating_add(members.len()) >= limits.max_records {
217 completeness = CppExternalDeclarationCompleteness::Partial;
218 diagnostics.push(CppExternalDeclarationDiagnostic {
219 code: "cpp.external.record_limit",
220 message: format!(
221 "external header `{}` exceeded the declaration record limit",
222 source_path.display()
223 ),
224 });
225 break;
226 }
227 match declaration.kind() {
228 CodeUnitType::Class => types.push(CppExternalType {
229 name: declaration.fq_name(),
230 source_name: cpp_name_for(&declaration),
231 visibility: parsed
232 .ranges
233 .get(&declaration)
234 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
235 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
236 .unwrap_or(CppExternalVisibility::Private),
237 source_path: source_path.to_path_buf(),
238 direct_bases: parsed
239 .raw_supertypes
240 .get(&declaration)
241 .cloned()
242 .unwrap_or_default(),
243 }),
244 CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
245 let metadata = parsed
246 .signature_metadata
247 .get(&declaration)
248 .and_then(|records| records.first());
249 let declaration_start = parsed
250 .ranges
251 .get(&declaration)
252 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
253 let parameter_types = (declaration.kind() == CodeUnitType::Function)
254 .then(|| {
255 declaration_start
256 .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
257 .map(|declarator| {
258 cpp_callable_parameter_type_identities(
259 declarator, source, &ancestry,
260 )
261 })
262 })
263 .flatten();
264 members.push(CppExternalMember {
265 owner: nearest_type_owner(&declaration, &parent_by_child),
266 name: declaration.terminal_name().to_owned(),
267 qualified_name: cpp_name_for(&declaration),
268 kind: match declaration.kind() {
269 CodeUnitType::Function => CppExternalMemberKind::Function,
270 CodeUnitType::Field => CppExternalMemberKind::Field,
271 CodeUnitType::Macro => CppExternalMemberKind::Macro,
272 _ => unreachable!("the outer match admits exactly member kinds"),
273 },
274 visibility: declaration_start
275 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
276 .unwrap_or(CppExternalVisibility::Private),
277 is_constructor: metadata
278 .is_some_and(|metadata| metadata.callable_is_constructor()),
279 signature: declaration.signature().map(str::to_owned),
280 parameter_types,
281 return_type: metadata
282 .and_then(|metadata| metadata.return_type_identity())
283 .cloned(),
284 source_path: source_path.to_path_buf(),
285 });
286 }
287 CodeUnitType::Module | CodeUnitType::FileScope => {}
288 }
289 }
290
291 CppExternalDeclarationSet {
292 types,
293 members,
294 completeness,
295 diagnostics,
296 }
297}
298
299fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
300 let mut current = root.descendant_for_byte_range(start_byte, start_byte);
301 while let Some(node) = current {
302 let Some(parent) = node.parent() else {
303 break;
304 };
305 if parent.kind() == "field_declaration_list" {
306 let default = match parent.parent().map(|owner| owner.kind()) {
307 Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
308 _ => CppExternalVisibility::Private,
309 };
310 let mut visibility = default;
311 let mut cursor = parent.walk();
312 for child in parent.named_children(&mut cursor) {
313 if child.start_byte() > start_byte {
314 break;
315 }
316 if child.kind() == "access_specifier" {
317 visibility = match node_text(child, source).trim_end_matches(':').trim() {
318 "public" => CppExternalVisibility::Public,
319 "protected" => CppExternalVisibility::Protected,
320 "private" => CppExternalVisibility::Private,
321 _ => CppExternalVisibility::Private,
322 };
323 }
324 }
325 return visibility;
326 }
327 current = Some(parent);
328 }
329 CppExternalVisibility::Public
330}
331
332fn nearest_type_owner(
333 declaration: &CodeUnit,
334 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
335) -> Option<String> {
336 let mut current = declaration;
337 while let Some(parent) = parent_by_child.get(current) {
338 if parent.kind() == CodeUnitType::Class {
339 return Some(parent.fq_name());
340 }
341 current = parent;
342 }
343 None
344}
345
346fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
347 let mut stack = vec![root];
348 while let Some(node) = stack.pop() {
349 if matches!(
350 node.kind(),
351 "preproc_def"
352 | "preproc_function_def"
353 | "preproc_if"
354 | "preproc_ifdef"
355 | "preproc_ifndef"
356 | "preproc_elif"
357 | "preproc_else"
358 ) {
359 return true;
360 }
361 let mut cursor = node.walk();
362 stack.extend(node.named_children(&mut cursor));
363 }
364 false
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn extract(source: &str) -> CppExternalDeclarationSet {
372 let temp = tempfile::tempdir().expect("temp root");
373 extract_external_declarations(
374 temp.path(),
375 Path::new("vector"),
376 source,
377 CppExternalDeclarationLimits::default(),
378 )
379 }
380
381 #[test]
382 fn extracts_only_literal_angle_include_paths() {
383 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
384 assert_eq!(
385 vec![PathBuf::from("vector")],
386 external_angle_include_paths(source)
387 );
388 let mut parser = Parser::new();
389 parser
390 .set_language(&tree_sitter_cpp::LANGUAGE.into())
391 .expect("C++ grammar");
392 let tree = parser.parse(source, None).expect("tree");
393 assert_eq!(
394 vec![PathBuf::from("vector")],
395 external_angle_include_paths_from_root(source, tree.root_node())
396 );
397 }
398
399 #[test]
400 fn extracts_namespaced_template_type_and_owned_members() {
401 let declarations = extract(
402 r#"
403 namespace std {
404 template <typename T> class vector : public sequence<T> {
405 public:
406 vector();
407 void push_back(const T& value);
408 T size;
409 };
410 }
411 "#,
412 );
413
414 assert_eq!(
415 CppExternalDeclarationCompleteness::Complete,
416 declarations.completeness
417 );
418 assert!(
419 declarations.types.iter().any(|record| {
420 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
421 }),
422 "{declarations:#?}"
423 );
424 assert!(
425 declarations.members.iter().any(|record| {
426 record.owner.as_deref() == Some("std.vector")
427 && record.name == "push_back"
428 && record.visibility == CppExternalVisibility::Public
429 }),
430 "{declarations:#?}"
431 );
432 assert!(
433 declarations.members.iter().any(|record| {
434 record.owner.as_deref() == Some("std.vector") && record.name == "size"
435 }),
436 "{declarations:#?}"
437 );
438 }
439
440 #[test]
441 fn keeps_same_short_names_under_distinct_owners() {
442 let declarations = extract(
443 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
444 );
445 let mut owners = declarations
446 .members
447 .iter()
448 .filter(|member| member.name == "add")
449 .filter_map(|member| member.owner.clone())
450 .collect::<Vec<_>>();
451 owners.sort();
452
453 assert_eq!(vec!["first.box", "second.box"], owners);
454 assert!(
455 declarations
456 .members
457 .iter()
458 .filter(|member| member.name == "add")
459 .all(|member| member.visibility == CppExternalVisibility::Private)
460 );
461 }
462
463 #[test]
464 fn nested_type_visibility_follows_the_enclosing_access_section() {
465 let declarations = extract(
466 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
467 );
468 assert!(declarations.types.iter().any(|record| {
469 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
470 }));
471 assert!(declarations.types.iter().any(|record| {
472 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
473 }));
474 assert!(declarations.types.iter().any(|record| {
475 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
476 }));
477 }
478
479 #[test]
480 fn preprocessor_and_record_limits_make_the_surface_partial() {
481 let temp = tempfile::tempdir().expect("temp root");
482 let declarations = extract_external_declarations(
483 temp.path(),
484 Path::new("limited.hpp"),
485 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
486 CppExternalDeclarationLimits { max_records: 1 },
487 );
488
489 assert_eq!(
490 CppExternalDeclarationCompleteness::Partial,
491 declarations.completeness
492 );
493 assert!(
494 declarations
495 .diagnostics
496 .iter()
497 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
498 );
499 assert!(
500 declarations
501 .diagnostics
502 .iter()
503 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
504 );
505 }
506}