1use crate::declarations::{node_text as cpp_node_text, normalize_cpp_whitespace};
9use brokk_bifrost_core::analyzer::CodeUnit;
10use tree_sitter::Node;
11
12#[derive(Clone, PartialEq, Eq, Hash)]
13pub struct CppArgType {
14 pub name: String,
15 pub unit: Option<CodeUnit>,
16 pub indirection: i32,
17 pub pointee_const: bool,
18}
19
20pub fn cpp_signature_param_types(signature: &str) -> Option<Vec<String>> {
21 let inner = cpp_signature_parameter_text(signature)
22 .unwrap_or(signature)
23 .trim();
24 if inner.is_empty() || inner == "void" {
25 return Some(Vec::new());
26 }
27 Some(
28 cpp_split_top_level_commas(inner)
29 .map(cpp_parameter_type_text)
30 .collect(),
31 )
32}
33
34pub fn cpp_parameter_type_text(parameter: &str) -> String {
35 let mut text = parameter
36 .split_once('=')
37 .map(|(before, _)| before)
38 .unwrap_or(parameter)
39 .trim()
40 .trim_end_matches(';')
41 .trim();
42 let pointer_depth = cpp_type_text_pointer_depth(text);
43 if let Some((before, last)) = text.rsplit_once(char::is_whitespace)
44 && cpp_parameter_name_token(last)
45 {
46 text = before.trim();
47 }
48 let pointee_const = pointer_depth > 0 && cpp_type_text_pointee_is_const(text);
49 format!(
50 "{}{}{}",
51 if pointee_const { "const " } else { "" },
52 normalize_cpp_type_name(text),
53 "*".repeat(pointer_depth as usize)
54 )
55}
56
57pub fn normalize_cpp_type_name(text: &str) -> String {
58 let normalized = normalize_cpp_whitespace(text);
59 let base = cpp_type_text_base(&normalized)
60 .trim_start_matches("const ")
61 .trim();
62 strip_tag_type_prefix(base.strip_suffix(" const").unwrap_or(base)).to_string()
63}
64
65pub fn cpp_type_text_pointer_depth(text: &str) -> i32 {
66 cpp_type_text_shape(text).1
67}
68
69fn cpp_type_text_shape(text: &str) -> (usize, i32) {
70 let mut depth = 0i32;
71 let mut nesting = 0i32;
72 let mut base_end = text.len();
73 for (offset, ch) in text.char_indices() {
74 match ch {
75 '<' | '(' | '[' => nesting += 1,
76 '>' | ')' | ']' => nesting -= 1,
77 '*' if nesting <= 0 => {
78 base_end = base_end.min(offset);
79 depth += 1;
80 }
81 '&' if nesting <= 0 => base_end = base_end.min(offset),
82 _ => {}
83 }
84 }
85 (base_end, depth)
86}
87
88pub fn cpp_literal_arg_type(node: Node<'_>, source: &str) -> Option<CppArgType> {
89 let scalar = |name: &str| CppArgType {
90 name: name.to_string(),
91 unit: None,
92 indirection: 0,
93 pointee_const: false,
94 };
95 match node.kind() {
96 "number_literal" => {
97 let text = cpp_node_text(node, source);
98 if cpp_number_literal_is_float(text) {
99 Some(scalar("double"))
100 } else {
101 Some(scalar("int"))
102 }
103 }
104 "true" | "false" => Some(scalar("bool")),
105 "char_literal" => Some(scalar("char")),
106 "string_literal" => {
107 let text = cpp_node_text(node, source).trim_start();
108 (text.starts_with('"') || text.starts_with("R\"")).then(|| CppArgType {
109 name: "char".to_string(),
110 unit: None,
111 indirection: 1,
112 pointee_const: true,
113 })
114 }
115 "unary_expression" => {
116 let operator = node.child_by_field_name("operator")?;
117 let inner = node
118 .child_by_field_name("argument")
119 .or_else(|| node.named_child(0))?;
120 matches!(operator.kind(), "+" | "-")
121 .then(|| cpp_literal_arg_type(inner, source))
122 .flatten()
123 }
124 _ => None,
125 }
126}
127
128pub fn cpp_filter_candidates_by_args(
129 candidates: Vec<CodeUnit>,
130 arg_types: &[Option<CppArgType>],
131 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
132 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
133) -> Vec<CodeUnit> {
134 if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
135 return candidates;
136 }
137
138 let filtered: Vec<_> = candidates
139 .iter()
140 .filter(|candidate| {
141 cpp_signature_param_types(candidate.signature().unwrap_or_default()).is_some_and(
142 |params| {
143 params.len() == arg_types.len()
144 && params.iter().zip(arg_types.iter()).all(|(param, arg)| {
145 cpp_param_matches_arg(param, arg, resolve_type, assignable)
146 })
147 },
148 )
149 })
150 .cloned()
151 .collect();
152 if filtered.is_empty() {
153 candidates
154 } else {
155 filtered
156 }
157}
158
159fn cpp_param_matches_arg(
160 param: &str,
161 arg: &Option<CppArgType>,
162 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
163 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
164) -> bool {
165 let Some(arg) = arg else {
166 return false;
167 };
168 if cpp_type_text_pointer_depth(param) != arg.indirection {
169 return false;
170 }
171 if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
172 return false;
173 }
174 let param_name = normalize_cpp_type_name(param);
175 match (resolve_type(¶m_name), arg.unit.as_ref()) {
176 (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, ¶m_unit),
177 _ => param_name == arg.name,
178 }
179}
180
181fn cpp_type_text_pointee_is_const(text: &str) -> bool {
182 let normalized = normalize_cpp_whitespace(text);
183 let base = cpp_type_text_base(&normalized).trim();
184 base.starts_with("const ") || base.ends_with(" const")
185}
186
187fn cpp_type_text_base(text: &str) -> &str {
188 text[..cpp_type_text_shape(text).0].trim()
189}
190
191pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
192 struct TopLevelCommaSplit<'a> {
193 value: &'a str,
194 start: usize,
195 angle: usize,
196 paren: usize,
197 brace: usize,
198 bracket: usize,
199 }
200
201 impl<'a> Iterator for TopLevelCommaSplit<'a> {
202 type Item = &'a str;
203
204 fn next(&mut self) -> Option<Self::Item> {
205 if self.start > self.value.len() {
206 return None;
207 }
208 for (offset, ch) in self.value[self.start..].char_indices() {
209 let absolute = self.start + offset;
210 match ch {
211 '<' => self.angle += 1,
212 '>' => self.angle = self.angle.saturating_sub(1),
213 '(' => self.paren += 1,
214 ')' => self.paren = self.paren.saturating_sub(1),
215 '{' => self.brace += 1,
216 '}' => self.brace = self.brace.saturating_sub(1),
217 '[' => self.bracket += 1,
218 ']' => self.bracket = self.bracket.saturating_sub(1),
219 ',' if self.angle == 0
220 && self.paren == 0
221 && self.brace == 0
222 && self.bracket == 0 =>
223 {
224 let item = self.value[self.start..absolute].trim();
225 self.start = absolute + ch.len_utf8();
226 return Some(item);
227 }
228 _ => {}
229 }
230 }
231 let item = self.value[self.start..].trim();
232 self.start = self.value.len() + 1;
233 Some(item)
234 }
235 }
236
237 TopLevelCommaSplit {
238 value,
239 start: 0,
240 angle: 0,
241 paren: 0,
242 brace: 0,
243 bracket: 0,
244 }
245 .filter(|item| !item.is_empty())
246}
247
248fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
249 let open = signature.find('(')?;
250 let mut depth = 0i32;
251 for (offset, ch) in signature[open..].char_indices() {
252 match ch {
253 '(' => depth += 1,
254 ')' => {
255 depth -= 1;
256 if depth == 0 {
257 return Some(signature[open + 1..open + offset].trim());
258 }
259 }
260 _ => {}
261 }
262 }
263 None
264}
265
266fn cpp_parameter_name_token(token: &str) -> bool {
267 let token = token.trim_start_matches('*').trim_start_matches('&').trim();
268 token
269 .chars()
270 .next()
271 .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
272 && token
273 .chars()
274 .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
275}
276
277fn strip_tag_type_prefix(value: &str) -> &str {
278 let value = value.trim_start_matches("const ");
279 value
280 .strip_prefix("struct ")
281 .or_else(|| value.strip_prefix("class "))
282 .or_else(|| value.strip_prefix("enum "))
283 .unwrap_or(value)
284 .trim()
285}
286
287fn cpp_number_literal_is_float(text: &str) -> bool {
288 let text = text.trim();
289 text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use brokk_bifrost_core::analyzer::ProjectFile;
296 use brokk_bifrost_core::analyzer::model::CodeUnitType;
297
298 fn test_file() -> ProjectFile {
299 ProjectFile::new(std::env::temp_dir(), "test.cpp")
300 }
301
302 fn function(name: &str, signature: &str) -> CodeUnit {
303 CodeUnit::with_signature(
304 test_file(),
305 CodeUnitType::Function,
306 "ns",
307 name,
308 Some(signature.to_string()),
309 false,
310 )
311 }
312
313 fn class(name: &str) -> CodeUnit {
314 CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
315 }
316
317 #[test]
318 fn cpp_filter_candidates_matches_named_unindexed_types() {
319 let candidates = vec![
320 function("format", "std::string format(const std::string& value)"),
321 function("format", "std::string format(int value)"),
322 ];
323 let filtered = cpp_filter_candidates_by_args(
324 candidates,
325 &[Some(CppArgType {
326 name: "std::string".to_string(),
327 unit: None,
328 indirection: 0,
329 pointee_const: false,
330 })],
331 &|_| None,
332 &|_, _| false,
333 );
334 assert_eq!(1, filtered.len());
335 assert!(filtered[0].signature().unwrap().contains("std::string&"));
336 }
337
338 #[test]
339 fn cpp_filter_candidates_matches_assignable_units() {
340 let arg = class("Arg");
341 let param = class("Param");
342 let filtered = cpp_filter_candidates_by_args(
343 vec![function("take", "void take(Param value)")],
344 &[Some(CppArgType {
345 name: "Arg".to_string(),
346 unit: Some(arg.clone()),
347 indirection: 0,
348 pointee_const: false,
349 })],
350 &|name| (name == "Param").then(|| param.clone()),
351 &|from, to| from == &arg && to == ¶m,
352 );
353 assert_eq!(1, filtered.len());
354 }
355
356 #[test]
357 fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
358 let candidates = vec![
359 function("take", "void take(int* value)"),
360 function("take", "void take(int value)"),
361 ];
362 let filtered = cpp_filter_candidates_by_args(
363 candidates,
364 &[Some(CppArgType {
365 name: "int".to_string(),
366 unit: None,
367 indirection: 0,
368 pointee_const: false,
369 })],
370 &|_| None,
371 &|_, _| false,
372 );
373 assert_eq!(1, filtered.len());
374 assert_eq!("void take(int value)", filtered[0].signature().unwrap());
375 }
376
377 #[test]
378 fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
379 let literal = Some(CppArgType {
380 name: "char".to_string(),
381 unit: None,
382 indirection: 1,
383 pointee_const: true,
384 });
385 let direct = cpp_filter_candidates_by_args(
386 vec![
387 function("select", "int select(int value)"),
388 function("select", "int select(const char* value)"),
389 ],
390 std::slice::from_ref(&literal),
391 &|_| None,
392 &|_, _| false,
393 );
394 assert_eq!(1, direct.len());
395 assert_eq!(
396 "int select(const char* value)",
397 direct[0].signature().unwrap()
398 );
399
400 for candidates in [
401 vec![
402 function("select", "int select(int value)"),
403 function("select", "int select(char* value)"),
404 ],
405 vec![
406 function("format", "int format(int value)"),
407 function("format", "int format(std::string value)"),
408 ],
409 ] {
410 let filtered = cpp_filter_candidates_by_args(
411 candidates.clone(),
412 std::slice::from_ref(&literal),
413 &|_| None,
414 &|_, _| false,
415 );
416 assert_eq!(
417 candidates, filtered,
418 "unmodeled or invalid conversions must remain conservative"
419 );
420 }
421 }
422
423 #[test]
424 fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
425 assert_eq!("char*", cpp_parameter_type_text("char * const value"));
426 assert_eq!(
427 "const char*",
428 cpp_parameter_type_text("const char * const value")
429 );
430 assert_eq!("char", normalize_cpp_type_name("char * const"));
431 }
432
433 #[test]
434 fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
435 let candidates = vec![
436 function("format", "void format(std::string value)"),
437 function("format", "void format(int value)"),
438 ];
439 let filtered =
440 cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
441 assert_eq!(candidates, filtered);
442 }
443
444 #[test]
445 fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
446 let candidates = vec![
447 function("format", "void format(std::string value)"),
448 function("format", "void format(int value)"),
449 ];
450 let filtered = cpp_filter_candidates_by_args(
451 candidates.clone(),
452 &[Some(CppArgType {
453 name: "double".to_string(),
454 unit: None,
455 indirection: 0,
456 pointee_const: false,
457 })],
458 &|_| None,
459 &|_, _| false,
460 );
461 assert_eq!(candidates, filtered);
462 }
463}