1use tree_sitter::Node;
19
20use crate::declarations::node_text;
21use brokk_bifrost_core::hash::{HashMap, HashSet};
22
23const TRIVIAL_TYPE_KINDS: &[&str] = &["primitive_type", "sized_type_specifier"];
26
27#[derive(Default)]
29struct NameFacts {
30 declarations: usize,
32 trivial_signature: bool,
35 poisoned: bool,
39}
40
41pub struct CppTemporaryFreeCallIndex<'a> {
44 source: &'a str,
45 facts: Option<HashMap<&'a str, NameFacts>>,
48 visited_nodes: usize,
50}
51
52impl<'a> CppTemporaryFreeCallIndex<'a> {
53 pub fn build(source: &'a str, root: Node<'a>) -> Self {
55 let mut facts: HashMap<&'a str, NameFacts> = HashMap::default();
56 let mut declaration_names: HashSet<usize> = HashSet::default();
57 let mut provable_file = true;
58 let mut visited_nodes = 0usize;
59 let mut stack = vec![root];
60 while let Some(node) = stack.pop() {
61 visited_nodes += 1;
62 let kind = node.kind();
63 if kind.starts_with("preproc_") {
64 provable_file = false;
67 }
68 if let Some(candidate) = free_function_candidate(node) {
69 declaration_names.insert(candidate.name.id());
70 let name = node_text(candidate.name, source);
71 let entry = facts.entry(name).or_default();
72 entry.declarations += 1;
73 entry.trivial_signature = candidate.trivial_signature;
74 }
75 if matches!(
76 kind,
77 "identifier" | "field_identifier" | "type_identifier" | "namespace_identifier"
78 ) && !declaration_names.contains(&node.id())
79 && !(kind == "identifier" && is_callee_position(node))
80 {
81 facts.entry(node_text(node, source)).or_default().poisoned = true;
82 }
83 let mut cursor = node.walk();
84 stack.extend(node.named_children(&mut cursor));
85 }
86 Self {
87 source,
88 facts: provable_file.then_some(facts),
89 visited_nodes,
90 }
91 }
92
93 pub fn visited_nodes(&self) -> usize {
95 self.visited_nodes
96 }
97
98 pub fn call_is_provably_temporary_free(&self, call: Node<'_>) -> bool {
105 debug_assert_eq!(call.kind(), "call_expression");
106 let Some(facts) = &self.facts else {
107 return false;
108 };
109 let Some(callee) = call.child_by_field_name("function") else {
110 return false;
111 };
112 if callee.kind() != "identifier" {
113 return false;
114 }
115 let Some(name) = facts.get(node_text(callee, self.source)) else {
116 return false;
117 };
118 if name.declarations != 1 || !name.trivial_signature || name.poisoned {
119 return false;
120 }
121 let Some(arguments) = call.child_by_field_name("arguments") else {
122 return false;
123 };
124 let mut cursor = arguments.walk();
125 arguments
126 .named_children(&mut cursor)
127 .all(argument_is_trivially_shaped)
128 }
129}
130
131struct FreeFunctionCandidate<'a> {
134 name: Node<'a>,
135 trivial_signature: bool,
136}
137
138fn free_function_candidate(node: Node<'_>) -> Option<FreeFunctionCandidate<'_>> {
139 if !matches!(node.kind(), "function_definition" | "declaration") {
140 return None;
141 }
142 if node.parent()?.kind() != "translation_unit" {
145 return None;
146 }
147 let type_node = node.child_by_field_name("type")?;
148 let mut declarator = node.child_by_field_name("declarator")?;
149 let mut indirect_return = false;
150 while matches!(
151 declarator.kind(),
152 "pointer_declarator" | "reference_declarator"
153 ) {
154 indirect_return = true;
155 declarator = declarator.child_by_field_name("declarator")?;
156 }
157 if declarator.kind() != "function_declarator" {
158 return None;
159 }
160 let name = declarator.child_by_field_name("declarator")?;
161 if name.kind() != "identifier" {
162 return None;
163 }
164 let trivial_return = indirect_return || TRIVIAL_TYPE_KINDS.contains(&type_node.kind());
167 let trivial_signature = trivial_return
168 && declarator
169 .child_by_field_name("parameters")
170 .is_some_and(|parameters| {
171 let mut cursor = parameters.walk();
172 parameters
173 .named_children(&mut cursor)
174 .all(parameter_is_provably_trivial)
175 });
176 Some(FreeFunctionCandidate {
177 name,
178 trivial_signature,
179 })
180}
181
182fn parameter_is_provably_trivial(parameter: Node<'_>) -> bool {
186 if parameter.kind() != "parameter_declaration" {
187 return false;
188 }
189 let Some(type_node) = parameter.child_by_field_name("type") else {
190 return false;
191 };
192 if TRIVIAL_TYPE_KINDS.contains(&type_node.kind()) {
193 return true;
194 }
195 parameter
200 .child_by_field_name("declarator")
201 .is_some_and(declarator_contains_pointer)
202}
203
204fn declarator_contains_pointer(declarator: Node<'_>) -> bool {
205 let mut stack = vec![declarator];
206 while let Some(node) = stack.pop() {
207 if matches!(
208 node.kind(),
209 "pointer_declarator" | "abstract_pointer_declarator"
210 ) {
211 return true;
212 }
213 let mut cursor = node.walk();
214 stack.extend(node.named_children(&mut cursor));
215 }
216 false
217}
218
219fn is_callee_position(node: Node<'_>) -> bool {
220 node.parent().is_some_and(|parent| {
221 parent.kind() == "call_expression"
222 && parent
223 .child_by_field_name("function")
224 .is_some_and(|function| function.id() == node.id())
225 })
226}
227
228fn argument_is_trivially_shaped(argument: Node<'_>) -> bool {
232 let mut node = argument;
233 loop {
234 match node.kind() {
235 "parenthesized_expression" => {
236 let mut cursor = node.walk();
237 let mut children = node.named_children(&mut cursor);
238 let (Some(inner), None) = (children.next(), children.next()) else {
239 return false;
240 };
241 node = inner;
242 }
243 "identifier"
244 | "number_literal"
245 | "char_literal"
246 | "string_literal"
247 | "concatenated_string"
248 | "true"
249 | "false"
250 | "null"
251 | "nullptr" => return true,
252 "call_expression" => return true,
253 _ => return false,
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use tree_sitter::{Parser, Tree};
262
263 fn parse_cpp(source: &str) -> Tree {
264 let mut parser = Parser::new();
265 parser
266 .set_language(&tree_sitter_cpp::LANGUAGE.into())
267 .expect("cpp language");
268 parser.parse(source, None).expect("cpp tree")
269 }
270
271 fn classified_calls(source: &str) -> Vec<(String, bool)> {
273 let tree = parse_cpp(source);
274 let index = CppTemporaryFreeCallIndex::build(source, tree.root_node());
275 let mut calls = Vec::new();
276 let mut stack = vec![tree.root_node()];
277 while let Some(node) = stack.pop() {
278 if node.kind() == "call_expression" {
279 calls.push((
280 node_text(node, source).to_string(),
281 index.call_is_provably_temporary_free(node),
282 ));
283 }
284 let mut cursor = node.walk();
285 stack.extend(node.named_children(&mut cursor));
286 }
287 calls.sort();
288 calls
289 }
290
291 fn assert_calls(source: &str, expected: &[(&str, bool)]) {
292 let mut expected = expected
293 .iter()
294 .map(|(text, provable)| (text.to_string(), *provable))
295 .collect::<Vec<_>>();
296 expected.sort();
297 assert_eq!(classified_calls(source), expected);
298 }
299
300 #[test]
303 fn exact_local_free_function_calls_are_provable() {
304 assert_calls(
305 r#"
306const char *dfb_source() {
307 return "tainted";
308}
309
310void dfb_sink(const char *value) {}
311
312void run() {
313 dfb_sink(dfb_source());
314}
315"#,
316 &[("dfb_source()", true), ("dfb_sink(dfb_source())", true)],
317 );
318 }
319
320 #[test]
323 fn discarded_result_and_literal_argument_are_provable() {
324 assert_calls(
325 r#"
326const char *dfb_source() {
327 return "tainted";
328}
329
330void dfb_sink(const char *value) {}
331
332void run() {
333 dfb_source();
334 dfb_sink("clean");
335}
336"#,
337 &[("dfb_source()", true), ("dfb_sink(\"clean\")", true)],
338 );
339 }
340
341 #[test]
343 fn local_prototype_is_provable() {
344 assert_calls(
345 r#"
346const char *dfb_source();
347
348void run() {
349 dfb_source();
350}
351"#,
352 &[("dfb_source()", true)],
353 );
354 }
355
356 #[test]
359 fn overloaded_functions_stay_unproven() {
360 assert_calls(
361 r#"
362const char *dfb_source() { return "a"; }
363const char *dfb_source(int selector) { return "b"; }
364
365void run() {
366 dfb_source();
367}
368"#,
369 &[("dfb_source()", false)],
370 );
371 }
372
373 #[test]
376 fn function_pointer_calls_stay_unproven() {
377 assert_calls(
378 r#"
379const char *real_source() { return "a"; }
380
381void run() {
382 const char *(*dfb_source)() = real_source;
383 dfb_source();
384}
385"#,
386 &[("dfb_source()", false)],
387 );
388 }
389
390 #[test]
393 fn virtual_member_calls_stay_unproven() {
394 assert_calls(
395 r#"
396struct Producer {
397 virtual const char *dfb_source() { return "a"; }
398};
399
400void run(Producer *producer) {
401 producer->dfb_source();
402}
403"#,
404 &[("producer->dfb_source()", false)],
405 );
406 }
407
408 #[test]
411 fn member_declaration_poisons_unqualified_calls() {
412 assert_calls(
413 r#"
414const char *dfb_source() { return "a"; }
415
416struct Wrapper {
417 const char *dfb_source() { return "b"; }
418 const char *read() { return dfb_source(); }
419};
420"#,
421 &[("dfb_source()", false)],
422 );
423 }
424
425 #[test]
428 fn template_functions_stay_unproven() {
429 assert_calls(
430 r#"
431template <typename T>
432T dfb_source() { return T(); }
433
434void run() {
435 dfb_source<const char *>();
436}
437"#,
438 &[("dfb_source<const char *>()", false), ("T()", false)],
439 );
440 }
441
442 #[test]
445 fn class_return_types_stay_unproven() {
446 assert_calls(
447 r#"
448struct Token {};
449Token dfb_source() { return Token{}; }
450
451void run() {
452 dfb_source();
453}
454"#,
455 &[("dfb_source()", false)],
456 );
457 }
458
459 #[test]
462 fn class_reference_parameters_stay_unproven() {
463 assert_calls(
464 r#"
465struct Token {};
466void dfb_sink(const Token &value) {}
467
468void run() {
469 dfb_sink(Token{});
470}
471"#,
472 &[("dfb_sink(Token{})", false)],
473 );
474 }
475
476 #[test]
478 fn default_arguments_stay_unproven() {
479 assert_calls(
480 r#"
481void dfb_sink(const char *value = "d") {}
482
483void run() {
484 dfb_sink();
485}
486"#,
487 &[("dfb_sink()", false)],
488 );
489 }
490
491 #[test]
495 fn complex_argument_expressions_stay_unproven() {
496 assert_calls(
497 r#"
498void dfb_sink(const char *value) {}
499
500void run(const char *left) {
501 dfb_sink(left + 1);
502}
503"#,
504 &[("dfb_sink(left + 1)", false)],
505 );
506 }
507
508 #[test]
511 fn preprocessor_content_makes_the_file_unprovable() {
512 assert_calls(
513 r#"
514#include <string>
515
516const char *dfb_source() { return "a"; }
517
518void run() {
519 dfb_source();
520}
521"#,
522 &[("dfb_source()", false)],
523 );
524 }
525
526 #[test]
529 fn address_taken_names_stay_unproven() {
530 assert_calls(
531 r#"
532const char *dfb_source() { return "a"; }
533
534void keep(const char *(*pointer)()) {}
535
536void run() {
537 keep(&dfb_source);
538 dfb_source();
539}
540"#,
541 &[("dfb_source()", false), ("keep(&dfb_source)", false)],
542 );
543 }
544
545 #[test]
548 fn pointer_indirection_over_class_types_is_provable() {
549 assert_calls(
550 r#"
551struct Token;
552Token *dfb_source() { return nullptr; }
553
554void dfb_sink(Token *value) {}
555
556void run() {
557 dfb_sink((dfb_source()));
558}
559"#,
560 &[("dfb_source()", true), ("dfb_sink((dfb_source()))", true)],
561 );
562 }
563}