Skip to main content

brokk_bifrost_cpp/
diagnostics.rs

1//! C++'s semantic diagnostics: conservative unrecognized-type reporting.
2//!
3//! The pass only fires where it can prove what a translation unit sees: a
4//! `compile_commands.json` entry with no forced or system includes, an
5//! `#include` closure of quoted project headers that all parse cleanly, and no
6//! preprocessor conditionals or macro definitions anywhere in that closure. Any
7//! of those gives up and reports nothing, because the alternative is to
8//! second-guess a preprocessor this analyzer does not run.
9//!
10//! `analyzer/cpp/diagnostics.rs` in `brokk-bifrost-analysis` keeps only the
11//! fixtures that build a real `CppAnalyzer`; the `SemanticDiagnosticReport`
12//! wrapper `IAnalyzer::semantic_diagnostics` returns stays on the analyzer.
13
14use crate::compile_context::CppCompileContext;
15use crate::graph_support::CppSource;
16use brokk_bifrost_core::analyzer::ProjectFile;
17use brokk_bifrost_core::analyzer::model::SemanticDiagnostic;
18use brokk_bifrost_core::analyzer::semantic_diagnostics::{node_range, node_text};
19use brokk_bifrost_core::analyzer::tree_walk::collect_parse_errors;
20use brokk_bifrost_core::hash::HashSet;
21use brokk_bifrost_core::text_utils::compute_line_starts;
22use tree_sitter::{Node, Parser, Tree};
23
24pub const CPP_UNRECOGNIZED_SYMBOL: &str = "cpp_unrecognized_symbol";
25pub const CPP_SEMANTIC_DIAGNOSTIC_SOURCE: &str = "bifrost-cpp";
26const MAX_CPP_SEMANTIC_DIAGNOSTIC_BYTES: usize = 512 * 1024;
27const MAX_CPP_SEMANTIC_DIAGNOSTICS: usize = 200;
28
29pub fn collect_cpp_semantic_diagnostics(
30    analyzer: &dyn CppSource,
31    file: &ProjectFile,
32    source: &str,
33) -> Vec<SemanticDiagnostic> {
34    if source.len() > MAX_CPP_SEMANTIC_DIAGNOSTIC_BYTES {
35        return Vec::new();
36    }
37    let Some(context) = analyzer.compile_context_for(file) else {
38        return Vec::new();
39    };
40    let Some(tree) = parse_cpp_tree(source) else {
41        return Vec::new();
42    };
43    let Some(known_type_names) = proven_project_type_names(file, source, context) else {
44        return Vec::new();
45    };
46
47    let line_starts = compute_line_starts(source);
48    let mut diagnostics = Vec::new();
49    let mut stack = vec![tree.root_node()];
50    while let Some(node) = stack.pop() {
51        if diagnostics.len() >= MAX_CPP_SEMANTIC_DIAGNOSTICS {
52            break;
53        }
54        if node.kind() == "type_identifier" && is_plain_type_reference(node) {
55            let name = node_text(node, source);
56            if !name.is_empty()
57                && !context.defined_macros.contains(name)
58                && !known_type_names.contains(name)
59            {
60                diagnostics.push(SemanticDiagnostic {
61                    range: node_range(node, &line_starts),
62                    source: CPP_SEMANTIC_DIAGNOSTIC_SOURCE,
63                    kind: CPP_UNRECOGNIZED_SYMBOL,
64                    message: format!("Unrecognized C++ type `{name}`"),
65                });
66            }
67        }
68        push_named_children(&mut stack, node);
69    }
70    diagnostics
71}
72
73fn parse_cpp_tree(source: &str) -> Option<Tree> {
74    let mut parser = Parser::new();
75    parser
76        .set_language(&tree_sitter_cpp::LANGUAGE.into())
77        .ok()?;
78    parser.parse(source, None)
79}
80
81fn has_parse_errors(root: Node<'_>) -> bool {
82    let mut errors = Vec::new();
83    collect_parse_errors(root, &mut errors);
84    !errors.is_empty()
85}
86
87fn proven_project_type_names(
88    source_file: &ProjectFile,
89    source: &str,
90    context: &CppCompileContext,
91) -> Option<HashSet<String>> {
92    if !context.forced_includes.is_empty() || !context.system_include_roots.is_empty() {
93        return None;
94    }
95
96    let mut visited = HashSet::default();
97    let mut known_type_names = HashSet::default();
98    let mut pending = vec![(source_file.clone(), source.to_string())];
99    while let Some((file, source)) = pending.pop() {
100        if !visited.insert(file.abs_path()) {
101            continue;
102        }
103        let tree = parse_cpp_tree(&source)?;
104        if has_parse_errors(tree.root_node()) {
105            return None;
106        }
107        let mut stack = vec![tree.root_node()];
108        while let Some(node) = stack.pop() {
109            match node.kind() {
110                "preproc_include" => {
111                    let include = quoted_include_path(node, &source)?;
112                    let header = resolve_project_header(&file, &include, context)?;
113                    let Ok(header_source) = header.read_to_string() else {
114                        return None;
115                    };
116                    pending.push((header, header_source));
117                }
118                "preproc_def"
119                | "preproc_function_def"
120                | "preproc_if"
121                | "preproc_ifdef"
122                | "preproc_ifndef"
123                | "preproc_elif"
124                | "preproc_else"
125                | "preproc_call" => return None,
126                "class_specifier" | "struct_specifier" | "enum_specifier" => {
127                    if let Some(name) = declared_type_name(node, &source) {
128                        known_type_names.insert(name);
129                    }
130                    push_named_children(&mut stack, node);
131                }
132                _ => push_named_children(&mut stack, node),
133            }
134        }
135    }
136    Some(known_type_names)
137}
138
139fn declared_type_name(node: Node<'_>, source: &str) -> Option<String> {
140    let name = node.child_by_field_name("name").or_else(|| {
141        let mut cursor = node.walk();
142        node.named_children(&mut cursor)
143            .find(|child| matches!(child.kind(), "type_identifier" | "identifier"))
144    })?;
145    let name = node_text(name, source).trim();
146    (!name.is_empty()).then(|| name.to_string())
147}
148
149fn quoted_include_path(node: Node<'_>, source: &str) -> Option<String> {
150    let mut cursor = node.walk();
151    let literal = node
152        .named_children(&mut cursor)
153        .find(|child| child.kind() == "string_literal")?;
154    let text = node_text(literal, source);
155    text.strip_prefix('"')?
156        .strip_suffix('"')
157        .filter(|path| !path.is_empty())
158        .map(ToOwned::to_owned)
159}
160
161fn resolve_project_header(
162    source_file: &ProjectFile,
163    include: &str,
164    context: &CppCompileContext,
165) -> Option<ProjectFile> {
166    let mut candidates = HashSet::default();
167    let source_parent = source_file.abs_path().parent()?.to_path_buf();
168    for root in std::iter::once(source_parent).chain(context.project_include_roots.iter().cloned())
169    {
170        let candidate = root.join(include);
171        if candidate.is_file() && candidate.starts_with(source_file.root()) {
172            candidates.insert(candidate);
173        }
174    }
175    (candidates.len() == 1).then(|| {
176        ProjectFile::new(
177            source_file.root().to_path_buf(),
178            candidates
179                .into_iter()
180                .next()
181                .expect("one candidate")
182                .strip_prefix(source_file.root())
183                .expect("candidate inside project")
184                .to_path_buf(),
185        )
186    })
187}
188
189fn is_plain_type_reference(node: Node<'_>) -> bool {
190    let Some(parent) = node.parent() else {
191        return false;
192    };
193    if !matches!(
194        parent.kind(),
195        "declaration" | "type_descriptor" | "sized_type_specifier"
196    ) {
197        return false;
198    }
199    let mut current = parent;
200    while let Some(ancestor) = current.parent() {
201        if matches!(
202            ancestor.kind(),
203            "class_specifier"
204                | "struct_specifier"
205                | "enum_specifier"
206                | "template_declaration"
207                | "template_parameter_list"
208                | "template_type"
209                | "qualified_identifier"
210                | "scoped_type_identifier"
211        ) {
212            return false;
213        }
214        current = ancestor;
215    }
216    true
217}
218
219fn push_named_children<'tree>(stack: &mut Vec<Node<'tree>>, node: Node<'tree>) {
220    let mut cursor = node.walk();
221    let children: Vec<_> = node.named_children(&mut cursor).collect();
222    stack.extend(children.into_iter().rev());
223}