use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use cha_core::SourceModel;
pub fn enrich_c_oop(models: &mut [(PathBuf, SourceModel)]) {
if !models.iter().any(|(_, m)| is_c_like(m)) {
return;
}
let index = build_index(models);
let attributions = attribute_methods(models, &index);
let exported_in_headers = collect_header_exports(models);
write_back(models, &attributions, &exported_in_headers);
}
pub fn attribute_methods_by_name(
models: &[(PathBuf, SourceModel)],
) -> HashMap<String, Vec<(PathBuf, String, bool)>> {
let mut out: HashMap<String, Vec<(PathBuf, String, bool)>> = HashMap::new();
if !models.iter().any(|(_, m)| is_c_like(m)) {
return out;
}
let index = build_index(models);
for (path, model) in models {
if !is_c_like(model) {
continue;
}
for f in &model.functions {
if let Some(key) = attribute_one(f, &index) {
out.entry(key)
.or_default()
.push((path.clone(), f.name.clone(), f.is_exported));
}
}
}
out
}
type ClassKey = String;
struct Index {
type_to_class: HashMap<Vec<String>, ClassKey>,
prefix_to_owners: HashMap<Vec<String>, HashSet<ClassKey>>,
parent_of: HashMap<ClassKey, ClassKey>,
}
fn build_index(models: &[(PathBuf, SourceModel)]) -> Index {
let mut type_to_class: HashMap<Vec<String>, ClassKey> = HashMap::new();
let mut prefix_to_owners: HashMap<Vec<String>, HashSet<ClassKey>> = HashMap::new();
register_structs(models, &mut type_to_class, &mut prefix_to_owners);
register_aliases(models, &mut type_to_class, &mut prefix_to_owners);
let parent_of = build_parent_map(models, &type_to_class);
Index {
type_to_class,
prefix_to_owners,
parent_of,
}
}
fn register_structs(
models: &[(PathBuf, SourceModel)],
type_to_class: &mut HashMap<Vec<String>, ClassKey>,
prefix_to_owners: &mut HashMap<Vec<String>, HashSet<ClassKey>>,
) {
for (_, model) in models {
if !is_c_like(model) {
continue;
}
for c in &model.classes {
let tokens = tokenize(&c.name);
if tokens.is_empty() {
continue;
}
type_to_class
.entry(tokens.clone())
.or_insert(c.name.clone());
for prefix in candidate_prefixes(&tokens) {
prefix_to_owners
.entry(prefix)
.or_default()
.insert(c.name.clone());
}
}
}
}
fn register_aliases(
models: &[(PathBuf, SourceModel)],
type_to_class: &mut HashMap<Vec<String>, ClassKey>,
prefix_to_owners: &mut HashMap<Vec<String>, HashSet<ClassKey>>,
) {
for (_, model) in models {
if !is_c_like(model) {
continue;
}
for (alias, original) in &model.type_aliases {
let alias_tokens = tokenize(alias);
let original_tokens = tokenize(original);
if alias_tokens.is_empty() {
continue;
}
let Some(key) = type_to_class
.get(&original_tokens)
.or_else(|| type_to_class.get(&alias_tokens))
.cloned()
else {
continue;
};
type_to_class
.entry(alias_tokens.clone())
.or_insert(key.clone());
for prefix in candidate_prefixes(&alias_tokens) {
prefix_to_owners
.entry(prefix)
.or_default()
.insert(key.clone());
}
}
}
}
fn build_parent_map(
models: &[(PathBuf, SourceModel)],
type_to_class: &HashMap<Vec<String>, ClassKey>,
) -> HashMap<ClassKey, ClassKey> {
let mut parent_of: HashMap<ClassKey, ClassKey> = HashMap::new();
for (_, model) in models {
if !is_c_like(model) {
continue;
}
for c in &model.classes {
let Some(parent_raw) = c.parent_name.as_deref() else {
continue;
};
let parent_tokens = tokenize(parent_raw);
if parent_tokens.is_empty() {
continue;
}
let Some(parent_key) = type_to_class.get(&parent_tokens) else {
continue;
};
if *parent_key == c.name {
continue; }
parent_of
.entry(c.name.clone())
.or_insert_with(|| parent_key.clone());
}
}
parent_of
}
fn attribute_methods(models: &[(PathBuf, SourceModel)], index: &Index) -> HashMap<ClassKey, usize> {
let mut counts: HashMap<ClassKey, usize> = HashMap::new();
for (_, model) in models {
if !is_c_like(model) {
continue;
}
for f in &model.functions {
let Some(key) = attribute_one(f, index) else {
continue;
};
*counts.entry(key).or_default() += 1;
}
}
counts
}
fn attribute_one(f: &cha_core::FunctionInfo, index: &Index) -> Option<ClassKey> {
let first = f.parameter_types.first()?;
let bare = normalize_type_raw(&first.raw);
let param_tokens = tokenize(&bare);
if param_tokens.is_empty() {
return None;
}
let target = index.type_to_class.get(¶m_tokens)?;
let fn_tokens = tokenize(&f.name);
let owners = longest_prefix_owners(&fn_tokens, &index.prefix_to_owners)?;
if owners.contains(target) {
return Some(target.clone());
}
owners
.iter()
.find(|owner| ancestor_chain(owner, &index.parent_of).contains(target.as_str()))
.cloned()
}
fn longest_prefix_owners<'a>(
fn_tokens: &[String],
index: &'a HashMap<Vec<String>, HashSet<ClassKey>>,
) -> Option<&'a HashSet<ClassKey>> {
(1..=fn_tokens.len())
.rev()
.find_map(|len| index.get(&fn_tokens[..len].to_vec()))
}
fn ancestor_chain<'a>(
owner: &ClassKey,
parent_of: &'a HashMap<ClassKey, ClassKey>,
) -> HashSet<&'a str> {
let mut seen = HashSet::new();
let mut cur = parent_of.get(owner);
let mut depth = 0;
while let Some(p) = cur {
if !seen.insert(p.as_str()) {
break; }
depth += 1;
if depth > 32 {
break;
}
cur = parent_of.get(p);
}
seen
}
fn collect_header_exports(models: &[(PathBuf, SourceModel)]) -> HashSet<String> {
let mut set = HashSet::new();
for (path, model) in models {
if !is_c_like(model) {
continue;
}
if !is_header_path(path) {
continue;
}
for f in &model.functions {
set.insert(f.name.clone());
}
}
set
}
fn write_back(
models: &mut [(PathBuf, SourceModel)],
attributions: &HashMap<ClassKey, usize>,
exported_in_headers: &HashSet<String>,
) {
for (path, model) in models.iter_mut() {
if !is_c_like(model) {
continue;
}
if !is_header_path(path) {
tighten_exports(model, exported_in_headers);
}
apply_attributions(model, attributions);
}
}
fn tighten_exports(model: &mut SourceModel, exported_in_headers: &HashSet<String>) {
for f in &mut model.functions {
if f.is_exported && !exported_in_headers.contains(&f.name) {
f.is_exported = false;
}
}
}
fn apply_attributions(model: &mut SourceModel, attributions: &HashMap<ClassKey, usize>) {
for c in &mut model.classes {
if let Some(&added) = attributions.get(&c.name) {
c.method_count += added;
c.has_behavior = true;
}
}
}
pub(crate) fn tokenize(name: &str) -> Vec<String> {
let mut tokens = Vec::new();
for segment in name.split('_') {
if segment.is_empty() {
continue;
}
split_case(segment, &mut tokens);
}
tokens
}
fn split_case(segment: &str, out: &mut Vec<String>) {
let chars: Vec<char> = segment.chars().collect();
let mut cur = String::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_ascii_uppercase() {
let run_start = i;
while i < chars.len() && chars[i].is_ascii_uppercase() {
i += 1;
}
emit_uppercase_run(&chars, run_start, i, &mut cur, out);
} else {
cur.push(c.to_ascii_lowercase());
i += 1;
}
}
flush(&mut cur, out);
}
fn emit_uppercase_run(
chars: &[char],
start: usize,
end: usize,
cur: &mut String,
out: &mut Vec<String>,
) {
let run_len = end - start;
let followed_by_lower = end < chars.len() && chars[end].is_ascii_lowercase();
flush(cur, out);
if run_len > 1 && followed_by_lower {
for c in &chars[start..end - 1] {
cur.push(c.to_ascii_lowercase());
}
flush(cur, out);
cur.push(chars[end - 1].to_ascii_lowercase());
} else if run_len == 1 {
cur.push(chars[start].to_ascii_lowercase());
} else {
for c in &chars[start..end] {
cur.push(c.to_ascii_lowercase());
}
flush(cur, out);
}
}
fn flush(cur: &mut String, out: &mut Vec<String>) {
if !cur.is_empty() {
out.push(std::mem::take(cur));
}
}
fn candidate_prefixes(tokens: &[String]) -> Vec<Vec<String>> {
(1..=tokens.len()).map(|n| tokens[..n].to_vec()).collect()
}
fn normalize_type_raw(raw: &str) -> String {
let mut s = raw.to_string();
for kw in &[
"const ",
"volatile ",
"static ",
"restrict ",
"struct ",
"union ",
"enum ",
] {
while let Some(pos) = s.find(kw) {
s.replace_range(pos..pos + kw.len(), "");
}
}
s.chars()
.filter(|c| !matches!(c, '*' | '&') && !c.is_whitespace())
.collect()
}
fn is_c_like(model: &SourceModel) -> bool {
matches!(model.language.as_str(), "c" | "cpp")
}
fn is_header_path(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| matches!(e, "h" | "hxx" | "hpp"))
}
#[cfg(test)]
mod tests;