1use crate::compile_context::CppCompileContext;
21use crate::graph_support::CppSource;
22use brokk_bifrost_core::analyzer::model::{
23 SemanticAbsenceProof, SemanticDiagnostic, SemanticDiagnosticDomain,
24 SemanticDiagnosticIncompleteReason, SemanticDiagnosticReport,
25};
26use brokk_bifrost_core::analyzer::semantic_diagnostics::{node_range, node_text};
27use brokk_bifrost_core::analyzer::structural::resolution::BoundaryStatus;
28use brokk_bifrost_core::analyzer::tree_walk::collect_parse_errors;
29use brokk_bifrost_core::analyzer::{ProjectFile, Range};
30use brokk_bifrost_core::hash::{HashMap, HashSet};
31use brokk_bifrost_core::path_utils::rel_path_string;
32use brokk_bifrost_core::text_utils::compute_line_starts;
33use std::path::Path;
34use tree_sitter::{Node, Parser, Tree};
35
36pub const CPP_UNRECOGNIZED_SYMBOL: &str = "cpp_unrecognized_symbol";
37pub const CPP_SEMANTIC_DIAGNOSTIC_SOURCE: &str = "bifrost-cpp";
38const MAX_CPP_SEMANTIC_DIAGNOSTIC_BYTES: usize = 512 * 1024;
39const MAX_CPP_SEMANTIC_DIAGNOSTICS: usize = 200;
40
41pub fn collect_cpp_semantic_diagnostics(
42 analyzer: &dyn CppSource,
43 file: &ProjectFile,
44 source: &str,
45) -> SemanticDiagnosticReport {
46 let mut report = SemanticDiagnosticReport::new();
47 if source.len() > MAX_CPP_SEMANTIC_DIAGNOSTIC_BYTES {
48 report.push_incomplete(None, vec![SemanticDiagnosticIncompleteReason::Truncated]);
49 return report;
50 }
51
52 let tree = parse_cpp_tree(source);
53 if has_parse_errors(tree.root_node()) {
54 report.push_incomplete(
58 None,
59 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
60 detail: "C++ source has parse errors".to_string(),
61 }],
62 );
63 return report;
64 }
65
66 let contexts = analyzer.compile_contexts_for(file);
70 if contexts.is_empty() {
71 report.push_incomplete(
72 None,
73 vec![
74 SemanticDiagnosticIncompleteReason::MissingDependencyDiscovery {
75 boundary: BoundaryStatus::ExternalUnknown,
76 },
77 ],
78 );
79 return report;
80 }
81
82 let mut closures = Vec::with_capacity(contexts.len());
83 for context in contexts {
84 match prove_include_closure(file, source, context) {
85 Ok(closure) => closures.push((context, closure)),
86 Err(reason) => {
90 report.push_incomplete(None, vec![reason]);
91 return report;
92 }
93 }
94 }
95
96 let line_starts = compute_line_starts(source);
97 let mut stack = vec![tree.root_node()];
98 while let Some(node) = stack.pop() {
99 if node.kind() == "type_identifier" && is_plain_type_reference(node) {
100 let name = node_text(node, source);
101 if !name.is_empty() {
102 if report.diagnostics().len() >= MAX_CPP_SEMANTIC_DIAGNOSTICS {
103 report
104 .push_incomplete(None, vec![SemanticDiagnosticIncompleteReason::Truncated]);
105 break;
106 }
107 record_type_reference(&mut report, &closures, name, node_range(node, &line_starts));
108 }
109 }
110 push_named_children(&mut stack, node);
111 }
112 report
113}
114
115fn record_type_reference(
118 report: &mut SemanticDiagnosticReport,
119 closures: &[(&CppCompileContext, ProvenClosure)],
120 name: &str,
121 range: Range,
122) {
123 let mut resolutions = closures
124 .iter()
125 .map(|(context, closure)| closure.resolve(context, name));
126 let first = resolutions.next().expect("at least one proven closure");
127 if resolutions.any(|resolution| resolution != first) {
128 report.push_incomplete(
131 Some(range),
132 vec![SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
133 detail: format!("the compile commands for this file disagree about type `{name}`"),
134 }],
135 );
136 return;
137 }
138
139 match first {
140 NameResolution::CommandLineMacro => {
141 report.push_incomplete(
144 Some(range),
145 vec![
146 SemanticDiagnosticIncompleteReason::UnsupportedGeneratedSurface {
147 detail: format!(
148 "type name `{name}` is defined as a compile-command macro (-D{name})"
149 ),
150 },
151 ],
152 );
153 }
154 NameResolution::Declared { definition_sites } if definition_sites > 1 => {
155 report.push_ambiguous(
159 range,
160 vec![BoundaryStatus::WorkspaceLocal; definition_sites],
161 );
162 }
163 NameResolution::Declared { .. } => {
164 report.push_resolved(range, BoundaryStatus::WorkspaceLocal);
165 }
166 NameResolution::Absent => {
167 report.push_absent(
170 SemanticAbsenceProof {
171 range,
172 domain: SemanticDiagnosticDomain::Type {
173 name: name.to_string(),
174 },
175 boundary: BoundaryStatus::WorkspaceLocal,
176 },
177 SemanticDiagnostic {
178 range,
179 source: CPP_SEMANTIC_DIAGNOSTIC_SOURCE,
180 kind: CPP_UNRECOGNIZED_SYMBOL,
181 message: format!("Unrecognized C++ type `{name}`"),
182 },
183 );
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190enum NameResolution {
191 CommandLineMacro,
194 Absent,
196 Declared { definition_sites: usize },
200}
201
202#[derive(Debug, Default)]
204struct ProvenClosure {
205 declared: HashSet<String>,
208 definition_sites: HashMap<String, usize>,
211}
212
213impl ProvenClosure {
214 fn resolve(&self, context: &CppCompileContext, name: &str) -> NameResolution {
215 if context.defined_macros.contains(name) {
216 return NameResolution::CommandLineMacro;
217 }
218 if !self.declared.contains(name) {
219 return NameResolution::Absent;
220 }
221 NameResolution::Declared {
222 definition_sites: self.definition_sites.get(name).copied().unwrap_or(0),
223 }
224 }
225}
226
227fn parse_cpp_tree(source: &str) -> Tree {
228 let mut parser = Parser::new();
229 parser
230 .set_language(&tree_sitter_cpp::LANGUAGE.into())
231 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
232 parser.parse(source, None).expect("uncancelled C++ parse")
235}
236
237fn has_parse_errors(root: Node<'_>) -> bool {
238 let mut errors = Vec::new();
239 collect_parse_errors(root, &mut errors);
240 !errors.is_empty()
241}
242
243fn prove_include_closure(
249 source_file: &ProjectFile,
250 source: &str,
251 context: &CppCompileContext,
252) -> Result<ProvenClosure, SemanticDiagnosticIncompleteReason> {
253 if let Some(forced) = context.forced_includes.first() {
257 return Err(SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
258 detail: format!(
259 "the compile command forces `-include {}`, whose declarations this pass cannot read",
260 forced.display()
261 ),
262 });
263 }
264 if let Some(root) = context.system_include_roots.first() {
265 return Err(SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
266 detail: format!(
267 "the compile command adds system include root `{}`, whose declarations this pass cannot read",
268 root.display()
269 ),
270 });
271 }
272
273 let mut closure = ProvenClosure::default();
274 let mut visited = HashSet::default();
275 let mut pending = vec![(source_file.clone(), source.to_string())];
276 while let Some((file, source)) = pending.pop() {
277 if !visited.insert(file.abs_path()) {
278 continue;
279 }
280 let tree = parse_cpp_tree(&source);
281 if has_parse_errors(tree.root_node()) {
282 debug_assert_ne!(
283 file.abs_path(),
284 source_file.abs_path(),
285 "the entry file's parse errors are reported before the closure walk starts"
286 );
287 return Err(SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
288 detail: format!(
289 "included header `{}` has parse errors",
290 rel_path_string(&file)
291 ),
292 });
293 }
294 let mut stack = vec![tree.root_node()];
295 while let Some(node) = stack.pop() {
296 match node.kind() {
297 "preproc_include" => {
298 let Some(include) = quoted_include_path(node, &source) else {
299 return Err(
302 SemanticDiagnosticIncompleteReason::MissingDependencyDiscovery {
303 boundary: BoundaryStatus::ExternalDeclaredUnindexed,
304 },
305 );
306 };
307 let header = resolve_project_header(&file, &include, context)?;
308 let Ok(header_source) = header.read_to_string() else {
309 return Err(
310 SemanticDiagnosticIncompleteReason::MissingDependencyDiscovery {
311 boundary: BoundaryStatus::ExternalDeclaredUnindexed,
312 },
313 );
314 };
315 pending.push((header, header_source));
316 }
317 "preproc_def" | "preproc_function_def" => {
318 let name = node
322 .child_by_field_name("name")
323 .map(|name| node_text(name, &source))
324 .unwrap_or_default();
325 return Err(
326 SemanticDiagnosticIncompleteReason::UnsupportedGeneratedSurface {
327 detail: format!(
328 "`#define {name}` in `{}` can generate type names this pass does not expand",
329 rel_path_string(&file)
330 ),
331 },
332 );
333 }
334 "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif"
335 | "preproc_else" => {
336 return Err(SemanticDiagnosticIncompleteReason::DynamicBehavior {
341 detail: format!(
342 "conditional compilation `{}` in `{}` selects declarations this pass does not evaluate",
343 directive_keyword(node, &source),
344 rel_path_string(&file)
345 ),
346 });
347 }
348 kind if kind.starts_with("preproc_") => {
349 return Err(SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
353 detail: format!(
354 "unsupported preprocessor directive `{}` in `{}`",
355 directive_keyword(node, &source),
356 rel_path_string(&file)
357 ),
358 });
359 }
360 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier" => {
361 if let Some(name) = declared_type_name(node, &source) {
362 if node.child_by_field_name("body").is_some() {
363 *closure.definition_sites.entry(name.clone()).or_default() += 1;
364 }
365 closure.declared.insert(name);
366 }
367 push_named_children(&mut stack, node);
368 }
369 "type_definition" | "alias_declaration" => {
370 for name in alias_type_names(node, &source) {
373 closure.declared.insert(name);
374 }
375 push_named_children(&mut stack, node);
376 }
377 _ => push_named_children(&mut stack, node),
378 }
379 }
380 }
381 Ok(closure)
382}
383
384fn declared_type_name(node: Node<'_>, source: &str) -> Option<String> {
385 let name = node.child_by_field_name("name").or_else(|| {
386 let mut cursor = node.walk();
387 node.named_children(&mut cursor)
388 .find(|child| matches!(child.kind(), "type_identifier" | "identifier"))
389 })?;
390 let name = node_text(name, source).trim();
391 (!name.is_empty()).then(|| name.to_string())
392}
393
394fn alias_type_names(node: Node<'_>, source: &str) -> Vec<String> {
399 let mut names = Vec::new();
400 let mut stack: Vec<Node<'_>> = node
401 .child_by_field_name("name")
402 .into_iter()
403 .chain({
404 let mut cursor = node.walk();
405 node.children_by_field_name("declarator", &mut cursor)
406 .collect::<Vec<_>>()
407 })
408 .collect();
409 while let Some(current) = stack.pop() {
412 if current.kind() == "type_identifier" {
413 let name = node_text(current, source).trim();
414 if !name.is_empty() {
415 names.push(name.to_string());
416 }
417 continue;
418 }
419 push_named_children(&mut stack, current);
420 }
421 names
422}
423
424fn directive_keyword(node: Node<'_>, source: &str) -> String {
426 node.child(0)
427 .map(|token| node_text(token, source).trim().to_string())
428 .filter(|token| !token.is_empty())
429 .unwrap_or_else(|| node.kind().to_string())
430}
431
432fn quoted_include_path(node: Node<'_>, source: &str) -> Option<String> {
433 let mut cursor = node.walk();
434 let literal = node
435 .named_children(&mut cursor)
436 .find(|child| child.kind() == "string_literal")?;
437 let text = node_text(literal, source);
438 text.strip_prefix('"')?
439 .strip_suffix('"')
440 .filter(|path| !path.is_empty())
441 .map(ToOwned::to_owned)
442}
443
444fn resolve_project_header(
445 source_file: &ProjectFile,
446 include: &str,
447 context: &CppCompileContext,
448) -> Result<ProjectFile, SemanticDiagnosticIncompleteReason> {
449 let mut candidates = HashSet::default();
450 if let Some(source_parent) = source_file.abs_path().parent().map(Path::to_path_buf) {
451 for root in
452 std::iter::once(source_parent).chain(context.project_include_roots.iter().cloned())
453 {
454 let candidate = root.join(include);
455 if candidate.is_file() && candidate.starts_with(source_file.root()) {
456 candidates.insert(candidate);
457 }
458 }
459 }
460 match candidates.len() {
461 0 => Err(
464 SemanticDiagnosticIncompleteReason::MissingDependencyDiscovery {
465 boundary: BoundaryStatus::ExternalDeclaredUnindexed,
466 },
467 ),
468 1 => Ok(ProjectFile::new(
469 source_file.root().to_path_buf(),
470 candidates
471 .into_iter()
472 .next()
473 .expect("one candidate")
474 .strip_prefix(source_file.root())
475 .expect("candidate inside project")
476 .to_path_buf(),
477 )),
478 _ => Err(SemanticDiagnosticIncompleteReason::UnsupportedSemantics {
481 detail: format!(
482 "`#include \"{include}\"` matches more than one project header on the include path"
483 ),
484 }),
485 }
486}
487
488fn is_plain_type_reference(node: Node<'_>) -> bool {
489 let Some(parent) = node.parent() else {
490 return false;
491 };
492 if !matches!(
493 parent.kind(),
494 "declaration" | "type_descriptor" | "sized_type_specifier"
495 ) {
496 return false;
497 }
498 let mut current = parent;
499 while let Some(ancestor) = current.parent() {
500 if matches!(
501 ancestor.kind(),
502 "class_specifier"
503 | "struct_specifier"
504 | "union_specifier"
505 | "enum_specifier"
506 | "template_declaration"
507 | "template_parameter_list"
508 | "template_type"
509 | "qualified_identifier"
510 | "scoped_type_identifier"
511 ) {
512 return false;
513 }
514 current = ancestor;
515 }
516 true
517}
518
519fn push_named_children<'tree>(stack: &mut Vec<Node<'tree>>, node: Node<'tree>) {
520 let mut cursor = node.walk();
521 let children: Vec<_> = node.named_children(&mut cursor).collect();
522 stack.extend(children.into_iter().rev());
523}