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 tree_sitter::{Node, Parser};
9
10const CPP_CLONE_AST_IDENTIFIER_TYPES: &[&str] = &[
11    "identifier",
12    "field_identifier",
13    "namespace_identifier",
14    "type_identifier",
15];
16const CPP_CLONE_AST_STRING_TYPES: &[&str] = &["string_literal", "raw_string_literal"];
17const CPP_CLONE_AST_NUMBER_TYPES: &[&str] = &["number_literal"];
18
19pub fn cpp_clone_parser() -> Parser {
20    let mut parser = Parser::new();
21    parser
22        .set_language(&tree_sitter_cpp::LANGUAGE.into())
23        .expect("failed to load cpp parser");
24    parser
25}
26
27fn normalize_cpp_clone_leaf_token(node: Node<'_>, source: &str) -> String {
28    let kind = node.kind();
29    let token = source
30        .get(node.start_byte()..node.end_byte())
31        .unwrap_or("")
32        .trim();
33    if token.is_empty() || kind == "comment" {
34        return String::new();
35    }
36    if CPP_CLONE_AST_IDENTIFIER_TYPES.contains(&kind) {
37        return "ID".to_string();
38    }
39    if CPP_CLONE_AST_STRING_TYPES.contains(&kind) {
40        return "STR".to_string();
41    }
42    if CPP_CLONE_AST_NUMBER_TYPES.contains(&kind) {
43        return "NUM".to_string();
44    }
45    if matches!(token, "true" | "false") {
46        return "BOOL".to_string();
47    }
48    if token.chars().count() == 1 && token.chars().all(|ch| !ch.is_alphanumeric()) {
49        return format!("OP:{token}");
50    }
51    format!("T:{kind}")
52}
53
54/// The normalized token stream and the joined AST-label signature for one C++
55/// declaration body, in a single walk.
56pub fn cpp_clone_profile(parser: &mut Parser, source: &str) -> (Vec<String>, String) {
57    let Some(tree) = parser.parse(source, None) else {
58        return (Vec::new(), String::new());
59    };
60    let mut normalized_tokens = Vec::new();
61    let mut ast_labels = Vec::new();
62    let mut stack = vec![tree.root_node()];
63    while let Some(node) = stack.pop() {
64        if cpp_is_ignorable_clone_logging_node(node, source) {
65            continue;
66        }
67        ast_labels.push(normalize_cpp_clone_ast_label(node, source));
68        if node.named_child_count() == 0 {
69            let token = normalize_cpp_clone_leaf_token(node, source);
70            if !token.is_empty() {
71                normalized_tokens.push(token);
72            }
73        }
74        push_children_reversed(node, &mut stack);
75    }
76    (normalized_tokens, ast_labels.join("|"))
77}
78
79fn normalize_cpp_clone_ast_label(node: Node<'_>, source: &str) -> String {
80    let kind = node.kind();
81    let text = source
82        .get(node.start_byte()..node.end_byte())
83        .unwrap_or("")
84        .trim();
85    if CPP_CLONE_AST_IDENTIFIER_TYPES.contains(&kind) {
86        return "ID".to_string();
87    }
88    if CPP_CLONE_AST_STRING_TYPES.contains(&kind) {
89        return "STR".to_string();
90    }
91    if CPP_CLONE_AST_NUMBER_TYPES.contains(&kind) {
92        return "NUM".to_string();
93    }
94    if matches!(text, "true" | "false") {
95        return "BOOL".to_string();
96    }
97    format!("N:{kind}")
98}
99
100fn cpp_is_ignorable_clone_logging_node(node: Node<'_>, source: &str) -> bool {
101    if node.kind() != "expression_statement" {
102        return false;
103    }
104    let text = source
105        .get(node.start_byte()..node.end_byte())
106        .unwrap_or("")
107        .trim();
108    text.contains("std::cout")
109        || text.contains("std::cerr")
110        || text.contains("std::clog")
111        || text.starts_with("printf(")
112}