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