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_forwarding_call_argument<'tree>(call: Node<'tree>, source: &str) -> Option<Node<'tree>> {
106 if call.kind() != "call_expression" {
107 return None;
108 }
109 let mut callee = call.child_by_field_name("function")?;
110 if callee.kind() == "template_function" {
111 callee = callee.child_by_field_name("name")?;
112 }
113 if callee.kind() != "qualified_identifier" {
114 return None;
115 }
116 let scope = callee.child_by_field_name("scope")?;
117 if !matches!(scope.kind(), "namespace_identifier" | "identifier")
118 || cpp_node_text(scope, source).trim() != "std"
119 {
120 return None;
121 }
122 let mut name = callee.child_by_field_name("name")?;
123 if name.kind() == "template_function" {
127 name = name.child_by_field_name("name")?;
128 }
129 if !matches!(name.kind(), "identifier" | "field_identifier")
130 || !matches!(cpp_node_text(name, source).trim(), "move" | "forward")
131 {
132 return None;
133 }
134 let arguments = call.child_by_field_name("arguments")?;
135 let mut cursor = arguments.walk();
136 let forwarded = arguments
137 .named_children(&mut cursor)
138 .filter(|argument| argument.kind() != "comment")
139 .collect::<Vec<_>>();
140 let [argument] = forwarded.as_slice() else {
141 return None;
142 };
143 Some(*argument)
144}
145
146pub fn cpp_literal_arg_type(node: Node<'_>, source: &str) -> Option<CppArgType> {
147 let scalar = |name: &str| CppArgType {
148 name: name.to_string(),
149 unit: None,
150 indirection: 0,
151 pointee_const: false,
152 };
153 match node.kind() {
154 "number_literal" => {
155 let text = cpp_node_text(node, source);
156 if cpp_number_literal_is_float(text) {
157 Some(scalar("double"))
158 } else {
159 Some(scalar("int"))
160 }
161 }
162 "true" | "false" => Some(scalar("bool")),
163 "char_literal" => Some(scalar("char")),
164 "string_literal" => {
165 let text = cpp_node_text(node, source).trim_start();
166 (text.starts_with('"') || text.starts_with("R\"")).then(|| CppArgType {
167 name: "char".to_string(),
168 unit: None,
169 indirection: 1,
170 pointee_const: true,
171 })
172 }
173 "unary_expression" => {
174 let operator = node.child_by_field_name("operator")?;
175 let inner = node
176 .child_by_field_name("argument")
177 .or_else(|| node.named_child(0))?;
178 matches!(operator.kind(), "+" | "-")
179 .then(|| cpp_literal_arg_type(inner, source))
180 .flatten()
181 }
182 _ => None,
183 }
184}
185
186#[cfg(test)]
187fn cpp_filter_candidates_by_args(
188 candidates: Vec<CodeUnit>,
189 arg_types: &[Option<CppArgType>],
190 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
191 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
192) -> Vec<CodeUnit> {
193 cpp_filter_candidates_by_args_with_parameter_types(
194 candidates,
195 arg_types,
196 &|candidate| cpp_signature_param_types(candidate.signature().unwrap_or_default()),
197 &cpp_signature_is_template_candidate,
198 resolve_type,
199 assignable,
200 )
201}
202
203pub fn cpp_filter_candidates_by_args_with_parameter_types(
204 candidates: Vec<CodeUnit>,
205 arg_types: &[Option<CppArgType>],
206 parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
207 is_template: &dyn Fn(&CodeUnit) -> bool,
208 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
209 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
210) -> Vec<CodeUnit> {
211 if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
212 return candidates;
213 }
214 let args: Vec<&CppArgType> = arg_types.iter().flatten().collect();
215 debug_assert_eq!(args.len(), arg_types.len());
216
217 let exact = cpp_candidates_matching(
223 &candidates,
224 &args,
225 parameter_types,
226 &|param, arg, template_candidate| {
227 cpp_param_matches_arg(param, arg, template_candidate, resolve_type, assignable)
228 },
229 );
230 let filtered = if exact.is_empty() {
231 cpp_candidates_matching(
232 &candidates,
233 &args,
234 parameter_types,
235 &|param, arg, template_candidate| {
236 cpp_param_matches_arg(param, arg, template_candidate, resolve_type, assignable)
237 || cpp_standard_conversion_applies(arg, param)
238 },
239 )
240 } else {
241 exact
242 };
243 if filtered.is_empty() {
244 candidates
245 } else if filtered.iter().any(is_template) {
246 candidates
251 } else {
252 filtered
253 }
254}
255
256fn cpp_candidates_matching(
259 candidates: &[CodeUnit],
260 args: &[&CppArgType],
261 parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
262 matches: &dyn Fn(&str, &CppArgType, bool) -> bool,
263) -> Vec<CodeUnit> {
264 candidates
265 .iter()
266 .filter(|candidate| {
267 let template_candidate = cpp_signature_is_template_candidate(candidate);
268 parameter_types(candidate).is_some_and(|params| {
269 params.len() == args.len()
270 && params
271 .iter()
272 .zip(args.iter())
273 .all(|(param, arg)| matches(param, arg, template_candidate))
274 })
275 })
276 .cloned()
277 .collect()
278}
279fn cpp_signature_is_template_candidate(candidate: &CodeUnit) -> bool {
280 candidate
281 .signature()
282 .is_some_and(|signature| signature.trim_start().starts_with('<'))
283}
284
285fn cpp_param_matches_arg(
286 param: &str,
287 arg: &CppArgType,
288 template_candidate: bool,
289 resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
290 assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
291) -> bool {
292 if cpp_type_text_pointer_depth(param) != arg.indirection {
293 return false;
294 }
295 if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
296 return false;
297 }
298 if template_candidate {
304 return true;
305 }
306 let param_name = normalize_cpp_type_name(param);
307 match (resolve_type(¶m_name), arg.unit.as_ref()) {
308 (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, ¶m_unit),
309 _ => param_name == arg.name,
310 }
311}
312
313fn cpp_standard_conversion_applies(arg: &CppArgType, param: &str) -> bool {
345 if cpp_type_text_pointer_depth(param) != 0 {
346 return false;
347 }
348 let param_name = normalize_cpp_type_name(param);
349 let span_element = cpp_template_arguments(¶m_name, "std::span")
350 .and_then(|arguments| arguments.first().copied());
351 if let Some(span_element) = span_element {
352 return arg.indirection == 0
353 && cpp_contiguous_range_element(&arg.name)
354 .is_some_and(|element| cpp_span_element_accepts(span_element, element));
355 }
356 if param_name == "std::string_view" {
357 return (arg.indirection == 0 && arg.name == "std::string")
358 || (arg.indirection == 1 && arg.name == "char");
359 }
360 false
361}
362
363fn cpp_template_arguments<'a>(text: &'a str, base: &str) -> Option<Vec<&'a str>> {
370 let inner = text
371 .strip_prefix(base)?
372 .trim_start()
373 .strip_prefix('<')?
374 .trim_end()
375 .strip_suffix('>')?;
376 Some(cpp_split_top_level_commas(inner).collect())
377}
378
379fn cpp_contiguous_range_element(name: &str) -> Option<&str> {
382 ["std::vector", "std::array"]
383 .into_iter()
384 .find_map(|base| cpp_template_arguments(name, base))
385 .and_then(|arguments| arguments.first().copied())
386}
387
388fn cpp_span_element_accepts(param_element: &str, arg_element: &str) -> bool {
391 cpp_type_text_pointer_depth(param_element) == cpp_type_text_pointer_depth(arg_element)
392 && normalize_cpp_type_name(param_element) == normalize_cpp_type_name(arg_element)
393 && (cpp_type_text_pointee_is_const(param_element)
394 || !cpp_type_text_pointee_is_const(arg_element))
395}
396
397fn cpp_type_text_pointee_is_const(text: &str) -> bool {
398 let normalized = normalize_cpp_whitespace(text);
399 let base = cpp_type_text_base(&normalized).trim();
400 base.starts_with("const ") || base.ends_with(" const")
401}
402
403fn cpp_type_text_base(text: &str) -> &str {
404 text[..cpp_type_text_shape(text).0].trim()
405}
406
407pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
408 struct TopLevelCommaSplit<'a> {
409 value: &'a str,
410 start: usize,
411 angle: usize,
412 paren: usize,
413 brace: usize,
414 bracket: usize,
415 }
416
417 impl<'a> Iterator for TopLevelCommaSplit<'a> {
418 type Item = &'a str;
419
420 fn next(&mut self) -> Option<Self::Item> {
421 if self.start > self.value.len() {
422 return None;
423 }
424 for (offset, ch) in self.value[self.start..].char_indices() {
425 let absolute = self.start + offset;
426 match ch {
427 '<' => self.angle += 1,
428 '>' => self.angle = self.angle.saturating_sub(1),
429 '(' => self.paren += 1,
430 ')' => self.paren = self.paren.saturating_sub(1),
431 '{' => self.brace += 1,
432 '}' => self.brace = self.brace.saturating_sub(1),
433 '[' => self.bracket += 1,
434 ']' => self.bracket = self.bracket.saturating_sub(1),
435 ',' if self.angle == 0
436 && self.paren == 0
437 && self.brace == 0
438 && self.bracket == 0 =>
439 {
440 let item = self.value[self.start..absolute].trim();
441 self.start = absolute + ch.len_utf8();
442 return Some(item);
443 }
444 _ => {}
445 }
446 }
447 let item = self.value[self.start..].trim();
448 self.start = self.value.len() + 1;
449 Some(item)
450 }
451 }
452
453 TopLevelCommaSplit {
454 value,
455 start: 0,
456 angle: 0,
457 paren: 0,
458 brace: 0,
459 bracket: 0,
460 }
461 .filter(|item| !item.is_empty())
462}
463
464fn cpp_signature_parameter_span(signature: &str) -> Option<(usize, usize)> {
466 let open = signature.find('(')?;
467 let mut depth = 0i32;
468 for (offset, ch) in signature[open..].char_indices() {
469 match ch {
470 '(' => depth += 1,
471 ')' => {
472 depth -= 1;
473 if depth == 0 {
474 return Some((open, open + offset));
475 }
476 }
477 _ => {}
478 }
479 }
480 None
481}
482
483fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
484 let (open, close) = cpp_signature_parameter_span(signature)?;
485 Some(signature[open + 1..close].trim())
486}
487
488pub fn cpp_signature_trailing_qualifiers(signature: &str) -> &str {
497 match cpp_signature_parameter_span(signature) {
498 Some((_, close)) => signature[close + 1..].trim(),
499 None => "",
500 }
501}
502
503#[derive(Clone, Copy, Debug, PartialEq, Eq)]
504pub enum CppRefQualifier {
505 Lvalue,
506 Rvalue,
507}
508
509pub fn cpp_signature_ref_qualifier(signature: &str) -> Option<CppRefQualifier> {
516 let mut suffix = cpp_signature_trailing_qualifiers(signature);
517 loop {
518 if let Some(rest) = suffix.strip_prefix("const ") {
519 suffix = rest;
520 } else if let Some(rest) = suffix.strip_prefix("volatile ") {
521 suffix = rest;
522 } else {
523 break;
524 }
525 }
526 if suffix == "&&" || suffix.starts_with("&& ") {
527 Some(CppRefQualifier::Rvalue)
528 } else if suffix == "&" || suffix.starts_with("& ") {
529 Some(CppRefQualifier::Lvalue)
530 } else {
531 None
532 }
533}
534
535fn cpp_parameter_name_token(token: &str) -> bool {
536 let token = token.trim_start_matches('*').trim_start_matches('&').trim();
537 token
538 .chars()
539 .next()
540 .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
541 && token
542 .chars()
543 .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
544}
545
546fn strip_tag_type_prefix(value: &str) -> &str {
547 let value = value.trim_start_matches("const ");
548 value
549 .strip_prefix("struct ")
550 .or_else(|| value.strip_prefix("class "))
551 .or_else(|| value.strip_prefix("enum "))
552 .unwrap_or(value)
553 .trim()
554}
555
556fn cpp_number_literal_is_float(text: &str) -> bool {
557 let text = text.trim();
558 text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use brokk_bifrost_core::analyzer::ProjectFile;
565 use brokk_bifrost_core::analyzer::model::CodeUnitType;
566
567 fn test_file() -> ProjectFile {
568 ProjectFile::new(std::env::temp_dir(), "test.cpp")
569 }
570
571 fn function(name: &str, signature: &str) -> CodeUnit {
572 CodeUnit::with_signature(
573 test_file(),
574 CodeUnitType::Function,
575 "ns",
576 name,
577 Some(signature.to_string()),
578 false,
579 )
580 }
581
582 fn class(name: &str) -> CodeUnit {
583 CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
584 }
585
586 #[test]
587 fn cpp_filter_candidates_matches_named_unindexed_types() {
588 let candidates = vec![
589 function("format", "std::string format(const std::string& value)"),
590 function("format", "std::string format(int value)"),
591 ];
592 let filtered = cpp_filter_candidates_by_args(
593 candidates,
594 &[Some(CppArgType {
595 name: "std::string".to_string(),
596 unit: None,
597 indirection: 0,
598 pointee_const: false,
599 })],
600 &|_| None,
601 &|_, _| false,
602 );
603 assert_eq!(1, filtered.len());
604 assert!(filtered[0].signature().unwrap().contains("std::string&"));
605 }
606
607 #[test]
608 fn cpp_filter_candidates_matches_assignable_units() {
609 let arg = class("Arg");
610 let param = class("Param");
611 let filtered = cpp_filter_candidates_by_args(
612 vec![function("take", "void take(Param value)")],
613 &[Some(CppArgType {
614 name: "Arg".to_string(),
615 unit: Some(arg.clone()),
616 indirection: 0,
617 pointee_const: false,
618 })],
619 &|name| (name == "Param").then(|| param.clone()),
620 &|from, to| from == &arg && to == ¶m,
621 );
622 assert_eq!(1, filtered.len());
623 }
624
625 #[test]
626 fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
627 let candidates = vec![
628 function("take", "void take(int* value)"),
629 function("take", "void take(int value)"),
630 ];
631 let filtered = cpp_filter_candidates_by_args(
632 candidates,
633 &[Some(CppArgType {
634 name: "int".to_string(),
635 unit: None,
636 indirection: 0,
637 pointee_const: false,
638 })],
639 &|_| None,
640 &|_, _| false,
641 );
642 assert_eq!(1, filtered.len());
643 assert_eq!("void take(int value)", filtered[0].signature().unwrap());
644 }
645
646 #[test]
647 fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
648 let literal = Some(CppArgType {
649 name: "char".to_string(),
650 unit: None,
651 indirection: 1,
652 pointee_const: true,
653 });
654 let direct = cpp_filter_candidates_by_args(
655 vec![
656 function("select", "int select(int value)"),
657 function("select", "int select(const char* value)"),
658 ],
659 std::slice::from_ref(&literal),
660 &|_| None,
661 &|_, _| false,
662 );
663 assert_eq!(1, direct.len());
664 assert_eq!(
665 "int select(const char* value)",
666 direct[0].signature().unwrap()
667 );
668
669 for candidates in [
670 vec![
671 function("select", "int select(int value)"),
672 function("select", "int select(char* value)"),
673 ],
674 vec![
675 function("format", "int format(int value)"),
676 function("format", "int format(std::string value)"),
677 ],
678 ] {
679 let filtered = cpp_filter_candidates_by_args(
680 candidates.clone(),
681 std::slice::from_ref(&literal),
682 &|_| None,
683 &|_, _| false,
684 );
685 assert_eq!(
686 candidates, filtered,
687 "unmodeled or invalid conversions must remain conservative"
688 );
689 }
690 }
691
692 #[test]
693 fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
694 assert_eq!("char*", cpp_parameter_type_text("char * const value"));
695 assert_eq!(
696 "const char*",
697 cpp_parameter_type_text("const char * const value")
698 );
699 assert_eq!("char", normalize_cpp_type_name("char * const"));
700 }
701
702 #[test]
703 fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
704 let candidates = vec![
705 function("format", "void format(std::string value)"),
706 function("format", "void format(int value)"),
707 ];
708 let filtered =
709 cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
710 assert_eq!(candidates, filtered);
711 }
712
713 #[test]
714 fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
715 let candidates = vec![
716 function("format", "void format(std::string value)"),
717 function("format", "void format(int value)"),
718 ];
719 let filtered = cpp_filter_candidates_by_args(
720 candidates.clone(),
721 &[Some(CppArgType {
722 name: "double".to_string(),
723 unit: None,
724 indirection: 0,
725 pointee_const: false,
726 })],
727 &|_| None,
728 &|_, _| false,
729 );
730 assert_eq!(candidates, filtered);
731 }
732
733 fn call_expressions(tree: &tree_sitter::Tree) -> Vec<Node<'_>> {
735 let mut calls = Vec::new();
736 let mut stack = vec![tree.root_node()];
737 while let Some(node) = stack.pop() {
738 if node.kind() == "call_expression" {
739 calls.push(node);
740 }
741 let mut cursor = node.walk();
742 stack.extend(node.named_children(&mut cursor));
743 }
744 calls.sort_by_key(Node::start_byte);
745 calls
746 }
747
748 fn parse(source: &str) -> tree_sitter::Tree {
749 let mut parser = tree_sitter::Parser::new();
750 parser
751 .set_language(&tree_sitter_cpp::LANGUAGE.into())
752 .expect("the C++ grammar loads");
753 parser.parse(source, None).expect("the fixture parses")
754 }
755
756 #[test]
760 fn a_forwarding_call_reports_the_one_argument_it_forwards() {
761 let source = r#"void f() {
762 sink(std::move(a));
763 sink(std::forward<T>(b));
764 sink(std::move(c, 1));
765 sink(std::swap(d, e));
766 sink(move(g));
767 sink(other::move(h));
768 sink(std::vector<int>(i));
769}
770"#;
771 let tree = parse(source);
772 let forwarded = call_expressions(&tree)
773 .into_iter()
774 .filter_map(|call| cpp_forwarding_call_argument(call, source))
775 .map(|argument| cpp_node_text(argument, source).to_string())
776 .collect::<Vec<_>>();
777 assert_eq!(forwarded, vec!["a".to_string(), "b".to_string()]);
778 }
779
780 fn value_arg(name: &str) -> Option<CppArgType> {
781 Some(CppArgType {
782 name: name.to_string(),
783 unit: None,
784 indirection: 0,
785 pointee_const: false,
786 })
787 }
788
789 #[test]
794 fn a_contiguous_range_satisfies_a_span_over_its_element() {
795 for arg in ["std::vector<uint8_t>", "std::array<uint8_t, 16>"] {
796 for param in ["std::span<const uint8_t>", "std::span<uint8_t>"] {
797 let candidates = vec![
798 function("take", &format!("void take({param} bytes)")),
799 function("take", "void take(int count)"),
800 ];
801 let filtered = cpp_filter_candidates_by_args(
802 candidates,
803 &[value_arg(arg)],
804 &|_| None,
805 &|_, _| false,
806 );
807 assert_eq!(filtered.len(), 1, "{arg} -> {param}");
808 assert!(
809 filtered[0].signature().unwrap().contains(param),
810 "{arg} -> {param}: {:?}",
811 filtered[0].signature()
812 );
813 }
814 }
815 }
816
817 #[test]
820 fn a_range_over_another_element_does_not_satisfy_a_span() {
821 for (arg, param) in [
822 ("std::vector<int>", "std::span<const uint8_t>"),
823 ("std::vector<const uint8_t>", "std::span<uint8_t>"),
824 ("std::vector<uint8_t>", "std::span<const uint8_t*>"),
825 ("std::deque<uint8_t>", "std::span<const uint8_t>"),
826 ] {
827 let candidates = vec![
828 function("take", &format!("void take({param} bytes)")),
829 function("take", "void take(int count)"),
830 ];
831 let filtered = cpp_filter_candidates_by_args(
832 candidates.clone(),
833 &[value_arg(arg)],
834 &|_| None,
835 &|_, _| false,
836 );
837 assert_eq!(
838 candidates, filtered,
839 "{arg} must not satisfy {param}, so every candidate stays"
840 );
841 }
842 }
843
844 #[test]
845 fn an_owned_string_and_a_character_pointer_satisfy_a_string_view() {
846 let literal = Some(CppArgType {
847 name: "char".to_string(),
848 unit: None,
849 indirection: 1,
850 pointee_const: true,
851 });
852 for arg in [value_arg("std::string"), literal] {
853 let filtered = cpp_filter_candidates_by_args(
854 vec![
855 function("label", "void label(std::string_view text)"),
856 function("label", "void label(int count)"),
857 ],
858 std::slice::from_ref(&arg),
859 &|_| None,
860 &|_, _| false,
861 );
862 assert_eq!(filtered.len(), 1, "{:?}", arg.as_ref().map(|arg| &arg.name));
863 assert!(
864 filtered[0]
865 .signature()
866 .unwrap()
867 .contains("std::string_view"),
868 "{:?}",
869 filtered[0].signature()
870 );
871 }
872 }
873
874 #[test]
878 fn an_exact_match_outranks_a_standard_conversion() {
879 let candidates = vec![
880 function("own", "void own(std::vector<uint8_t> bytes)"),
881 function("own", "void own(std::span<const uint8_t> bytes)"),
882 ];
883 let filtered = cpp_filter_candidates_by_args(
884 candidates,
885 &[value_arg("std::vector<uint8_t>")],
886 &|_| None,
887 &|_, _| false,
888 );
889 assert_eq!(filtered.len(), 1);
890 assert_eq!(
891 "void own(std::vector<uint8_t> bytes)",
892 filtered[0].signature().unwrap()
893 );
894 }
895
896 #[test]
897 fn cpp_filter_candidates_keeps_templates_when_only_type_shape_is_unknown() {
898 let candidates = vec![
899 function("take", "void take(Vec256<float> value)"),
900 function("take", "<typename T>(Vec256<T>)"),
901 ];
902 let filtered = cpp_filter_candidates_by_args(
903 candidates.clone(),
904 &[Some(CppArgType {
905 name: "Vec256<int>".to_string(),
906 unit: Some(class("Vec256")),
907 indirection: 0,
908 pointee_const: false,
909 })],
910 &|_| None,
911 &|_, _| false,
912 );
913 assert_eq!(filtered, candidates);
914 }
915
916 #[test]
917 fn signature_ref_qualifier_comes_from_the_ast_normalized_suffix() {
918 assert_eq!(
919 cpp_signature_ref_qualifier("() const & noexcept"),
920 Some(CppRefQualifier::Lvalue)
921 );
922 assert_eq!(
923 cpp_signature_ref_qualifier("(int) volatile && noexcept"),
924 Some(CppRefQualifier::Rvalue)
925 );
926 assert_eq!(cpp_signature_ref_qualifier("() const noexcept"), None);
927 assert_eq!(
928 cpp_signature_ref_qualifier("() noexcept(left && right)"),
929 None
930 );
931 }
932}