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