Skip to main content

brokk_bifrost_cpp/
clones.rs

1//! Token and AST-label normalization for C++ structural-clone candidates.
2//!
3//! `CloneCandidateData` and `compact_clone_excerpt` are analysis-owned and the
4//! declaration source comes from the analyzer, so `analyzer/cpp/clones.rs` keeps
5//! the ~12-LOC entry point; everything that knows C++ is here.
6
7use brokk_bifrost_core::analyzer::tree_walk::push_children_reversed;
8use brokk_bifrost_core::clones::{
9    CloneNormalizationRules, normalize_clone_ast_label as normalize_cpp_clone_ast_label,
10    normalize_clone_leaf_token as normalize_cpp_clone_leaf_token,
11};
12use tree_sitter::{Node, Parser};
13
14const CPP_CLONE_AST_IDENTIFIER_TYPES: &[&str] = &[
15    "identifier",
16    "field_identifier",
17    "namespace_identifier",
18    "type_identifier",
19];
20const CPP_CLONE_AST_STRING_TYPES: &[&str] = &["string_literal", "raw_string_literal"];
21const CPP_CLONE_AST_NUMBER_TYPES: &[&str] = &["number_literal"];
22const CPP_CLONE_NORMALIZATION_RULES: CloneNormalizationRules = CloneNormalizationRules {
23    identifier_kinds: CPP_CLONE_AST_IDENTIFIER_TYPES,
24    string_kinds: CPP_CLONE_AST_STRING_TYPES,
25    number_kinds: CPP_CLONE_AST_NUMBER_TYPES,
26    boolean_kinds: &[],
27    boolean_values: &["true", "false"],
28    null_kinds: &[],
29    null_values: &[],
30    comment_kinds: &["comment"],
31    ignored_ast_kinds: &[],
32};
33
34pub fn cpp_clone_parser() -> Parser {
35    let mut parser = Parser::new();
36    parser
37        .set_language(&tree_sitter_cpp::LANGUAGE.into())
38        .expect("failed to load cpp parser");
39    parser
40}
41
42/// The normalized token stream and the joined AST-label signature for one C++
43/// declaration body, in a single walk.
44pub fn cpp_clone_profile(parser: &mut Parser, source: &str) -> (Vec<String>, String) {
45    let Some(tree) = parser.parse(source, None) else {
46        return (Vec::new(), String::new());
47    };
48    let mut normalized_tokens = Vec::new();
49    let mut ast_labels = Vec::new();
50    let mut stack = vec![tree.root_node()];
51    while let Some(node) = stack.pop() {
52        if cpp_is_ignorable_clone_logging_node(node, source) {
53            continue;
54        }
55        ast_labels.push(normalize_cpp_clone_ast_label(
56            node,
57            source,
58            CPP_CLONE_NORMALIZATION_RULES,
59        ));
60        if node.named_child_count() == 0 {
61            let token = normalize_cpp_clone_leaf_token(node, source, CPP_CLONE_NORMALIZATION_RULES);
62            if !token.is_empty() {
63                normalized_tokens.push(token);
64            }
65        }
66        push_children_reversed(node, &mut stack);
67    }
68    (normalized_tokens, ast_labels.join("|"))
69}
70
71fn cpp_is_ignorable_clone_logging_node(node: Node<'_>, source: &str) -> bool {
72    if node.kind() != "expression_statement" {
73        return false;
74    }
75    let Some(mut expression) = node.named_child(0) else {
76        return false;
77    };
78    if expression.kind() == "call_expression" {
79        return expression
80            .child_by_field_name("function")
81            .is_some_and(|function| {
82                function.kind() == "identifier"
83                    && function.utf8_text(source.as_bytes()).expect("source node") == "printf"
84            });
85    }
86    if expression.kind() != "binary_expression" {
87        return false;
88    }
89    // Stream insertion is left associative. Only a chain rooted at a logging
90    // stream is ignorable; a sink mentioned in an argument is ordinary work.
91    while expression.kind() == "binary_expression" {
92        if expression
93            .child_by_field_name("operator")
94            .is_none_or(|operator| operator.kind() != "<<")
95        {
96            return false;
97        }
98        expression = expression
99            .child_by_field_name("left")
100            .expect("binary expression has a left operand");
101    }
102    expression.kind() == "qualified_identifier"
103        && expression
104            .child_by_field_name("scope")
105            .is_some_and(|scope| {
106                scope.kind() == "namespace_identifier"
107                    && scope.utf8_text(source.as_bytes()).expect("source node") == "std"
108            })
109        && expression.child_by_field_name("name").is_some_and(|name| {
110            name.kind() == "identifier"
111                && matches!(
112                    name.utf8_text(source.as_bytes()).expect("source node"),
113                    "cout" | "cerr" | "clog"
114                )
115        })
116}
117
118#[cfg(test)]
119mod tests {
120    mod clone_properties {
121        use brokk_bifrost_test_macros::hardwon;
122        use proptest::prelude::*;
123
124        // Oracle: consistent identifier/literal renaming and inserting standalone
125        // printf/std::{cout,cerr,clog} statements preserve the clone class. Adding
126        // a branch or a non-logging call changes it, even when a string argument
127        // spells a sink or a sink call is nested inside the new call.
128        proptest! {
129            #![proptest_config(ProptestConfig::with_cases(32))]
130            #[test]
131            #[hardwon("d4f00f8567e4: ordinary calls containing stream names disappeared from clone profiles")]
132            fn clone_edits_obey_declared_equivalence_classes(value in 1u16..1000) {
133                let mut parser = crate::clones::cpp_clone_parser();
134                let original = "int sum(int n) { int total = 1; for (int i = 0; i < n; ++i) { total += i; } return total; }";
135                let renamed = format!("int count(int limit) {{ int result = {value}; for (int index = 0; index < limit; ++index) {{ result += index; }} return result; }}");
136                let profile = crate::clones::cpp_clone_profile(&mut parser, original);
137                prop_assert_eq!(&profile, &crate::clones::cpp_clone_profile(&mut parser, &renamed));
138                for log in ["printf(\"progress\");", "std::cout << total;", "std::cerr << total;", "std::clog << total;"] {
139                    let edited = original.replace("return total;", &format!("{log} return total;"));
140                    prop_assert_eq!(&profile, &crate::clones::cpp_clone_profile(&mut parser, &edited), "log: {}", log);
141                }
142                for work in ["if (n) { ++total; }", "consume(total);", "consume(\"std::cout\");", "consume(\"std::cerr\");", "consume(\"std::clog\");", "consume(\"printf(\");", "consume(printf(\"progress\"));", "consume(std::cout);"] {
143                    let edited = original.replace("return total;", &format!("{work} return total;"));
144                    prop_assert_ne!(&profile, &crate::clones::cpp_clone_profile(&mut parser, &edited), "work: {}", work);
145                }
146            }
147        }
148    }
149}