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::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 mut types = Vec::new();
211 let mut members = Vec::new();
212 for declaration in declarations {
213 if types.len().saturating_add(members.len()) >= limits.max_records {
214 completeness = CppExternalDeclarationCompleteness::Partial;
215 diagnostics.push(CppExternalDeclarationDiagnostic {
216 code: "cpp.external.record_limit",
217 message: format!(
218 "external header `{}` exceeded the declaration record limit",
219 source_path.display()
220 ),
221 });
222 break;
223 }
224 match declaration.kind() {
225 CodeUnitType::Class => types.push(CppExternalType {
226 name: declaration.fq_name(),
227 source_name: cpp_name_for(&declaration),
228 visibility: parsed
229 .ranges
230 .get(&declaration)
231 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
232 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
233 .unwrap_or(CppExternalVisibility::Private),
234 source_path: source_path.to_path_buf(),
235 direct_bases: parsed
236 .raw_supertypes
237 .get(&declaration)
238 .cloned()
239 .unwrap_or_default(),
240 }),
241 CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
242 let metadata = parsed
243 .signature_metadata
244 .get(&declaration)
245 .and_then(|records| records.first());
246 let declaration_start = parsed
247 .ranges
248 .get(&declaration)
249 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
250 let parameter_types = (declaration.kind() == CodeUnitType::Function)
251 .then(|| {
252 declaration_start
253 .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
254 .map(|declarator| {
255 cpp_callable_parameter_type_identities(declarator, source)
256 })
257 })
258 .flatten();
259 members.push(CppExternalMember {
260 owner: nearest_type_owner(&declaration, &parent_by_child),
261 name: declaration.terminal_name().to_owned(),
262 qualified_name: cpp_name_for(&declaration),
263 kind: match declaration.kind() {
264 CodeUnitType::Function => CppExternalMemberKind::Function,
265 CodeUnitType::Field => CppExternalMemberKind::Field,
266 CodeUnitType::Macro => CppExternalMemberKind::Macro,
267 _ => unreachable!("the outer match admits exactly member kinds"),
268 },
269 visibility: declaration_start
270 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
271 .unwrap_or(CppExternalVisibility::Private),
272 is_constructor: metadata
273 .is_some_and(|metadata| metadata.callable_is_constructor()),
274 signature: declaration.signature().map(str::to_owned),
275 parameter_types,
276 return_type: metadata
277 .and_then(|metadata| metadata.return_type_identity())
278 .cloned(),
279 source_path: source_path.to_path_buf(),
280 });
281 }
282 CodeUnitType::Module | CodeUnitType::FileScope => {}
283 }
284 }
285
286 CppExternalDeclarationSet {
287 types,
288 members,
289 completeness,
290 diagnostics,
291 }
292}
293
294fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
295 let mut current = root.descendant_for_byte_range(start_byte, start_byte);
296 while let Some(node) = current {
297 let Some(parent) = node.parent() else {
298 break;
299 };
300 if parent.kind() == "field_declaration_list" {
301 let default = match parent.parent().map(|owner| owner.kind()) {
302 Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
303 _ => CppExternalVisibility::Private,
304 };
305 let mut visibility = default;
306 let mut cursor = parent.walk();
307 for child in parent.named_children(&mut cursor) {
308 if child.start_byte() > start_byte {
309 break;
310 }
311 if child.kind() == "access_specifier" {
312 visibility = match node_text(child, source).trim_end_matches(':').trim() {
313 "public" => CppExternalVisibility::Public,
314 "protected" => CppExternalVisibility::Protected,
315 "private" => CppExternalVisibility::Private,
316 _ => CppExternalVisibility::Private,
317 };
318 }
319 }
320 return visibility;
321 }
322 current = Some(parent);
323 }
324 CppExternalVisibility::Public
325}
326
327fn nearest_type_owner(
328 declaration: &CodeUnit,
329 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
330) -> Option<String> {
331 let mut current = declaration;
332 while let Some(parent) = parent_by_child.get(current) {
333 if parent.kind() == CodeUnitType::Class {
334 return Some(parent.fq_name());
335 }
336 current = parent;
337 }
338 None
339}
340
341fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
342 let mut stack = vec![root];
343 while let Some(node) = stack.pop() {
344 if matches!(
345 node.kind(),
346 "preproc_def"
347 | "preproc_function_def"
348 | "preproc_if"
349 | "preproc_ifdef"
350 | "preproc_ifndef"
351 | "preproc_elif"
352 | "preproc_else"
353 ) {
354 return true;
355 }
356 let mut cursor = node.walk();
357 stack.extend(node.named_children(&mut cursor));
358 }
359 false
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 fn extract(source: &str) -> CppExternalDeclarationSet {
367 let temp = tempfile::tempdir().expect("temp root");
368 extract_external_declarations(
369 temp.path(),
370 Path::new("vector"),
371 source,
372 CppExternalDeclarationLimits::default(),
373 )
374 }
375
376 #[test]
377 fn extracts_only_literal_angle_include_paths() {
378 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
379 assert_eq!(
380 vec![PathBuf::from("vector")],
381 external_angle_include_paths(source)
382 );
383 let mut parser = Parser::new();
384 parser
385 .set_language(&tree_sitter_cpp::LANGUAGE.into())
386 .expect("C++ grammar");
387 let tree = parser.parse(source, None).expect("tree");
388 assert_eq!(
389 vec![PathBuf::from("vector")],
390 external_angle_include_paths_from_root(source, tree.root_node())
391 );
392 }
393
394 #[test]
395 fn extracts_namespaced_template_type_and_owned_members() {
396 let declarations = extract(
397 r#"
398 namespace std {
399 template <typename T> class vector : public sequence<T> {
400 public:
401 vector();
402 void push_back(const T& value);
403 T size;
404 };
405 }
406 "#,
407 );
408
409 assert_eq!(
410 CppExternalDeclarationCompleteness::Complete,
411 declarations.completeness
412 );
413 assert!(
414 declarations.types.iter().any(|record| {
415 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
416 }),
417 "{declarations:#?}"
418 );
419 assert!(
420 declarations.members.iter().any(|record| {
421 record.owner.as_deref() == Some("std.vector")
422 && record.name == "push_back"
423 && record.visibility == CppExternalVisibility::Public
424 }),
425 "{declarations:#?}"
426 );
427 assert!(
428 declarations.members.iter().any(|record| {
429 record.owner.as_deref() == Some("std.vector") && record.name == "size"
430 }),
431 "{declarations:#?}"
432 );
433 }
434
435 #[test]
436 fn keeps_same_short_names_under_distinct_owners() {
437 let declarations = extract(
438 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
439 );
440 let mut owners = declarations
441 .members
442 .iter()
443 .filter(|member| member.name == "add")
444 .filter_map(|member| member.owner.clone())
445 .collect::<Vec<_>>();
446 owners.sort();
447
448 assert_eq!(vec!["first.box", "second.box"], owners);
449 assert!(
450 declarations
451 .members
452 .iter()
453 .filter(|member| member.name == "add")
454 .all(|member| member.visibility == CppExternalVisibility::Private)
455 );
456 }
457
458 #[test]
459 fn nested_type_visibility_follows_the_enclosing_access_section() {
460 let declarations = extract(
461 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
462 );
463 assert!(declarations.types.iter().any(|record| {
464 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
465 }));
466 assert!(declarations.types.iter().any(|record| {
467 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
468 }));
469 assert!(declarations.types.iter().any(|record| {
470 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
471 }));
472 }
473
474 #[test]
475 fn preprocessor_and_record_limits_make_the_surface_partial() {
476 let temp = tempfile::tempdir().expect("temp root");
477 let declarations = extract_external_declarations(
478 temp.path(),
479 Path::new("limited.hpp"),
480 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
481 CppExternalDeclarationLimits { max_records: 1 },
482 );
483
484 assert_eq!(
485 CppExternalDeclarationCompleteness::Partial,
486 declarations.completeness
487 );
488 assert!(
489 declarations
490 .diagnostics
491 .iter()
492 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
493 );
494 assert!(
495 declarations
496 .diagnostics
497 .iter()
498 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
499 );
500 }
501}