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