miniextendr_macros/miniextendr_impl_trait/method_context.rs
1//! Shared per-method context for trait-impl R wrapper generation.
2//!
3//! Parallels `r_class_formatter::MethodContext` (used by the 6 inherent-impl
4//! class generators) so the 5 trait generators (env/s3/s4/s7/r6) build
5//! `.Call()` invocations and method preludes the same way instead of each
6//! hand-rolling its own version. See
7//! `audit/2026-07-03-dogfooding-macros-codegen.md` finding #1: the trait path
8//! previously shared nothing above `DotCallBuilder`, which caused a
9//! substring-corruption bug in the receiver-ptr extraction (S4/S7/R6) and a
10//! silent 3-of-6-step prelude omission (missing `precondition_checks` /
11//! `match_arg_prelude`) relative to inherent methods.
12
13use super::TraitMethod;
14
15/// Pre-computed context for a trait method, mirroring `MethodContext`
16/// (`r_class_formatter.rs`) for inherent-impl methods. Holds the C wrapper
17/// name, R formals (with defaults), and `.Call()` argument string so all 5
18/// trait generators build calls identically.
19pub(super) struct TraitMethodContext<'a> {
20 /// Reference to the parsed trait method metadata.
21 pub(super) method: &'a TraitMethod,
22 /// The C wrapper identifier string (e.g., `"C_Foo__Bar__value"`), used in `.Call()`.
23 pub(super) c_ident: String,
24 /// R formals string with defaults, used in `function(...)` signatures.
25 pub(super) params: String,
26 /// R call arguments string (without defaults), used inside `.Call()`
27 /// expressions. `Missing<T>` parameters are forwarded as
28 /// `if (missing(p)) quote(expr=) else p` — see
29 /// `r_wrapper_builder::RArgumentBuilder::build_call_args_vec`. Before this
30 /// context existed, trait methods built call args via
31 /// `collect_param_idents` instead, which skipped that forwarding entirely.
32 pub(super) args: String,
33}
34
35impl<'a> TraitMethodContext<'a> {
36 /// Build a context for `method`, which implements `trait_name` for `type_ident`.
37 pub(super) fn new(
38 method: &'a TraitMethod,
39 type_ident: &syn::Ident,
40 trait_name: &syn::Ident,
41 ) -> Self {
42 let c_ident = method.c_wrapper_ident_string(type_ident, trait_name);
43 // match_arg/choices formal defaults are load-bearing for match.arg()
44 // (see `effective_r_defaults` docs) — not just cosmetic.
45 let effective_defaults = crate::r_class_formatter::effective_r_defaults(
46 &method.param_defaults,
47 &method.per_param,
48 &c_ident,
49 );
50 let params =
51 crate::r_wrapper_builder::build_r_formals_from_sig(&method.sig, &effective_defaults);
52 let args = crate::r_wrapper_builder::build_r_call_args_from_sig(&method.sig);
53 Self {
54 method,
55 c_ident,
56 params,
57 args,
58 }
59 }
60
61 /// Build the `.Call()` expression for a static (non-receiver) trait method.
62 pub(super) fn static_call(&self) -> String {
63 crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
64 .with_args_str(&self.args)
65 .build()
66 }
67
68 /// Build the `.Call()` expression for an instance trait method, with
69 /// `self_expr` passed directly as the receiver argument (e.g. `".ptr"`,
70 /// `"x"`, `"self@.ptr"`).
71 ///
72 /// This is the structured equivalent of `MethodContext::instance_call` —
73 /// no string surgery. It fixes the substring-corruption bug where S4/S7/R6
74 /// built the call with `self = "x"` and then ran
75 /// `call.replace(", x", ", .ptr")`: `str::replace` rewrites *every* match
76 /// of the substring `", x"`, so a parameter whose R name started with `x`
77 /// (e.g. `x_factor`) was corrupted into `.ptr_factor`, producing a runtime
78 /// "object '.ptr_factor' not found" error. Passing the receiver expression
79 /// directly to `with_self` never touches the other arguments.
80 pub(super) fn instance_call(&self, self_expr: &str) -> String {
81 crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
82 .with_self(self_expr)
83 .with_args_str(&self.args)
84 .build()
85 }
86
87 /// Build R prelude lines validating `match_arg`/`choices` params. See
88 /// `r_class_formatter::build_match_arg_prelude`.
89 pub(super) fn match_arg_prelude(&self) -> Vec<String> {
90 crate::r_class_formatter::build_match_arg_prelude(&self.method.per_param)
91 }
92
93 /// R-side `stopifnot()` precondition checks for this method's parameters.
94 /// See `MethodContext::precondition_checks` for the inherent-impl twin.
95 pub(super) fn precondition_checks(&self) -> Vec<String> {
96 crate::r_class_formatter::build_method_precondition_checks(
97 &self.method.sig.inputs,
98 &self.method.per_param,
99 self.method.coerce,
100 )
101 }
102
103 /// Emit the shared method prelude into `lines`, each line prefixed with
104 /// `indent` — the trait-impl twin of `MethodContext::emit_method_prelude`.
105 /// In order: `r_entry`, `r_on_exit`, `lifecycle_prelude`,
106 /// `precondition_checks`, `match_arg_prelude`, `r_post_checks`.
107 ///
108 /// `Missing<T>` forwarding is not a prelude step here either — it's inline
109 /// in `self.args` (built in `new`), matching the inherent path.
110 ///
111 /// `what` is the human-readable label passed to the lifecycle prelude.
112 /// This mirrors the pre-refactor `trait_method_preamble_lines`, which
113 /// always used the R-facing method name as `what` regardless of class
114 /// system (inherent methods instead use a class-qualified label like
115 /// `"Type.method"` — trait methods keep the simpler unqualified label to
116 /// avoid changing existing lifecycle-warning wording).
117 pub(super) fn emit_method_prelude(&self, lines: &mut Vec<String>, indent: &str, what: &str) {
118 let m = self.method;
119 if let Some(ref entry) = m.r_entry {
120 for line in entry.lines() {
121 lines.push(format!("{indent}{line}"));
122 }
123 }
124 if let Some(ref on_exit) = m.r_on_exit {
125 lines.push(format!("{indent}{}", on_exit.to_r_code()));
126 }
127 if let Some(ref spec) = m.lifecycle
128 && let Some(prelude) = spec.r_prelude(what)
129 {
130 for line in prelude.lines() {
131 lines.push(format!("{indent}{line}"));
132 }
133 }
134 for check in self.precondition_checks() {
135 lines.push(format!("{indent}{check}"));
136 }
137 for line in self.match_arg_prelude() {
138 lines.push(format!("{indent}{line}"));
139 }
140 if let Some(ref post) = m.r_post_checks {
141 for line in post.lines() {
142 lines.push(format!("{indent}{line}"));
143 }
144 }
145 }
146}