1use crate::abi::{mx_erased, mx_tag};
9use crate::sys::{DllInfo, R_CallMethodDef};
10#[cfg(not(target_arch = "wasm32"))]
11use linkme::distributed_slice;
12use std::os::raw::c_void;
13
14#[cfg(not(target_arch = "wasm32"))]
32#[distributed_slice]
33pub static MX_CALL_DEFS: [R_CallMethodDef];
34
35#[cfg(not(target_arch = "wasm32"))]
41#[distributed_slice]
42pub static MX_R_WRAPPERS: [RWrapperEntry];
43
44#[cfg(not(target_arch = "wasm32"))]
56#[distributed_slice]
57pub static MX_ALTREP_REGISTRATIONS: [AltrepRegistration];
58
59#[cfg(not(target_arch = "wasm32"))]
64#[distributed_slice]
65pub static MX_TRAIT_DISPATCH: [TraitDispatchEntry];
66
67#[cfg(not(target_arch = "wasm32"))]
73#[distributed_slice]
74pub static MX_MATCH_ARG_CHOICES: [MatchArgChoicesEntry];
75
76#[cfg(not(target_arch = "wasm32"))]
83#[distributed_slice]
84pub static MX_MATCH_ARG_PARAM_DOCS: [MatchArgParamDocEntry];
85
86#[cfg(not(target_arch = "wasm32"))]
93#[distributed_slice]
94pub static MX_CLASS_NAMES: [ClassNameEntry];
95
96#[cfg(not(target_arch = "wasm32"))]
103#[distributed_slice]
104pub static MX_S7_SIDECAR_PROPS: [SidecarPropEntry];
105
106#[inline]
113pub(crate) fn call_defs() -> &'static [R_CallMethodDef] {
114 #[cfg(not(target_arch = "wasm32"))]
115 {
116 &MX_CALL_DEFS
117 }
118 #[cfg(target_arch = "wasm32")]
119 {
120 wasm_runtime::call_defs()
121 }
122}
123
124#[inline]
125pub(crate) fn altrep_regs() -> &'static [AltrepRegistration] {
126 #[cfg(not(target_arch = "wasm32"))]
127 {
128 &MX_ALTREP_REGISTRATIONS
129 }
130 #[cfg(target_arch = "wasm32")]
131 {
132 wasm_runtime::altrep_regs()
133 }
134}
135
136#[inline]
137pub(crate) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
138 #[cfg(not(target_arch = "wasm32"))]
139 {
140 &MX_TRAIT_DISPATCH
141 }
142 #[cfg(target_arch = "wasm32")]
143 {
144 wasm_runtime::trait_dispatch()
145 }
146}
147
148#[cfg(target_arch = "wasm32")]
149mod wasm_runtime {
150 use super::{AltrepRegistration, R_CallMethodDef, TraitDispatchEntry};
151 use std::sync::OnceLock;
152
153 static CALL_DEFS: OnceLock<&'static [R_CallMethodDef]> = OnceLock::new();
154 static ALTREP_REGS: OnceLock<&'static [AltrepRegistration]> = OnceLock::new();
155 static TRAIT_DISPATCH: OnceLock<&'static [TraitDispatchEntry]> = OnceLock::new();
156
157 pub(super) fn install(
158 c: &'static [R_CallMethodDef],
159 a: &'static [AltrepRegistration],
160 t: &'static [TraitDispatchEntry],
161 ) {
162 let _ = CALL_DEFS.set(c);
166 let _ = ALTREP_REGS.set(a);
167 let _ = TRAIT_DISPATCH.set(t);
168 }
169
170 pub(super) fn call_defs() -> &'static [R_CallMethodDef] {
171 CALL_DEFS.get().copied().unwrap_or(&[])
172 }
173 pub(super) fn altrep_regs() -> &'static [AltrepRegistration] {
174 ALTREP_REGS.get().copied().unwrap_or(&[])
175 }
176 pub(super) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
177 TRAIT_DISPATCH.get().copied().unwrap_or(&[])
178 }
179}
180
181#[cfg(target_arch = "wasm32")]
192pub fn install_wasm_runtime_slices(
193 call_defs: &'static [R_CallMethodDef],
194 altrep_regs: &'static [AltrepRegistration],
195 trait_dispatch: &'static [TraitDispatchEntry],
196) {
197 wasm_runtime::install(call_defs, altrep_regs, trait_dispatch);
198}
199
200#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
210pub enum RWrapperPriority {
211 Sidecar,
213 Class,
215 Function,
217 TraitImpl,
219 Vctrs,
221}
222
223pub struct RWrapperEntry {
225 pub priority: RWrapperPriority,
227 pub content: &'static str,
229 pub source_file: &'static str,
233}
234
235unsafe impl Sync for RWrapperEntry {}
237
238pub struct MatchArgChoicesEntry {
240 pub placeholder: &'static str,
242 pub choices_str: fn() -> String,
245 pub preferred_default: &'static str,
250}
251
252unsafe impl Sync for MatchArgChoicesEntry {}
254
255pub struct MatchArgParamDocEntry {
258 pub placeholder: &'static str,
261 pub several_ok: bool,
264 pub choices_str: fn() -> String,
267}
268
269unsafe impl Sync for MatchArgParamDocEntry {}
271
272pub struct ClassNameEntry {
278 pub rust_type: &'static str,
280 pub r_class_name: &'static str,
283 pub class_system: &'static str,
285}
286
287unsafe impl Sync for ClassNameEntry {}
289
290#[cfg(not(target_arch = "wasm32"))]
304fn build_class_name_index<'a>(
305 entries: impl IntoIterator<Item = &'a ClassNameEntry>,
306) -> std::collections::HashMap<&'a str, &'a ClassNameEntry> {
307 use std::collections::hash_map::Entry;
308 let mut index = std::collections::HashMap::new();
309 for e in entries {
310 match index.entry(e.rust_type) {
311 Entry::Vacant(slot) => {
312 slot.insert(e);
313 }
314 Entry::Occupied(slot) => {
315 let prev = *slot.get();
316 if prev.r_class_name != e.r_class_name || prev.class_system != e.class_system {
317 panic!(
318 "conflicting class registrations for Rust type `{}`: \
319 `{}` ({}) vs `{}` ({}). Multiple #[miniextendr] impl blocks \
320 on one type must agree on the class system and any \
321 `class = \"...\"` override.",
322 e.rust_type,
323 prev.r_class_name,
324 prev.class_system,
325 e.r_class_name,
326 e.class_system,
327 );
328 }
329 }
330 }
331 }
332 index
333}
334
335pub struct SidecarPropEntry {
341 pub rust_type: &'static str,
343 pub field_name: &'static str,
345 pub prop_doc: &'static str,
348}
349
350unsafe impl Sync for SidecarPropEntry {}
352
353#[repr(C)]
355pub struct TraitDispatchEntry {
356 pub concrete_tag: mx_tag,
358 pub trait_tag: mx_tag,
360 pub vtable: *const c_void,
362 pub vtable_symbol: &'static str,
367}
368
369unsafe impl Sync for TraitDispatchEntry {}
372unsafe impl Send for TraitDispatchEntry {}
373
374#[repr(C)]
378pub struct AltrepRegistration {
379 pub register: extern "C" fn(),
381 pub symbol: &'static str,
385}
386
387unsafe impl Sync for AltrepRegistration {}
389unsafe impl Send for AltrepRegistration {}
390pub unsafe extern "C" fn universal_query(ptr: *mut mx_erased, trait_tag: mx_tag) -> *const c_void {
407 let concrete_tag = unsafe { (*(*ptr).base).concrete_tag };
408 for entry in trait_dispatch().iter() {
409 if entry.concrete_tag == concrete_tag && entry.trait_tag == trait_tag {
410 return entry.vtable;
411 }
412 }
413 std::ptr::null()
414}
415#[unsafe(no_mangle)]
429pub unsafe extern "C" fn miniextendr_register_routines(dll: *mut DllInfo) {
430 let wrapper_gen = crate::init::wrapper_gen_mode();
441 if !wrapper_gen {
442 for reg in altrep_regs().iter() {
448 (reg.register)();
449 }
450
451 crate::altrep::assert_altrep_class_uniqueness();
455 }
456
457 let mut call_defs: Vec<R_CallMethodDef> = self::call_defs().to_vec();
459 #[cfg(not(target_arch = "wasm32"))]
466 {
467 call_defs.push(R_CallMethodDef {
468 name: c"miniextendr_write_wrappers".as_ptr(),
469 fun: unsafe {
470 std::mem::transmute::<
471 *const (),
472 Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
473 >(miniextendr_write_wrappers as *const ())
474 },
475 numArgs: 1,
476 });
477 call_defs.push(R_CallMethodDef {
478 name: c"miniextendr_write_wasm_registry".as_ptr(),
479 fun: unsafe {
480 std::mem::transmute::<
481 *const (),
482 Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
483 >(miniextendr_write_wasm_registry as *const ())
484 },
485 numArgs: 1,
486 });
487 }
488 call_defs.push(R_CallMethodDef {
489 name: std::ptr::null(),
490 fun: None,
491 numArgs: 0,
492 });
493
494 unsafe {
497 crate::sys::R_registerRoutines_unchecked(
498 dll,
499 std::ptr::null(),
500 call_defs.leak().as_ptr(),
501 std::ptr::null(),
502 std::ptr::null(),
503 );
504 }
505}
506
507#[cfg(not(target_arch = "wasm32"))]
514pub fn collect_r_wrappers() -> Vec<std::borrow::Cow<'static, str>> {
515 let mut entries: Vec<&RWrapperEntry> = MX_R_WRAPPERS.iter().collect();
516 entries.sort_by_key(|e| e.priority);
517
518 let mut seen = std::collections::HashSet::<&str>::new();
519 let mut result: Vec<std::borrow::Cow<'static, str>> = Vec::with_capacity(entries.len());
520 for entry in entries {
521 let trimmed = entry.content.trim();
522 if !trimmed.is_empty() && seen.insert(trimmed) {
523 if entry.priority == RWrapperPriority::Function
527 && !has_rdname_tag(trimmed)
528 && !has_no_rd_tag(trimmed)
529 {
530 if let Some(rdname) = rdname_from_source_file(entry.source_file) {
531 result.push(std::borrow::Cow::Owned(inject_rdname(trimmed, &rdname)));
532 continue;
533 }
534 }
535 result.push(std::borrow::Cow::Borrowed(trimmed));
536 }
537 }
538
539 sort_s7_classes(&mut result);
541
542 result
543}
544
545#[cfg(not(target_arch = "wasm32"))]
546fn has_rdname_tag(content: &str) -> bool {
548 content.lines().any(|line| {
549 let trimmed = line.trim();
550 trimmed.starts_with("#' @rdname ")
551 })
552}
553
554#[cfg(not(target_arch = "wasm32"))]
555fn has_no_rd_tag(content: &str) -> bool {
557 content.lines().any(|line| {
558 let trimmed = line.trim();
559 trimmed == "#' @noRd"
560 })
561}
562
563#[cfg(not(target_arch = "wasm32"))]
564fn rdname_from_source_file(path: &str) -> Option<String> {
569 let file_name = path.rsplit(['/', '\\']).next()?;
570 let stem = file_name.strip_suffix(".rs").unwrap_or(file_name);
571 if stem.is_empty() || stem == "lib" || stem == "mod" {
572 return None;
573 }
574 Some(stem.to_string())
575}
576
577#[cfg(not(target_arch = "wasm32"))]
578fn inject_rdname(content: &str, rdname: &str) -> String {
582 let rdname_line = format!("#' @rdname {rdname}");
583 let has_title = content.lines().any(|l| l.trim().starts_with("#' @title "));
584 let title_line = if has_title {
586 None
587 } else {
588 Some(format!("#' @title {}", rdname.replace('_', " ")))
589 };
590
591 let lines: Vec<&str> = content.lines().collect();
592 let mut result = Vec::with_capacity(lines.len() + 2);
593 let mut inserted = false;
594
595 for line in &lines {
596 let trimmed = line.trim();
597 if !inserted
599 && (trimmed.starts_with("#' @export")
600 || trimmed.starts_with("#' @keywords")
601 || trimmed.starts_with("#' @source"))
602 {
603 if let Some(ref t) = title_line {
604 result.push(t.as_str());
605 }
606 result.push(rdname_line.as_str());
607 inserted = true;
608 }
609 result.push(line);
610 }
611
612 if !inserted {
614 let last_roxy = lines
615 .iter()
616 .rposition(|l| l.trim().starts_with("#'"))
617 .unwrap_or(0);
618 let insert_at = last_roxy + 1;
619 if let Some(ref t) = title_line {
620 result.insert(insert_at, t.as_str());
621 result.insert(insert_at + 1, rdname_line.as_str());
622 } else {
623 result.insert(insert_at, rdname_line.as_str());
624 }
625 }
626
627 result.join("\n")
628}
629
630#[cfg(not(target_arch = "wasm32"))]
631fn sort_s7_classes(entries: &mut [std::borrow::Cow<'static, str>]) {
636 use std::collections::HashMap;
637
638 let mut s7_info: Vec<(usize, String, Option<String>)> = Vec::new();
640
641 for (i, entry) in entries.iter().enumerate() {
642 if let Some(nc_pos) = entry.find("S7::new_class(") {
643 let before = entry[..nc_pos].trim_end();
645 let name = before
646 .strip_suffix("<-")
647 .or_else(|| before.rsplit_once("<-").map(|(_, r)| r))
648 .map(|s| s.trim())
649 .and_then(|s| s.split_whitespace().last());
650
651 let Some(name) = name else { continue };
652
653 let after = &entry[nc_pos..];
655 let parent = after.find("parent = ").and_then(|p| {
656 let rest = &after[p + "parent = ".len()..];
657 let end = rest.find([',', ')', '\n']).unwrap_or(rest.len());
658 let p = rest[..end].trim();
659 if p.is_empty() {
660 None
661 } else {
662 Some(p.to_string())
663 }
664 });
665
666 s7_info.push((i, name.to_string(), parent));
667 }
668 }
669
670 if s7_info.len() <= 1 {
671 return;
672 }
673
674 let mut name_to_pos: HashMap<String, usize> = HashMap::new();
679 for (pos, (_, name, _)) in s7_info.iter().enumerate() {
680 name_to_pos.insert(name.clone(), pos);
681 name_to_pos.insert(format!(".__MX_CLASS_REF_{name}__"), pos);
684 }
685
686 let n = s7_info.len();
688 let mut order: Vec<usize> = Vec::with_capacity(n);
689 let mut placed = vec![false; n];
690
691 for _ in 0..n {
692 for (pos, (_, _, parent)) in s7_info.iter().enumerate() {
693 if placed[pos] {
694 continue;
695 }
696 let ready = match parent {
697 None => true,
698 Some(pname) => match name_to_pos.get(pname.as_str()) {
699 None => true, Some(&pp) => placed[pp],
701 },
702 };
703 if ready {
704 order.push(pos);
705 placed[pos] = true;
706 }
707 }
708 }
709
710 for (pos, &is_placed) in placed.iter().enumerate().take(n) {
712 if !is_placed {
713 order.push(pos);
714 }
715 }
716
717 let s7_indices: Vec<usize> = s7_info.iter().map(|(i, _, _)| *i).collect();
719 let original: Vec<std::borrow::Cow<'static, str>> =
720 s7_indices.iter().map(|&i| entries[i].clone()).collect();
721
722 for (slot, &src) in order.iter().enumerate() {
723 entries[s7_indices[slot]] = original[src].clone();
724 }
725}
726#[cfg(not(target_arch = "wasm32"))]
729fn rotate_choices_for_default(choices_str: &str, preferred: &str, placeholder: &str) -> String {
737 let parts: Vec<&str> = choices_str.split(", ").collect();
738 let pos = parts
739 .iter()
740 .position(|p| p.strip_prefix('"').and_then(|s| s.strip_suffix('"')) == Some(preferred))
741 .unwrap_or_else(|| {
742 panic!(
743 "miniextendr: preferred default `{preferred}` for placeholder `{placeholder}` \
744 does not match any choice in [{choices_str}]"
745 )
746 });
747 let mut rotated: Vec<&str> = Vec::with_capacity(parts.len());
748 rotated.push(parts[pos]);
749 for (i, p) in parts.iter().enumerate() {
750 if i != pos {
751 rotated.push(p);
752 }
753 }
754 rotated.join(", ")
755}
756
757#[cfg(not(target_arch = "wasm32"))]
771fn parse_generic_doc_marker(marker: &str) -> Option<(String, String, String, bool, String, bool)> {
772 let inner = marker
774 .strip_prefix(".__MX_GENERIC_DOC__(")?
775 .strip_suffix(')')?;
776
777 let mut kind = String::new();
778 let mut generic = String::new();
779 let mut class = String::new();
780 let mut export = false;
781 let mut dispatch = "x".to_string();
782 let mut no_dots = false;
783
784 for part in inner.split(", ") {
785 let part = part.trim();
786 if let Some(val) = part.strip_prefix("kind=") {
787 kind = val.trim_matches('"').to_string();
788 } else if let Some(val) = part.strip_prefix("generic=") {
789 generic = val.trim_matches('"').to_string();
790 } else if let Some(val) = part.strip_prefix("class=") {
791 class = val.trim_matches('"').to_string();
792 } else if let Some(val) = part.strip_prefix("export=") {
793 export = val == "true";
794 } else if let Some(val) = part.strip_prefix("dispatch=") {
795 dispatch = val.trim_matches('"').to_string();
796 } else if let Some(val) = part.strip_prefix("no_dots=") {
797 no_dots = val == "true";
798 }
799 }
800
801 if kind.is_empty() || generic.is_empty() || class.is_empty() {
802 return None;
803 }
804
805 Some((kind, generic, class, export, dispatch, no_dots))
806}
807
808#[cfg(not(target_arch = "wasm32"))]
820fn synthesise_generic_doc_block(
821 kind: &str,
822 generic: &str,
823 classes: &[String],
824 export: bool,
825 dispatch: &str,
826 no_dots: bool,
827) -> String {
828 let mut lines: Vec<String> = Vec::new();
829
830 lines.push(format!("#' The `{generic}()` generic"));
832 lines.push("#'".to_string());
833
834 let kind_phrase = if kind == "S4" {
836 "an S4 generic"
837 } else {
838 "an S7 generic"
839 };
840 lines.push("#' @description".to_string());
841 lines.push(format!(
842 "#' `{generic}()` is {kind_phrase} generated by miniextendr. Methods in this"
843 ));
844 lines.push("#' package are available for:".to_string());
845 lines.push("#' \\itemize{".to_string());
846 for cls in classes {
847 lines.push(format!("#' \\item \\code{{\\link{{{cls}}}}}"));
848 }
849 lines.push("#' }".to_string());
850
851 let dispatch_args: Vec<&str> = dispatch.split(',').map(|s| s.trim()).collect();
853 for arg in &dispatch_args {
854 lines.push(format!("#' @param {arg} An object."));
855 }
856 if !no_dots {
857 lines.push("#' @param ... Passed on to methods.".to_string());
858 }
859
860 lines.push(format!("#' @name {generic}"));
862
863 if export {
865 if kind == "S4" {
866 lines.push(format!("#' @exportMethod {generic}"));
868 } else {
869 lines.push(format!("#' @rawNamespace export({generic})"));
871 }
872 }
873
874 lines.push("NULL".to_string());
875
876 lines.join("\n")
877}
878
879#[cfg(not(target_arch = "wasm32"))]
886fn resolve_generic_doc_markers(content: String) -> String {
887 use std::collections::HashMap;
888
889 const MARKER_PREFIX: &str = ".__MX_GENERIC_DOC__(";
890
891 let mut by_generic: HashMap<String, (String, Vec<String>, bool, String, bool)> = HashMap::new();
894 let mut generic_order: Vec<String> = Vec::new(); for line in content.lines() {
897 let trimmed = line.trim();
898 if !trimmed.starts_with(MARKER_PREFIX) {
899 continue;
900 }
901 if let Some((kind, generic, class, export, dispatch, no_dots)) =
902 parse_generic_doc_marker(trimmed)
903 {
904 let entry = by_generic.entry(generic.clone()).or_insert_with(|| {
905 generic_order.push(generic.clone());
906 (kind, Vec::new(), export, dispatch, no_dots)
907 });
908 entry.1.push(class);
909 }
910 }
911
912 if by_generic.is_empty() {
913 return content;
914 }
915
916 let mut first_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
920 let mut result = String::with_capacity(content.len() + 512 * by_generic.len());
921
922 for line in content.lines() {
923 let trimmed = line.trim();
924 if trimmed.starts_with(MARKER_PREFIX) {
925 if let Some((_kind, generic, _class, _export, _dispatch, _no_dots)) =
926 parse_generic_doc_marker(trimmed)
927 {
928 if first_seen.insert(generic.clone()) {
929 if let Some((kind, classes, export, dispatch, no_dots)) =
931 by_generic.get(&generic)
932 {
933 let block = synthesise_generic_doc_block(
934 kind, &generic, classes, *export, dispatch, *no_dots,
935 );
936 result.push_str(&block);
937 result.push('\n');
938 }
939 }
940 continue;
942 }
943 }
944 result.push_str(line);
945 result.push('\n');
946 }
947
948 result
951}
952
953#[cfg(not(target_arch = "wasm32"))]
965fn parse_inherited_param_marker(marker: &str) -> Option<(String, String, String, String)> {
966 let inner = marker
967 .strip_prefix(".__MX_INHERITED_PARAM__(")?
968 .strip_suffix(')')?;
969
970 let mut class = String::new();
971 let mut parent = String::new();
972 let mut method = String::new();
973 let mut param = String::new();
974
975 for part in inner.split(", ") {
976 let part = part.trim();
977 if let Some(val) = part.strip_prefix("class=") {
978 class = val.trim_matches('"').to_string();
979 } else if let Some(val) = part.strip_prefix("parent=") {
980 parent = val.trim_matches('"').to_string();
981 } else if let Some(val) = part.strip_prefix("method=") {
982 method = val.trim_matches('"').to_string();
983 } else if let Some(val) = part.strip_prefix("param=") {
984 param = val.trim_matches('"').to_string();
985 }
986 }
987
988 if class.is_empty() || parent.is_empty() || method.is_empty() || param.is_empty() {
989 return None;
990 }
991
992 Some((class, parent, method, param))
993}
994
995#[cfg(not(target_arch = "wasm32"))]
1012fn resolve_inherited_param_markers(content: String) -> String {
1013 use std::collections::{HashMap, HashSet};
1014
1015 const MARKER_PREFIX: &str = ".__MX_INHERITED_PARAM__(";
1016
1017 if !content.contains(MARKER_PREFIX) {
1019 return content;
1020 }
1021
1022 let mut class_method_params: HashMap<(String, String), HashSet<String>> = HashMap::new();
1033
1034 let lines: Vec<&str> = content.lines().collect();
1038 let mut i = 0;
1039 while i < lines.len() {
1040 let line = lines[i].trim();
1041 if let Some(rest) = line.strip_prefix("#' @name ") {
1043 let class_name = rest.trim().to_string();
1044 let mut j = i + 1;
1046 while j < lines.len() {
1047 let next = lines[j].trim();
1048 if next.starts_with("#'") {
1049 if let Some(param_rest) = next.strip_prefix("#' @param ") {
1050 if let Some(param_name) = param_rest.split_whitespace().next() {
1051 class_method_params
1052 .entry((class_name.clone(), String::new()))
1053 .or_default()
1054 .insert(param_name.to_string());
1055 }
1056 }
1057 j += 1;
1058 } else {
1059 break;
1060 }
1061 }
1062 }
1063 i += 1;
1069 }
1070
1071 let mut current_class: Option<String> = None;
1074 let mut current_method: Option<String> = None;
1075 i = 0;
1076 while i < lines.len() {
1077 let raw = lines[i];
1078 let trimmed = raw.trim();
1079 if trimmed.contains("R6::R6Class(\"") {
1081 if let Some(start) = trimmed.find("R6::R6Class(\"") {
1082 let rest = &trimmed[start + "R6::R6Class(\"".len()..];
1083 if let Some(end) = rest.find('"') {
1084 current_class = Some(rest[..end].to_string());
1085 current_method = None;
1086 }
1087 }
1088 }
1089 if let Some(ref cls) = current_class.clone() {
1091 let prefix = format!("# {}::", cls);
1092 if trimmed.starts_with(&prefix) {
1093 let rest = &trimmed[prefix.len()..];
1094 let method_name = rest.split_whitespace().next().unwrap_or("").to_string();
1095 if !method_name.is_empty() {
1096 current_method = Some(method_name);
1097 }
1098 }
1099 if let Some(ref method) = current_method.clone() {
1101 if let Some(rest) = raw.strip_prefix(" #' @param ") {
1102 if let Some(param_name) = rest.split_whitespace().next() {
1103 class_method_params
1104 .entry((cls.clone(), method.clone()))
1105 .or_default()
1106 .insert(param_name.to_string());
1107 }
1108 }
1109 }
1110 }
1111 i += 1;
1112 }
1113
1114 let mut result = String::with_capacity(content.len());
1116 for line in content.lines() {
1117 let trimmed = line.trim();
1118 let inner = trimmed.strip_prefix("#' ").unwrap_or(trimmed);
1120 if inner.starts_with(MARKER_PREFIX) {
1121 if let Some((_class, parent, method, param)) = parse_inherited_param_marker(inner) {
1122 let parent_method_key = (parent.clone(), method.clone());
1125 let parent_class_key = (parent.clone(), String::new());
1126 let documented_in_method = class_method_params
1127 .get(&parent_method_key)
1128 .map(|s| s.contains(¶m))
1129 .unwrap_or(false);
1130 let documented_at_class = class_method_params
1131 .get(&parent_class_key)
1132 .map(|s| s.contains(¶m))
1133 .unwrap_or(false);
1134
1135 if documented_in_method || documented_at_class {
1136 continue;
1139 } else {
1140 let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
1143 result.push_str(&indent);
1144 result.push_str(&format!("#' @param {} (no documentation available)", param));
1145 result.push('\n');
1146 continue;
1147 }
1148 }
1149 }
1150 result.push_str(line);
1151 result.push('\n');
1152 }
1153
1154 result
1155}
1156
1157#[cfg(not(target_arch = "wasm32"))]
1175fn resolve_wrap_return_markers(
1176 content: &str,
1177 prefix: &str,
1178 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
1179 emit: impl Fn(&mut String, Option<&ClassNameEntry>, &str),
1180) -> String {
1181 const ARG_OPEN: &str = "__(";
1182 const ARG_CLOSE: char = ')';
1183
1184 let mut result = String::with_capacity(content.len());
1185 let mut remaining = content;
1186 while let Some(start) = remaining.find(prefix) {
1187 result.push_str(&remaining[..start]);
1188 let rest_from_prefix = &remaining[start..];
1189 let marker_body = &rest_from_prefix[prefix.len()..];
1190 let Some(name_end) = marker_body.find(ARG_OPEN) else {
1191 result.push_str(prefix);
1193 remaining = marker_body;
1194 break;
1195 };
1196 let rust_name = &marker_body[..name_end];
1197 let after_open = &marker_body[name_end + ARG_OPEN.len()..];
1198 let Some(expr_end) = after_open.find(ARG_CLOSE) else {
1199 result.push_str(prefix);
1201 remaining = marker_body;
1202 break;
1203 };
1204 let expr = &after_open[..expr_end];
1205 remaining = &after_open[expr_end + ARG_CLOSE.len_utf8()..];
1206 emit(&mut result, class_index.get(rust_name).copied(), expr);
1207 }
1208 result.push_str(remaining);
1209 result
1210}
1211
1212#[cfg(not(target_arch = "wasm32"))]
1221fn resolve_scalar_return_wrappers(
1222 content: &str,
1223 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
1224) -> String {
1225 resolve_wrap_return_markers(
1226 content,
1227 ".__MX_WRAP_RETURN_",
1228 class_index,
1229 |result, entry, expr| match entry {
1230 Some(entry) => match entry.class_system {
1231 "r6" => {
1232 result.push_str(&format!("{}$new(.ptr = {})", entry.r_class_name, expr));
1233 }
1234 "s7" => {
1235 result.push_str(&format!("{}(.ptr = {})", entry.r_class_name, expr));
1236 }
1237 "s4" => {
1238 result.push_str(&format!(
1239 "methods::new(\"{}\", ptr = {})",
1240 entry.r_class_name, expr
1241 ));
1242 }
1243 _ => {
1244 result.push_str(&format!(
1245 "structure({}, class = \"{}\")",
1246 expr, entry.r_class_name
1247 ));
1248 }
1249 },
1250 None => result.push_str(expr),
1251 },
1252 )
1253}
1254
1255#[cfg(not(target_arch = "wasm32"))]
1263fn resolve_list_return_wrappers(
1264 content: &str,
1265 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
1266) -> String {
1267 resolve_wrap_return_markers(
1268 content,
1269 ".__MX_WRAP_LIST_RETURN_",
1270 class_index,
1271 |result, entry, expr| match entry {
1272 Some(entry) => match entry.class_system {
1273 "r6" => {
1274 result.push_str(&format!(
1275 "lapply({}, function(.el) {}$new(.ptr = .el))",
1276 expr, entry.r_class_name
1277 ));
1278 }
1279 "s7" => {
1280 result.push_str(&format!(
1281 "lapply({}, function(.el) {}(.ptr = .el))",
1282 expr, entry.r_class_name
1283 ));
1284 }
1285 "s4" => {
1286 result.push_str(&format!(
1287 "lapply({}, function(.el) methods::new(\"{}\", ptr = .el))",
1288 expr, entry.r_class_name
1289 ));
1290 }
1291 _ => {
1292 result.push_str(&format!(
1293 "lapply({}, function(.el) structure(.el, class = \"{}\"))",
1294 expr, entry.r_class_name
1295 ));
1296 }
1297 },
1298 None => result.push_str(expr),
1299 },
1300 )
1301}
1302
1303#[cfg(not(target_arch = "wasm32"))]
1313pub fn write_r_wrappers_to_file(path: &str) {
1314 let mut content = String::from(
1316 "# ---- AUTO-GENERATED FILE - DO NOT EDIT ----
1317# This file is generated by the miniextendr proc-macro during package build.
1318# Any manual changes will be overwritten.
1319#
1320# To regenerate: rebuild the package (R CMD INSTALL or devtools::install).
1321# nolint start
1322# nocov start
1323
1324# Internal helper: re-raise a tagged Rust error/condition value as an R condition.
1325# Generated wrappers call this whenever `.Call()` returns a `rust_condition_value`.
1326# `.call_default` is the wrapper's `sys.call()`, used as the fallback when the
1327# Rust panic payload didn't carry a captured call (e.g. lambda contexts that
1328# pass `.call = NULL` to `.Call`). For error/panic kinds `stop()` longjmps;
1329# for warning/message/condition the helper signals and returns invisible(NULL),
1330# which the wrapper's surrounding `return(...)` propagates as its result.
1331.miniextendr_raise_condition <- function(.val, .call_default) {
1332 .msg <- .val$error
1333 .call <- (if (is.null(.val$call)) .call_default else .val$call)
1334 .class <- .val$class
1335 # `.val$data` is an optional named list of structured fields (from the
1336 # macros' `data = ...` form). When present, splice its named elements into
1337 # the condition object alongside message/call/kind so handlers can read
1338 # `e$<name>`. `utils::modifyList` keeps the base fields and appends the
1339 # data fields; a malformed (non-list / unnamed) payload is ignored.
1340 .data <- .val$data
1341 .cond_fields <- function(base) {
1342 if (is.null(.data) || !is.list(.data) || is.null(names(.data))) {
1343 base
1344 } else {
1345 utils::modifyList(base, .data)
1346 }
1347 }
1348 switch(.val$kind,
1349 error = stop(structure(.cond_fields(list(message = .msg, call = .call, kind = \"error\")),
1350 class = c(.class, \"rust_error\", \"simpleError\", \"error\", \"condition\"))),
1351 warning = warning(structure(.cond_fields(list(message = .msg, call = .call, kind = \"warning\")),
1352 class = c(.class, \"rust_warning\", \"simpleWarning\", \"warning\", \"condition\"))),
1353 message = message(structure(.cond_fields(list(message = paste0(.msg, \"\\n\"), call = NULL, kind = \"message\")),
1354 class = c(.class, \"rust_message\", \"simpleMessage\", \"message\", \"condition\"))),
1355 condition = signalCondition(structure(.cond_fields(list(message = .msg, call = .call, kind = \"condition\")),
1356 class = c(.class, \"rust_condition\", \"simpleCondition\", \"condition\"))),
1357 panic = stop(structure(list(message = .msg, call = .call, kind = \"panic\"),
1358 class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\"))),
1359 stop(structure(list(message = .msg, call = .call, kind = .val$kind),
1360 class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\")))
1361 )
1362 invisible(NULL)
1363}
1364
1365",
1366 );
1367
1368 for fragment in collect_r_wrappers() {
1369 content.push_str(fragment.as_ref());
1370 content.push_str("\n\n");
1371 }
1372
1373 content.push_str("# nocov end\n# nolint end\n");
1374
1375 for entry in MX_MATCH_ARG_CHOICES.iter() {
1380 let choices_str = (entry.choices_str)();
1381 let rotated = if entry.preferred_default.is_empty() {
1382 choices_str
1383 } else {
1384 rotate_choices_for_default(&choices_str, entry.preferred_default, entry.placeholder)
1385 };
1386 let replacement = format!("c({rotated})");
1387 content = content.replace(entry.placeholder, &replacement);
1388 }
1389
1390 for entry in MX_MATCH_ARG_PARAM_DOCS.iter() {
1392 let choices = (entry.choices_str)();
1393 let prefix = if entry.several_ok {
1394 "One or more of"
1395 } else {
1396 "One of"
1397 };
1398 let replacement = format!("{prefix} {choices}.");
1399 content = content.replace(entry.placeholder, &replacement);
1400 }
1401
1402 {
1407 use std::collections::HashMap;
1408 let mut by_type: HashMap<&'static str, Vec<&SidecarPropEntry>> = HashMap::new();
1410 for entry in MX_S7_SIDECAR_PROPS.iter() {
1411 by_type.entry(entry.rust_type).or_default().push(entry);
1412 }
1413
1414 const SIDECAR_PREFIX: &str = ".__MX_S7_SIDECAR_PROP_DOCS_";
1415 const SIDECAR_SUFFIX: &str = "__";
1416 let mut result = String::with_capacity(content.len());
1417 let mut remaining = content.as_str();
1418 while let Some(start) = remaining.find(SIDECAR_PREFIX) {
1419 result.push_str(&remaining[..start]);
1420 remaining = &remaining[start + SIDECAR_PREFIX.len()..];
1421 if let Some(end) = remaining.find(SIDECAR_SUFFIX) {
1422 let rust_name = &remaining[..end];
1423 remaining = &remaining[end + SIDECAR_SUFFIX.len()..];
1424 if let Some(entries) = by_type.get(rust_name) {
1426 let prop_lines: String = entries
1427 .iter()
1428 .map(|e| format!("#' @prop {} {}", e.field_name, e.prop_doc))
1429 .collect::<Vec<_>>()
1430 .join("\n");
1431 result.push_str(&prop_lines);
1432 }
1433 } else {
1435 result.push_str(SIDECAR_PREFIX);
1437 break;
1438 }
1439 }
1440 result.push_str(remaining);
1441 content = result;
1442 }
1443
1444 {
1450 let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
1451
1452 const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
1461 const PREFIX: &str = ".__MX_CLASS_REF_";
1462 const SUFFIX: &str = "__";
1463
1464 let mut result = String::with_capacity(content.len());
1465 let mut remaining = content.as_str();
1466 while let Some(start) = remaining.find(PREFIX) {
1467 result.push_str(&remaining[..start]);
1468 let rest_from_prefix = &remaining[start..];
1470 let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
1471 (true, OR_ANY_PREFIX.len())
1472 } else {
1473 (false, PREFIX.len())
1474 };
1475 remaining = &rest_from_prefix[header_len..];
1476 if let Some(end) = remaining.find(SUFFIX) {
1478 let rust_name = &remaining[..end];
1479 remaining = &remaining[end + SUFFIX.len()..];
1480 match class_index.get(rust_name) {
1481 Some(entry) if quiet_fallback => {
1482 if entry.class_system == "s7" {
1488 result.push_str(entry.r_class_name);
1489 } else {
1490 result.push_str("S7::class_any");
1491 }
1492 }
1493 Some(entry) => {
1494 result.push_str(entry.r_class_name);
1495 }
1496 None if quiet_fallback => {
1497 result.push_str("S7::class_any");
1500 }
1501 None => {
1502 eprintln!(
1505 "miniextendr: unresolved class reference `{rust_name}` \
1506 in R wrapper — is the class defined in a reachable crate?"
1507 );
1508 result.push_str(rust_name);
1509 }
1510 }
1511 } else {
1512 result.push_str(PREFIX);
1514 break;
1515 }
1516 }
1517 result.push_str(remaining);
1518 content = result;
1519 }
1520
1521 {
1527 let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
1528 content = resolve_scalar_return_wrappers(&content, &class_index);
1529 content = resolve_list_return_wrappers(&content, &class_index);
1530 }
1531
1532 content = resolve_generic_doc_markers(content);
1549
1550 content = resolve_inherited_param_markers(content);
1563
1564 {
1576 let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
1577 let accessor_producers = sidecar_accessor_names(&MX_S7_SIDECAR_PROPS, &class_index);
1578 if let Err(msg) = detect_duplicate_wrapper_defs(&content, &accessor_producers) {
1579 panic!("{msg}");
1580 }
1581 }
1582
1583 let existing = std::fs::read_to_string(path).unwrap_or_default();
1596 if wrappers_semantically_equal(&existing, &content) {
1597 return;
1598 }
1599
1600 let dest = std::path::Path::new(path);
1610 let tmp = dest.with_extension("tmp");
1611 std::fs::write(&tmp, content.as_bytes())
1612 .unwrap_or_else(|e| panic!("failed to write {}: {e}", tmp.display()));
1613 #[cfg(windows)]
1614 let _ = std::fs::remove_file(dest);
1615 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
1616 panic!(
1617 "failed to rename {} → {}: {e}",
1618 tmp.display(),
1619 dest.display()
1620 )
1621 });
1622
1623 if !existing.is_empty() {
1624 let filename = dest
1625 .file_name()
1626 .and_then(|f| f.to_str())
1627 .unwrap_or("wrappers.R");
1628 eprintln!();
1629 eprintln!("NOTE: {filename} changed — run devtools::document() to update NAMESPACE.");
1630 eprintln!();
1631 }
1632}
1633
1634#[cfg(not(target_arch = "wasm32"))]
1653fn wrappers_semantically_equal(a: &str, b: &str) -> bool {
1654 normalize_source_locs(a) == normalize_source_locs(b)
1655}
1656
1657#[cfg(not(target_arch = "wasm32"))]
1663fn normalize_source_locs(s: &str) -> std::borrow::Cow<'_, str> {
1664 if !s.contains(".rs:") {
1666 return std::borrow::Cow::Borrowed(s);
1667 }
1668
1669 let bytes = s.as_bytes();
1670 let len = bytes.len();
1671 let mut out = String::with_capacity(len);
1672 let mut pos = 0usize;
1673 let mut any_replaced = false;
1674
1675 while pos < len {
1676 let Some(open) = memchr(bytes, pos, b'(') else {
1678 break;
1679 };
1680
1681 let inner_start = open + 1;
1683 let Some(dot_rs) = find_substr(bytes, inner_start, b".rs:") else {
1684 out.push_str(&s[pos..]);
1686 return std::borrow::Cow::Owned(out);
1687 };
1688
1689 let intervening = &bytes[inner_start..dot_rs];
1691 if intervening
1692 .iter()
1693 .any(|&b| b == b')' || b == b'(' || b == b'\n')
1694 {
1695 out.push_str(&s[pos..=open]);
1697 pos = open + 1;
1698 continue;
1699 }
1700
1701 let after_colon1 = dot_rs + 4; let Some(colon2) = scan_digits(bytes, after_colon1) else {
1704 out.push_str(&s[pos..=open]);
1706 pos = open + 1;
1707 continue;
1708 };
1709 if colon2 >= len || bytes[colon2] != b':' {
1710 out.push_str(&s[pos..=open]);
1711 pos = open + 1;
1712 continue;
1713 }
1714
1715 let after_colon2 = colon2 + 1;
1717 let Some(close_pos) = scan_digits(bytes, after_colon2) else {
1718 out.push_str(&s[pos..=open]);
1719 pos = open + 1;
1720 continue;
1721 };
1722 if close_pos >= len || bytes[close_pos] != b')' {
1723 out.push_str(&s[pos..=open]);
1724 pos = open + 1;
1725 continue;
1726 }
1727
1728 any_replaced = true;
1730 out.push_str(&s[pos..inner_start]); out.push_str(&s[inner_start..dot_rs + 3]); out.push_str(":_:_)");
1733 pos = close_pos + 1; }
1735
1736 if !any_replaced {
1737 return std::borrow::Cow::Borrowed(s);
1738 }
1739
1740 out.push_str(&s[pos..]);
1741 std::borrow::Cow::Owned(out)
1742}
1743
1744#[cfg(not(target_arch = "wasm32"))]
1747#[inline]
1748fn find_substr(haystack: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
1749 let window = haystack.get(from..)?;
1750 window
1751 .windows(needle.len())
1752 .position(|w| w == needle)
1753 .map(|rel| from + rel)
1754}
1755
1756#[cfg(not(target_arch = "wasm32"))]
1759#[inline]
1760fn memchr(haystack: &[u8], from: usize, needle: u8) -> Option<usize> {
1761 haystack[from..]
1762 .iter()
1763 .position(|&b| b == needle)
1764 .map(|rel| from + rel)
1765}
1766
1767#[cfg(not(target_arch = "wasm32"))]
1771#[inline]
1772fn scan_digits(haystack: &[u8], from: usize) -> Option<usize> {
1773 let start = haystack.get(from..)?;
1774 let count = start.iter().take_while(|&&b| b.is_ascii_digit()).count();
1775 if count == 0 { None } else { Some(from + count) }
1776}
1777#[cfg(not(target_arch = "wasm32"))]
1795fn sidecar_accessor_names(
1796 sidecar_props: &[SidecarPropEntry],
1797 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
1798) -> std::collections::HashMap<String, String> {
1799 let mut out = std::collections::HashMap::new();
1800 for entry in sidecar_props {
1801 let prefix = class_index
1803 .get(entry.rust_type)
1804 .map(|c| c.r_class_name)
1805 .unwrap_or(entry.rust_type);
1806 let producer = format!(
1807 "#[derive(ExternalPtr)] sidecar field `{}` on `{}`",
1808 entry.field_name, entry.rust_type
1809 );
1810 out.insert(
1811 format!("{prefix}_get_{}", entry.field_name),
1812 producer.clone(),
1813 );
1814 out.insert(format!("{prefix}_set_{}", entry.field_name), producer);
1815 }
1816 out
1817}
1818
1819#[cfg(not(target_arch = "wasm32"))]
1845fn detect_duplicate_wrapper_defs(
1846 content: &str,
1847 accessor_producers: &std::collections::HashMap<String, String>,
1848) -> Result<(), String> {
1849 use std::collections::HashSet;
1850
1851 let mut seen: HashSet<&str> = HashSet::new();
1852
1853 for line in content.lines() {
1854 let Some(name) = parse_top_level_fn_def_name(line) else {
1859 continue;
1860 };
1861
1862 if !seen.insert(name) {
1863 if let Some(producer) = accessor_producers.get(name) {
1865 return Err(format!(
1866 "miniextendr: wrapper function `{name}` is defined more than once. \
1867 One definition comes from {producer}; the other from an S7 fast-path \
1868 shortcut or `#[miniextendr]` function of the same name. The second \
1869 definition silently overwrites the first at load time. Rename the \
1870 colliding S7 method (`#[miniextendr(s7(r_name = \"...\"))]`), opt it \
1871 out of the shortcut (`#[miniextendr(s7(no_shortcut))]`), or rename the \
1872 sidecar field."
1873 ));
1874 }
1875 return Err(format!(
1876 "miniextendr: wrapper function `{name}` is defined more than once in the \
1877 generated R wrappers. Two `#[miniextendr]`-generated top-level functions \
1878 emit the same name and the second silently overwrites the first at load \
1879 time. Common causes: two `#[miniextendr(s7)]` impl blocks (or an S7 \
1880 shortcut and another generated function) sharing a `<Class>_<method>` \
1881 name — rename one (`#[miniextendr(s7(r_name = \"...\"))]`) or opt it out \
1882 of the shortcut (`#[miniextendr(s7(no_shortcut))]`); or two trait impls \
1883 whose generated standalone wrapper names are not class-qualified — rename \
1884 the colliding method (`#[miniextendr(<system>(r_name = \"...\"))]`)."
1885 ));
1886 }
1887 }
1888
1889 Ok(())
1890}
1891
1892#[cfg(not(target_arch = "wasm32"))]
1901fn parse_top_level_fn_def_name(line: &str) -> Option<&str> {
1902 if line.starts_with([' ', '\t']) {
1904 return None;
1905 }
1906 let (lhs, rhs) = line.split_once("<-")?;
1907 if !rhs.trim_start().starts_with("function") {
1909 return None;
1910 }
1911 let name = lhs.trim();
1912 if name.is_empty() {
1913 return None;
1914 }
1915 let mut chars = name.chars();
1920 let first = chars.next()?;
1921 if !(first.is_ascii_alphabetic() || first == '.') {
1922 return None;
1923 }
1924 if name
1925 .chars()
1926 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
1927 {
1928 Some(name)
1929 } else {
1930 None
1931 }
1932}
1933#[cfg(not(target_arch = "wasm32"))]
1953#[unsafe(no_mangle)]
1954pub unsafe extern "C" fn miniextendr_write_wrappers(path_sexp: crate::SEXP) -> crate::SEXP {
1955 unsafe {
1956 use crate::{SEXP, SexpExt};
1957
1958 let char_sexp = path_sexp.string_elt_unchecked(0);
1959 let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
1960 let path = c_str
1961 .to_str()
1962 .unwrap_or_else(|e| panic!("invalid UTF-8 in path: {e}"));
1963
1964 write_r_wrappers_to_file(path);
1965
1966 SEXP::nil()
1967 }
1968}
1969
1970#[cfg(not(target_arch = "wasm32"))]
1981#[unsafe(no_mangle)]
1982pub unsafe extern "C" fn miniextendr_write_wasm_registry(path_sexp: crate::SEXP) -> crate::SEXP {
1983 unsafe {
1984 use crate::{SEXP, SexpExt};
1985
1986 let char_sexp = path_sexp.string_elt_unchecked(0);
1987 let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
1988 let path = c_str
1989 .to_str()
1990 .unwrap_or_else(|e| panic!("invalid UTF-8 in path: {e}"));
1991
1992 crate::wasm_registry_writer::write_wasm_registry_to_file(path);
1993
1994 SEXP::nil()
1995 }
1996}
1997#[cfg(test)]
2002mod tests {
2003 use super::*;
2004
2005 fn resolve_class_refs(content: &str, entries: &[ClassNameEntry]) -> String {
2010 let class_index = build_class_name_index(entries.iter());
2011
2012 const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
2013 const PREFIX: &str = ".__MX_CLASS_REF_";
2014 const SUFFIX: &str = "__";
2015
2016 let mut result = String::with_capacity(content.len());
2017 let mut remaining = content;
2018 while let Some(start) = remaining.find(PREFIX) {
2019 result.push_str(&remaining[..start]);
2020 let rest_from_prefix = &remaining[start..];
2021 let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
2022 (true, OR_ANY_PREFIX.len())
2023 } else {
2024 (false, PREFIX.len())
2025 };
2026 remaining = &rest_from_prefix[header_len..];
2027 if let Some(end) = remaining.find(SUFFIX) {
2028 let rust_name = &remaining[..end];
2029 remaining = &remaining[end + SUFFIX.len()..];
2030 match class_index.get(rust_name) {
2031 Some(entry) if quiet_fallback => {
2032 if entry.class_system == "s7" {
2033 result.push_str(entry.r_class_name);
2034 } else {
2035 result.push_str("S7::class_any");
2036 }
2037 }
2038 Some(entry) => result.push_str(entry.r_class_name),
2039 None if quiet_fallback => result.push_str("S7::class_any"),
2040 None => result.push_str(rust_name),
2041 }
2042 } else {
2043 result.push_str(PREFIX);
2044 break;
2045 }
2046 }
2047 result.push_str(remaining);
2048 result
2049 }
2050
2051 #[test]
2054 fn class_name_index_dedups_identical_entries() {
2055 let entries = [
2056 ClassNameEntry {
2057 rust_type: "Foo",
2058 r_class_name: "Foo",
2059 class_system: "env",
2060 },
2061 ClassNameEntry {
2062 rust_type: "Foo",
2063 r_class_name: "Foo",
2064 class_system: "env",
2065 },
2066 ];
2067 let index = build_class_name_index(entries.iter());
2068 assert_eq!(index.len(), 1);
2069 assert_eq!(index["Foo"].r_class_name, "Foo");
2070 }
2071
2072 #[test]
2076 #[should_panic(expected = "conflicting class registrations for Rust type `Foo`")]
2077 fn class_name_index_panics_on_conflicting_override() {
2078 let entries = [
2079 ClassNameEntry {
2080 rust_type: "Foo",
2081 r_class_name: "Foo",
2082 class_system: "env",
2083 },
2084 ClassNameEntry {
2085 rust_type: "Foo",
2086 r_class_name: "FooOverride",
2087 class_system: "env",
2088 },
2089 ];
2090 let _ = build_class_name_index(entries.iter());
2091 }
2092
2093 fn resolve_return_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
2096 let class_index = build_class_name_index(entries.iter());
2097 resolve_scalar_return_wrappers(content, &class_index)
2098 }
2099
2100 fn resolve_list_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
2103 let class_index = build_class_name_index(entries.iter());
2104 resolve_list_return_wrappers(content, &class_index)
2105 }
2106
2107 #[test]
2108 fn test_class_ref_resolver_with_override() {
2109 let entries = [
2111 ClassNameEntry {
2112 rust_type: "S7Shape",
2113 r_class_name: "Shape",
2114 class_system: "s7",
2115 },
2116 ClassNameEntry {
2117 rust_type: "S7Circle",
2118 r_class_name: "S7Circle",
2119 class_system: "s7",
2120 },
2121 ];
2122
2123 let input = r#"S7Circle <- S7::new_class("S7Circle", parent = .__MX_CLASS_REF_S7Shape__, properties = list())"#;
2124 let output = resolve_class_refs(input, &entries);
2125 assert_eq!(
2126 output,
2127 r#"S7Circle <- S7::new_class("S7Circle", parent = Shape, properties = list())"#
2128 );
2129 }
2130
2131 #[test]
2132 fn test_class_ref_resolver_multiple_placeholders() {
2133 let entries = [
2134 ClassNameEntry {
2135 rust_type: "Point2D",
2136 r_class_name: "Point2D",
2137 class_system: "s7",
2138 },
2139 ClassNameEntry {
2140 rust_type: "Point3D",
2141 r_class_name: "Point3D",
2142 class_system: "s7",
2143 },
2144 ];
2145
2146 let input = "S7::method(convert, list(.__MX_CLASS_REF_Point2D__, Point3D)) <- function(from, to) {}";
2147 let output = resolve_class_refs(input, &entries);
2148 assert_eq!(
2149 output,
2150 "S7::method(convert, list(Point2D, Point3D)) <- function(from, to) {}"
2151 );
2152 }
2153
2154 #[test]
2155 fn test_class_ref_resolver_unresolved_falls_back_to_rust_name() {
2156 let entries: [ClassNameEntry; 0] = [];
2157 let input = "parent = .__MX_CLASS_REF_UnknownClass__";
2158 let output = resolve_class_refs(input, &entries);
2159 assert_eq!(output, "parent = UnknownClass");
2161 }
2162
2163 #[test]
2164 fn test_class_ref_resolver_verbatim_passthrough() {
2165 let entries: [ClassNameEntry; 0] = [];
2168 let input = "parent = S7::class_any";
2169 let output = resolve_class_refs(input, &entries);
2170 assert_eq!(output, "parent = S7::class_any");
2171 }
2172
2173 #[test]
2174 fn test_is_bare_identifier_via_resolver() {
2175 let entries = [ClassNameEntry {
2177 rust_type: "MyClass",
2178 r_class_name: "MyClass",
2179 class_system: "r6",
2180 }];
2181 let input = "inherit = .__MX_CLASS_REF_MyClass__";
2182 let output = resolve_class_refs(input, &entries);
2183 assert_eq!(output, "inherit = MyClass");
2184 }
2185
2186 #[test]
2190 fn or_any_resolves_registered_s7_class() {
2191 let entries = [ClassNameEntry {
2192 rust_type: "S7PropInner",
2193 r_class_name: "S7PropInner",
2194 class_system: "s7",
2195 }];
2196 let input = "class = .__MX_CLASS_REF_OR_ANY_S7PropInner__";
2197 let output = resolve_class_refs(input, &entries);
2198 assert_eq!(output, "class = S7PropInner");
2199 }
2200
2201 #[test]
2202 fn or_any_unregistered_type_falls_back_to_class_any_silently() {
2203 let entries: [ClassNameEntry; 0] = [];
2205 let input = "class = .__MX_CLASS_REF_OR_ANY_SEXP__";
2206 let output = resolve_class_refs(input, &entries);
2207 assert_eq!(output, "class = S7::class_any");
2208 }
2209
2210 #[test]
2211 fn or_any_registered_non_s7_type_falls_back_to_class_any() {
2212 let entries = [ClassNameEntry {
2216 rust_type: "R6Counter",
2217 r_class_name: "R6Counter",
2218 class_system: "r6",
2219 }];
2220 let input = "class = .__MX_CLASS_REF_OR_ANY_R6Counter__";
2221 let output = resolve_class_refs(input, &entries);
2222 assert_eq!(output, "class = S7::class_any");
2223 }
2224
2225 #[test]
2226 fn loud_class_ref_still_emits_bare_name_when_unresolved() {
2227 let entries: [ClassNameEntry; 0] = [];
2231 let input = "parent = .__MX_CLASS_REF_UnknownClass__";
2232 let output = resolve_class_refs(input, &entries);
2233 assert_eq!(output, "parent = UnknownClass");
2234 }
2235
2236 #[test]
2237 fn mixed_placeholders_resolve_independently() {
2238 let entries = [
2241 ClassNameEntry {
2242 rust_type: "Parent",
2243 r_class_name: "Parent",
2244 class_system: "s7",
2245 },
2246 ];
2248 let input =
2249 "inherit = .__MX_CLASS_REF_Parent__, class = .__MX_CLASS_REF_OR_ANY_PropInner__";
2250 let output = resolve_class_refs(input, &entries);
2251 assert_eq!(output, "inherit = Parent, class = S7::class_any");
2252 }
2253
2254 #[test]
2255 fn return_wrapper_resolves_r6_constructor() {
2256 let entries = [ClassNameEntry {
2257 rust_type: "R6Board",
2258 r_class_name: "R6Board",
2259 class_system: "r6",
2260 }];
2261 let input = "return(.__MX_WRAP_RETURN_R6Board__(.val))";
2262 let output = resolve_return_wrappers(input, &entries);
2263 assert_eq!(output, "return(R6Board$new(.ptr = .val))");
2264 }
2265
2266 #[test]
2267 fn return_wrapper_resolves_s7_override_constructor() {
2268 let entries = [ClassNameEntry {
2269 rust_type: "S7Board",
2270 r_class_name: "PrettyBoard",
2271 class_system: "s7",
2272 }];
2273 let input = ".__MX_WRAP_RETURN_S7Board__(.val)";
2274 let output = resolve_return_wrappers(input, &entries);
2275 assert_eq!(output, "PrettyBoard(.ptr = .val)");
2276 }
2277
2278 #[test]
2279 fn return_wrapper_resolves_s4_constructor() {
2280 let entries = [ClassNameEntry {
2281 rust_type: "S4Board",
2282 r_class_name: "S4Board",
2283 class_system: "s4",
2284 }];
2285 let input = ".__MX_WRAP_RETURN_S4Board__(.val)";
2286 let output = resolve_return_wrappers(input, &entries);
2287 assert_eq!(output, "methods::new(\"S4Board\", ptr = .val)");
2288 }
2289
2290 #[test]
2291 fn return_wrapper_resolves_attribute_classes() {
2292 let entries = [
2293 ClassNameEntry {
2294 rust_type: "S3Board",
2295 r_class_name: "S3Board",
2296 class_system: "s3",
2297 },
2298 ClassNameEntry {
2299 rust_type: "EnvBoard",
2300 r_class_name: "EnvBoard",
2301 class_system: "env",
2302 },
2303 ];
2304 let input =
2305 "a <- .__MX_WRAP_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_RETURN_EnvBoard__(.ptr)";
2306 let output = resolve_return_wrappers(input, &entries);
2307 assert_eq!(
2308 output,
2309 "a <- structure(.val, class = \"S3Board\")\nb <- structure(.ptr, class = \"EnvBoard\")"
2310 );
2311 }
2312
2313 #[test]
2314 fn return_wrapper_unregistered_type_falls_back_to_value() {
2315 let entries: [ClassNameEntry; 0] = [];
2316 let input = ".__MX_WRAP_RETURN_JsonValue__(.val)";
2317 let output = resolve_return_wrappers(input, &entries);
2318 assert_eq!(output, ".val");
2319 }
2320
2321 #[test]
2330 fn scalar_resolver_leaves_list_markers_untouched() {
2331 let entries = [ClassNameEntry {
2332 rust_type: "R6Board",
2333 r_class_name: "R6Board",
2334 class_system: "r6",
2335 }];
2336 let input = "a <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
2337 let output = resolve_return_wrappers(input, &entries);
2338 assert_eq!(output, input, "scalar pass must not consume list markers");
2339 }
2340
2341 #[test]
2342 fn list_resolver_leaves_scalar_markers_untouched() {
2343 let entries = [ClassNameEntry {
2344 rust_type: "R6Board",
2345 r_class_name: "R6Board",
2346 class_system: "r6",
2347 }];
2348 let input = "a <- .__MX_WRAP_RETURN_R6Board__(.val)";
2349 let output = resolve_list_wrappers(input, &entries);
2350 assert_eq!(output, input, "list pass must not consume scalar markers");
2351 }
2352
2353 #[test]
2356 fn scalar_and_list_markers_resolve_independently() {
2357 let entries = [ClassNameEntry {
2358 rust_type: "R6Board",
2359 r_class_name: "R6Board",
2360 class_system: "r6",
2361 }];
2362 let input =
2363 "a <- .__MX_WRAP_RETURN_R6Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
2364 let expected = "a <- R6Board$new(.ptr = .val)\nb <- lapply(.val, function(.el) R6Board$new(.ptr = .el))";
2365
2366 let scalar_first =
2367 resolve_list_wrappers(&resolve_return_wrappers(input, &entries), &entries);
2368 let list_first = resolve_return_wrappers(&resolve_list_wrappers(input, &entries), &entries);
2369 assert_eq!(scalar_first, expected);
2370 assert_eq!(list_first, expected);
2371 }
2372
2373 #[test]
2374 fn list_return_wrapper_resolves_r6_lapply() {
2375 let entries = [ClassNameEntry {
2376 rust_type: "R6Board",
2377 r_class_name: "R6Board",
2378 class_system: "r6",
2379 }];
2380 let input = "return(.__MX_WRAP_LIST_RETURN_R6Board__(.val))";
2381 let output = resolve_list_wrappers(input, &entries);
2382 assert_eq!(
2383 output,
2384 "return(lapply(.val, function(.el) R6Board$new(.ptr = .el)))"
2385 );
2386 }
2387
2388 #[test]
2389 fn list_return_wrapper_resolves_s7_override_lapply() {
2390 let entries = [ClassNameEntry {
2391 rust_type: "S7Board",
2392 r_class_name: "PrettyBoard",
2393 class_system: "s7",
2394 }];
2395 let input = ".__MX_WRAP_LIST_RETURN_S7Board__(.val)";
2396 let output = resolve_list_wrappers(input, &entries);
2397 assert_eq!(
2398 output,
2399 "lapply(.val, function(.el) PrettyBoard(.ptr = .el))"
2400 );
2401 }
2402
2403 #[test]
2404 fn list_return_wrapper_resolves_s4_lapply() {
2405 let entries = [ClassNameEntry {
2406 rust_type: "S4Board",
2407 r_class_name: "S4Board",
2408 class_system: "s4",
2409 }];
2410 let input = ".__MX_WRAP_LIST_RETURN_S4Board__(.val)";
2411 let output = resolve_list_wrappers(input, &entries);
2412 assert_eq!(
2413 output,
2414 "lapply(.val, function(.el) methods::new(\"S4Board\", ptr = .el))"
2415 );
2416 }
2417
2418 #[test]
2419 fn list_return_wrapper_resolves_attribute_classes_lapply() {
2420 let entries = [
2421 ClassNameEntry {
2422 rust_type: "S3Board",
2423 r_class_name: "S3Board",
2424 class_system: "s3",
2425 },
2426 ClassNameEntry {
2427 rust_type: "EnvBoard",
2428 r_class_name: "EnvBoard",
2429 class_system: "env",
2430 },
2431 ];
2432 let input = "a <- .__MX_WRAP_LIST_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_EnvBoard__(.ptr)";
2433 let output = resolve_list_wrappers(input, &entries);
2434 assert_eq!(
2435 output,
2436 "a <- lapply(.val, function(.el) structure(.el, class = \"S3Board\"))\nb <- lapply(.ptr, function(.el) structure(.el, class = \"EnvBoard\"))"
2437 );
2438 }
2439
2440 #[test]
2441 fn list_return_wrapper_unregistered_type_falls_back_to_bare_list() {
2442 let entries: [ClassNameEntry; 0] = [];
2443 let input = ".__MX_WRAP_LIST_RETURN_JsonValue__(.val)";
2444 let output = resolve_list_wrappers(input, &entries);
2445 assert_eq!(output, ".val");
2446 }
2447
2448 #[test]
2451 fn normalize_source_locs_noop_on_plain_text() {
2452 let input = "# just a comment\nfoo <- function() {}\n";
2454 let result = normalize_source_locs(input);
2455 assert_eq!(result.as_ref(), input);
2456 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
2458 }
2459
2460 #[test]
2461 fn normalize_source_locs_single_attribution() {
2462 let input = "# Generated from Rust fn `foo` (lib.rs:42:8)";
2463 let result = normalize_source_locs(input);
2464 assert_eq!(
2465 result.as_ref(),
2466 "# Generated from Rust fn `foo` (lib.rs:_:_)"
2467 );
2468 }
2469
2470 #[test]
2471 fn normalize_source_locs_multiple_attributions() {
2472 let input = concat!(
2473 "# (conversions.rs:1:5)\n",
2474 "foo <- function() {}\n",
2475 "# (conversions.rs:158:8)\n",
2476 "bar <- function() {}\n",
2477 );
2478 let result = normalize_source_locs(input);
2479 assert_eq!(
2480 result.as_ref(),
2481 concat!(
2482 "# (conversions.rs:_:_)\n",
2483 "foo <- function() {}\n",
2484 "# (conversions.rs:_:_)\n",
2485 "bar <- function() {}\n",
2486 )
2487 );
2488 }
2489
2490 #[test]
2491 fn normalize_source_locs_does_not_match_extra_colon() {
2492 let input = "(lib.rs:1:5:6)";
2494 let result = normalize_source_locs(input);
2495 assert_eq!(result.as_ref(), input);
2498 }
2499
2500 #[test]
2501 fn normalize_source_locs_does_not_match_without_parens() {
2502 let input = "lib.rs:1:5";
2504 let result = normalize_source_locs(input);
2505 assert_eq!(result.as_ref(), input);
2506 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
2507 }
2508
2509 #[test]
2510 fn wrappers_semantically_equal_position_only_diff() {
2511 let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() {}\n";
2513 let new = "# Generated from Rust fn `foo` (lib.rs:99:8)\nfoo <- function() {}\n";
2514 assert!(wrappers_semantically_equal(old, new));
2515 }
2516
2517 #[test]
2518 fn wrappers_semantically_equal_content_diff() {
2519 let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 1L }\n";
2521 let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 2L }\n";
2522 assert!(!wrappers_semantically_equal(old, new));
2523 }
2524
2525 #[test]
2526 fn wrappers_semantically_equal_both_position_and_content_diff() {
2527 let old = "# Generated from Rust fn `foo` (lib.rs:1:1)\nfoo <- function() { 1L }\n";
2529 let new = "# Generated from Rust fn `bar` (lib.rs:9:1)\nbar <- function() { 2L }\n";
2530 assert!(!wrappers_semantically_equal(old, new));
2531 }
2532
2533 #[test]
2538 fn test_parse_generic_doc_marker_s7() {
2539 let marker = r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#;
2540 let result = parse_generic_doc_marker(marker);
2541 assert!(result.is_some());
2542 let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
2543 assert_eq!(kind, "S7");
2544 assert_eq!(generic, "get_value");
2545 assert_eq!(class, "MyClass");
2546 assert!(export);
2547 assert_eq!(dispatch, "x");
2548 assert!(!no_dots);
2549 }
2550
2551 #[test]
2552 fn test_parse_generic_doc_marker_s4() {
2553 let marker =
2554 r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_get", class="Counter", export=true)"#;
2555 let result = parse_generic_doc_marker(marker);
2556 assert!(result.is_some());
2557 let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
2558 assert_eq!(kind, "S4");
2559 assert_eq!(generic, "s4_get");
2560 assert_eq!(class, "Counter");
2561 assert!(export);
2562 assert_eq!(dispatch, "x"); assert!(!no_dots); }
2565
2566 #[test]
2567 fn test_resolve_generic_doc_markers_single_class() {
2568 let input = concat!(
2569 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#,
2570 "\n",
2571 "if (!exists(\"get_value\", mode = \"function\")) {\n",
2572 " get_value <- S7::new_generic(\"get_value\", \"x\", function(x, ...) S7::S7_dispatch())\n",
2573 "}\n",
2574 );
2575 let output = resolve_generic_doc_markers(input.to_string());
2576
2577 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2579 assert!(output.contains("#' @name get_value"));
2581 assert!(output.contains("\\link{MyClass}"));
2583 assert!(output.contains("\nNULL\n"));
2585 assert!(output.contains("if (!exists(\"get_value\""));
2587 }
2588
2589 #[test]
2590 fn test_resolve_generic_doc_markers_two_classes_one_generic() {
2591 let input = concat!(
2593 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassA", export=true, dispatch="x", no_dots=false)"#,
2594 "\n",
2595 "line_a\n",
2596 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassB", export=true, dispatch="x", no_dots=false)"#,
2597 "\n",
2598 "line_b\n",
2599 );
2600 let output = resolve_generic_doc_markers(input.to_string());
2601
2602 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2604 let name_count = output.matches("#' @name get_value").count();
2606 assert_eq!(
2607 name_count, 1,
2608 "expected exactly one @name get_value, got {name_count}"
2609 );
2610 assert!(output.contains("\\link{ClassA}"));
2612 assert!(output.contains("\\link{ClassB}"));
2613 assert!(output.contains("line_a"));
2615 assert!(output.contains("line_b"));
2616 }
2617
2618 #[test]
2619 fn test_resolve_generic_doc_markers_external_generic_excluded() {
2620 let input = "if (!exists(\"size\", mode = \"function\")) {\n size <- S7::new_external_generic(\"vctrs\", \"size\")\n}\n";
2623 let output = resolve_generic_doc_markers(input.to_string());
2624 let output_trimmed: Vec<&str> = output.lines().collect();
2626 let input_trimmed: Vec<&str> = input.lines().collect();
2627 assert_eq!(output_trimmed, input_trimmed);
2628 }
2629
2630 #[test]
2631 fn test_resolve_generic_doc_markers_no_dots() {
2632 let input = concat!(
2633 r#".__MX_GENERIC_DOC__(kind="S7", generic="strict_fn", class="A", export=true, dispatch="x", no_dots=true)"#,
2634 "\n",
2635 );
2636 let output = resolve_generic_doc_markers(input.to_string());
2637 assert!(!output.contains("@param ..."));
2639 assert!(output.contains("@param x"));
2641 }
2642
2643 #[test]
2644 fn test_resolve_generic_doc_markers_s4() {
2645 let input = concat!(
2646 r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_compute", class="S4Counter", export=true)"#,
2647 "\n",
2648 "if (!methods::isGeneric(\"s4_compute\")) methods::setGeneric(\"s4_compute\", function(x, ...) standardGeneric(\"s4_compute\"))\n",
2649 );
2650 let output = resolve_generic_doc_markers(input.to_string());
2651 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2652 assert!(output.contains("#' @name s4_compute"));
2653 assert!(output.contains("an S4 generic"));
2654 assert!(output.contains("\\link{S4Counter}"));
2655 assert!(output.contains("#' @exportMethod s4_compute"));
2657 assert!(output.contains("if (!methods::isGeneric(\"s4_compute\")"));
2659 }
2660
2661 fn accessor_map(
2668 sidecar: &[SidecarPropEntry],
2669 classes: &[ClassNameEntry],
2670 ) -> std::collections::HashMap<String, String> {
2671 let class_index: std::collections::HashMap<&str, &ClassNameEntry> =
2672 classes.iter().map(|e| (e.rust_type, e)).collect();
2673 sidecar_accessor_names(sidecar, &class_index)
2674 }
2675
2676 #[test]
2677 fn parse_top_level_fn_def_name_accepts_bare_def() {
2678 assert_eq!(
2679 parse_top_level_fn_def_name("Foo_get_value <- function(x) .Call(C, x)"),
2680 Some("Foo_get_value")
2681 );
2682 assert_eq!(
2684 parse_top_level_fn_def_name(".miniextendr_raise_condition <- function(.val) {"),
2685 Some(".miniextendr_raise_condition")
2686 );
2687 }
2688
2689 #[test]
2690 fn parse_top_level_fn_def_name_rejects_non_defs() {
2691 assert_eq!(
2693 parse_top_level_fn_def_name(" get_value = function(self) self$x"),
2694 None
2695 );
2696 assert_eq!(
2698 parse_top_level_fn_def_name("S7::method(get_value, Shape) <- function(x) .Call(C, x)"),
2699 None
2700 );
2701 assert_eq!(
2703 parse_top_level_fn_def_name("obj$method <- function() {}"),
2704 None
2705 );
2706 assert_eq!(parse_top_level_fn_def_name("x <- 1L"), None);
2708 assert_eq!(parse_top_level_fn_def_name("# a comment"), None);
2710 }
2711
2712 #[test]
2713 fn detect_duplicate_clean_content_passes() {
2714 let content = concat!(
2716 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2717 "Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
2718 "Shape_describe <- function(x, ...) .Call(C_describe, x)\n",
2719 );
2720 let map = accessor_map(
2721 &[SidecarPropEntry {
2722 rust_type: "Shape",
2723 field_name: "area",
2724 prop_doc: "",
2725 }],
2726 &[ClassNameEntry {
2727 rust_type: "Shape",
2728 r_class_name: "Shape",
2729 class_system: "s7",
2730 }],
2731 );
2732 assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
2733 }
2734
2735 #[test]
2736 fn detect_duplicate_shortcut_vs_sidecar_accessor_fails() {
2737 let content = concat!(
2740 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2741 "Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
2742 "Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
2743 );
2744 let map = accessor_map(
2745 &[SidecarPropEntry {
2746 rust_type: "Shape",
2747 field_name: "area",
2748 prop_doc: "",
2749 }],
2750 &[ClassNameEntry {
2751 rust_type: "Shape",
2752 r_class_name: "Shape",
2753 class_system: "s7",
2754 }],
2755 );
2756 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2757 assert!(err.contains("Shape_get_area"), "msg: {err}");
2758 assert!(err.contains("sidecar field `area`"), "msg: {err}");
2760 assert!(err.contains("Shape"), "msg: {err}");
2761 }
2762
2763 #[test]
2764 fn detect_duplicate_honours_class_override_prefix() {
2765 let content = concat!(
2769 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2770 "Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
2771 );
2772 let map = accessor_map(
2773 &[SidecarPropEntry {
2774 rust_type: "S7Shape",
2775 field_name: "area",
2776 prop_doc: "",
2777 }],
2778 &[ClassNameEntry {
2779 rust_type: "S7Shape",
2780 r_class_name: "Shape",
2781 class_system: "s7",
2782 }],
2783 );
2784 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2785 assert!(err.contains("Shape_get_area"), "msg: {err}");
2786 assert!(err.contains("sidecar field `area`"), "msg: {err}");
2787 }
2788
2789 #[test]
2790 fn detect_duplicate_cross_impl_block_generic_message() {
2791 let content = concat!(
2794 "Counter_inc <- function(x, ...) .Call(C_inc_a, x)\n",
2795 "Counter_inc <- function(x, ...) .Call(C_inc_b, x)\n",
2796 );
2797 let map = std::collections::HashMap::new();
2798 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2799 assert!(err.contains("Counter_inc"), "msg: {err}");
2800 assert!(err.contains("defined more than once"), "msg: {err}");
2801 }
2802
2803 #[test]
2804 fn detect_duplicate_ignores_repeated_s7_method_assignments() {
2805 let content = concat!(
2808 "S7::method(describe, Shape) <- function(x) .Call(C1, x)\n",
2809 "S7::method(area, Shape) <- function(x) .Call(C2, x)\n",
2810 "S7::method(describe, Circle) <- function(x) .Call(C3, x)\n",
2811 );
2812 let map = std::collections::HashMap::new();
2813 assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
2814 }
2815
2816 }
2818