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 cpp_filter_candidates_by_args_with_parameter_types(
135 candidates,
136 arg_types,
137 &|candidate| cpp_signature_param_types(candidate.signature().unwrap_or_default()),
138 resolve_type,
139 assignable,
140 )
141}
142
143pub fn cpp_filter_candidates_by_args_with_parameter_types(
144 candidates: Vec<CodeUnit>,
145 arg_types: &[Option<CppArgType>],
146 parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
147 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
148 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
149) -> Vec<CodeUnit> {
150 if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
151 return candidates;
152 }
153
154 let filtered: Vec<_> = candidates
155 .iter()
156 .filter(|candidate| {
157 let template_candidate = cpp_signature_is_template_candidate(candidate);
158 parameter_types(candidate).is_some_and(|params| {
159 params.len() == arg_types.len()
160 && params.iter().zip(arg_types.iter()).all(|(param, arg)| {
161 cpp_param_matches_arg(
162 param,
163 arg,
164 template_candidate,
165 resolve_type,
166 assignable,
167 )
168 })
169 })
170 })
171 .cloned()
172 .collect();
173 if filtered.is_empty() {
174 candidates
175 } else if filtered.iter().any(cpp_signature_is_template_candidate) {
176 candidates
181 } else {
182 filtered
183 }
184}
185
186fn cpp_signature_is_template_candidate(candidate: &CodeUnit) -> bool {
187 candidate
188 .signature()
189 .is_some_and(|signature| signature.trim_start().starts_with('<'))
190}
191
192fn cpp_param_matches_arg(
193 param: &str,
194 arg: &Option<CppArgType>,
195 template_candidate: bool,
196 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
197 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
198) -> bool {
199 let Some(arg) = arg else {
200 return false;
201 };
202 if cpp_type_text_pointer_depth(param) != arg.indirection {
203 return false;
204 }
205 if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
206 return false;
207 }
208 if template_candidate {
214 return true;
215 }
216 let param_name = normalize_cpp_type_name(param);
217 match (resolve_type(¶m_name), arg.unit.as_ref()) {
218 (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, ¶m_unit),
219 _ => param_name == arg.name,
220 }
221}
222
223fn cpp_type_text_pointee_is_const(text: &str) -> bool {
224 let normalized = normalize_cpp_whitespace(text);
225 let base = cpp_type_text_base(&normalized).trim();
226 base.starts_with("const ") || base.ends_with(" const")
227}
228
229fn cpp_type_text_base(text: &str) -> &str {
230 text[..cpp_type_text_shape(text).0].trim()
231}
232
233pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
234 struct TopLevelCommaSplit<'a> {
235 value: &'a str,
236 start: usize,
237 angle: usize,
238 paren: usize,
239 brace: usize,
240 bracket: usize,
241 }
242
243 impl<'a> Iterator for TopLevelCommaSplit<'a> {
244 type Item = &'a str;
245
246 fn next(&mut self) -> Option<Self::Item> {
247 if self.start > self.value.len() {
248 return None;
249 }
250 for (offset, ch) in self.value[self.start..].char_indices() {
251 let absolute = self.start + offset;
252 match ch {
253 '<' => self.angle += 1,
254 '>' => self.angle = self.angle.saturating_sub(1),
255 '(' => self.paren += 1,
256 ')' => self.paren = self.paren.saturating_sub(1),
257 '{' => self.brace += 1,
258 '}' => self.brace = self.brace.saturating_sub(1),
259 '[' => self.bracket += 1,
260 ']' => self.bracket = self.bracket.saturating_sub(1),
261 ',' if self.angle == 0
262 && self.paren == 0
263 && self.brace == 0
264 && self.bracket == 0 =>
265 {
266 let item = self.value[self.start..absolute].trim();
267 self.start = absolute + ch.len_utf8();
268 return Some(item);
269 }
270 _ => {}
271 }
272 }
273 let item = self.value[self.start..].trim();
274 self.start = self.value.len() + 1;
275 Some(item)
276 }
277 }
278
279 TopLevelCommaSplit {
280 value,
281 start: 0,
282 angle: 0,
283 paren: 0,
284 brace: 0,
285 bracket: 0,
286 }
287 .filter(|item| !item.is_empty())
288}
289
290fn cpp_signature_parameter_span(signature: &str) -> Option<(usize, usize)> {
292 let open = signature.find('(')?;
293 let mut depth = 0i32;
294 for (offset, ch) in signature[open..].char_indices() {
295 match ch {
296 '(' => depth += 1,
297 ')' => {
298 depth -= 1;
299 if depth == 0 {
300 return Some((open, open + offset));
301 }
302 }
303 _ => {}
304 }
305 }
306 None
307}
308
309fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
310 let (open, close) = cpp_signature_parameter_span(signature)?;
311 Some(signature[open + 1..close].trim())
312}
313
314pub fn cpp_signature_trailing_qualifiers(signature: &str) -> &str {
323 match cpp_signature_parameter_span(signature) {
324 Some((_, close)) => signature[close + 1..].trim(),
325 None => "",
326 }
327}
328
329fn cpp_parameter_name_token(token: &str) -> bool {
330 let token = token.trim_start_matches('*').trim_start_matches('&').trim();
331 token
332 .chars()
333 .next()
334 .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
335 && token
336 .chars()
337 .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
338}
339
340fn strip_tag_type_prefix(value: &str) -> &str {
341 let value = value.trim_start_matches("const ");
342 value
343 .strip_prefix("struct ")
344 .or_else(|| value.strip_prefix("class "))
345 .or_else(|| value.strip_prefix("enum "))
346 .unwrap_or(value)
347 .trim()
348}
349
350fn cpp_number_literal_is_float(text: &str) -> bool {
351 let text = text.trim();
352 text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use brokk_bifrost_core::analyzer::ProjectFile;
359 use brokk_bifrost_core::analyzer::model::CodeUnitType;
360
361 fn test_file() -> ProjectFile {
362 ProjectFile::new(std::env::temp_dir(), "test.cpp")
363 }
364
365 fn function(name: &str, signature: &str) -> CodeUnit {
366 CodeUnit::with_signature(
367 test_file(),
368 CodeUnitType::Function,
369 "ns",
370 name,
371 Some(signature.to_string()),
372 false,
373 )
374 }
375
376 fn class(name: &str) -> CodeUnit {
377 CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
378 }
379
380 #[test]
381 fn cpp_filter_candidates_matches_named_unindexed_types() {
382 let candidates = vec![
383 function("format", "std::string format(const std::string& value)"),
384 function("format", "std::string format(int value)"),
385 ];
386 let filtered = cpp_filter_candidates_by_args(
387 candidates,
388 &[Some(CppArgType {
389 name: "std::string".to_string(),
390 unit: None,
391 indirection: 0,
392 pointee_const: false,
393 })],
394 &|_| None,
395 &|_, _| false,
396 );
397 assert_eq!(1, filtered.len());
398 assert!(filtered[0].signature().unwrap().contains("std::string&"));
399 }
400
401 #[test]
402 fn cpp_filter_candidates_matches_assignable_units() {
403 let arg = class("Arg");
404 let param = class("Param");
405 let filtered = cpp_filter_candidates_by_args(
406 vec![function("take", "void take(Param value)")],
407 &[Some(CppArgType {
408 name: "Arg".to_string(),
409 unit: Some(arg.clone()),
410 indirection: 0,
411 pointee_const: false,
412 })],
413 &|name| (name == "Param").then(|| param.clone()),
414 &|from, to| from == &arg && to == ¶m,
415 );
416 assert_eq!(1, filtered.len());
417 }
418
419 #[test]
420 fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
421 let candidates = vec![
422 function("take", "void take(int* value)"),
423 function("take", "void take(int value)"),
424 ];
425 let filtered = cpp_filter_candidates_by_args(
426 candidates,
427 &[Some(CppArgType {
428 name: "int".to_string(),
429 unit: None,
430 indirection: 0,
431 pointee_const: false,
432 })],
433 &|_| None,
434 &|_, _| false,
435 );
436 assert_eq!(1, filtered.len());
437 assert_eq!("void take(int value)", filtered[0].signature().unwrap());
438 }
439
440 #[test]
441 fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
442 let literal = Some(CppArgType {
443 name: "char".to_string(),
444 unit: None,
445 indirection: 1,
446 pointee_const: true,
447 });
448 let direct = cpp_filter_candidates_by_args(
449 vec![
450 function("select", "int select(int value)"),
451 function("select", "int select(const char* value)"),
452 ],
453 std::slice::from_ref(&literal),
454 &|_| None,
455 &|_, _| false,
456 );
457 assert_eq!(1, direct.len());
458 assert_eq!(
459 "int select(const char* value)",
460 direct[0].signature().unwrap()
461 );
462
463 for candidates in [
464 vec![
465 function("select", "int select(int value)"),
466 function("select", "int select(char* value)"),
467 ],
468 vec![
469 function("format", "int format(int value)"),
470 function("format", "int format(std::string value)"),
471 ],
472 ] {
473 let filtered = cpp_filter_candidates_by_args(
474 candidates.clone(),
475 std::slice::from_ref(&literal),
476 &|_| None,
477 &|_, _| false,
478 );
479 assert_eq!(
480 candidates, filtered,
481 "unmodeled or invalid conversions must remain conservative"
482 );
483 }
484 }
485
486 #[test]
487 fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
488 assert_eq!("char*", cpp_parameter_type_text("char * const value"));
489 assert_eq!(
490 "const char*",
491 cpp_parameter_type_text("const char * const value")
492 );
493 assert_eq!("char", normalize_cpp_type_name("char * const"));
494 }
495
496 #[test]
497 fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
498 let candidates = vec![
499 function("format", "void format(std::string value)"),
500 function("format", "void format(int value)"),
501 ];
502 let filtered =
503 cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
504 assert_eq!(candidates, filtered);
505 }
506
507 #[test]
508 fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
509 let candidates = vec![
510 function("format", "void format(std::string value)"),
511 function("format", "void format(int value)"),
512 ];
513 let filtered = cpp_filter_candidates_by_args(
514 candidates.clone(),
515 &[Some(CppArgType {
516 name: "double".to_string(),
517 unit: None,
518 indirection: 0,
519 pointee_const: false,
520 })],
521 &|_| None,
522 &|_, _| false,
523 );
524 assert_eq!(candidates, filtered);
525 }
526
527 #[test]
528 fn cpp_filter_candidates_keeps_templates_when_only_type_shape_is_unknown() {
529 let candidates = vec![
530 function("take", "void take(Vec256<float> value)"),
531 function("take", "<typename T>(Vec256<T>)"),
532 ];
533 let filtered = cpp_filter_candidates_by_args(
534 candidates.clone(),
535 &[Some(CppArgType {
536 name: "Vec256<int>".to_string(),
537 unit: Some(class("Vec256")),
538 indirection: 0,
539 pointee_const: false,
540 })],
541 &|_| None,
542 &|_, _| false,
543 );
544 assert_eq!(filtered, candidates);
545 }
546}