brokk_bifrost_cpp/
diagnostics.rs1use 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}