Skip to main content

miniextendr_macros/
naming.rs

1//! Naming helpers for the static symbols the `#[miniextendr]` attribute emits.
2//!
3//! These formatters live in one place so the attribute macro and any later
4//! consumer (e.g. a registration-chasing linter) can compute the exact same
5//! identifiers from a source `syn::Ident`.
6
7/// Identifier for the generated `const &str` holding the R wrapper source.
8///
9/// Returns `R_WRAPPER_{RUST_IDENT}` (uppercased).
10pub(crate) fn r_wrapper_const_ident_for(rust_ident: &syn::Ident) -> syn::Ident {
11    let upper = rust_ident.to_string().to_uppercase();
12    quote::format_ident!("R_WRAPPER_{upper}")
13}
14
15// region: crate-prefixed C symbol naming (#1273)
16//
17// Every `#[miniextendr]`-emitted `#[no_mangle]` C symbol is prefixed with the
18// consuming crate's name so that two packages loaded into the same webR
19// session (which share one GOT under Emscripten's SIDE_MODULE model) can't
20// have the first-loaded package's definition of a name silently win for a
21// later package's identically-named export. Native `dyn.load` with
22// `RTLD_LOCAL` was never affected — this is purely a wasm cross-package
23// hygiene fix — but the prefix is applied unconditionally on every target so
24// there's a single code path and no target-dependent symbol shape.
25//
26// Registration name and linker symbol name are kept identical everywhere
27// (the invariant the wasm snapshot writer,
28// `miniextendr-api/src/wasm_registry_writer.rs`, depends on to reconstruct
29// `extern "C" { fn <name>(...); }` blocks from `R_CallMethodDef.name`) — so
30// every formatter below produces the *one* string used for both the
31// `#[no_mangle]` fn/static definition and its registration entry.
32
33/// The consuming crate's name, used as the prefix for every macro-emitted
34/// `#[no_mangle]` C symbol.
35///
36/// Reads `CARGO_CRATE_NAME`, which cargo sets (with hyphens already
37/// normalized to underscores) for the rustc invocation that expands this
38/// proc macro — the same precedent `miniextendr_init!()` relies on
39/// (`lib.rs`, `miniextendr_init` fn). Real macro expansion (i.e. compiling a
40/// downstream crate through cargo) always has this set.
41///
42/// Falls back to `CARGO_PKG_NAME` (normalized the same way, since it is
43/// *not* auto-normalized) for the one context where `CARGO_CRATE_NAME` is
44/// absent: this crate's own unit/insta tests call codegen functions directly
45/// at test *runtime* rather than through real macro expansion, and cargo
46/// does not forward `CARGO_CRATE_NAME` to a test binary's process
47/// environment (verified empirically — only `CARGO_PKG_*` vars are). Falls
48/// back further to the literal `"crate"` if even that is absent (e.g. a
49/// direct `rustc` invocation bypassing cargo entirely).
50pub(crate) fn crate_prefix() -> String {
51    std::env::var("CARGO_CRATE_NAME").unwrap_or_else(|_| {
52        std::env::var("CARGO_PKG_NAME")
53            .map(|n| n.replace('-', "_"))
54            .unwrap_or_else(|_| "crate".to_string())
55    })
56}
57
58/// `C_<crate>_<fn>` — bare-fn C wrapper identifier (internal-wrapper arm
59/// only; `extern`/`#[export_name]` fns pass through untouched and own their
60/// own cross-package uniqueness).
61pub(crate) fn bare_fn_c_wrapper_ident(rust_ident: &syn::Ident) -> syn::Ident {
62    let prefix = crate_prefix();
63    quote::format_ident!("C_{prefix}_{rust_ident}")
64}
65
66/// `C_<crate>_<Type>__<method>`, or `C_<crate>_<Type>_<label>_<method>` when
67/// the impl block carries a label — impl-method C wrapper identifier.
68pub(crate) fn impl_method_c_wrapper_ident(
69    type_ident: &syn::Ident,
70    label: Option<&str>,
71    method_ident: &syn::Ident,
72) -> syn::Ident {
73    let prefix = crate_prefix();
74    if let Some(label) = label {
75        quote::format_ident!("C_{prefix}_{type_ident}_{label}_{method_ident}")
76    } else {
77        quote::format_ident!("C_{prefix}_{type_ident}__{method_ident}")
78    }
79}
80
81/// `C_<crate>_<Type>__<Trait>__<member>` as a `String` — shared by
82/// `TraitMethod` and `TraitConst`, and by both the ident and string forms
83/// each needs, so the format string can't drift across the four call sites
84/// that used to duplicate it.
85pub(crate) fn trait_member_c_wrapper_string(
86    type_ident: &syn::Ident,
87    trait_name: &syn::Ident,
88    member_ident: &syn::Ident,
89) -> String {
90    let prefix = crate_prefix();
91    format!("C_{prefix}_{type_ident}__{trait_name}__{member_ident}")
92}
93
94/// Same as [`trait_member_c_wrapper_string`], as a `syn::Ident` for Rust
95/// token generation.
96pub(crate) fn trait_member_c_wrapper_ident(
97    type_ident: &syn::Ident,
98    trait_name: &syn::Ident,
99    member_ident: &syn::Ident,
100) -> syn::Ident {
101    quote::format_ident!(
102        "{}",
103        trait_member_c_wrapper_string(type_ident, trait_name, member_ident)
104    )
105}
106
107/// `C_<crate>__mx_rdata_get_<Type>_<field>` — sidecar field getter C symbol.
108pub(crate) fn sidecar_getter_c_name(type_name: &str, field_name: &str) -> String {
109    let prefix = crate_prefix();
110    format!("C_{prefix}__mx_rdata_get_{type_name}_{field_name}")
111}
112
113/// `C_<crate>__mx_rdata_set_<Type>_<field>` — sidecar field setter C symbol.
114pub(crate) fn sidecar_setter_c_name(type_name: &str, field_name: &str) -> String {
115    let prefix = crate_prefix();
116    format!("C_{prefix}__mx_rdata_set_{type_name}_{field_name}")
117}
118
119/// `__mx_altrep_reg_<crate>_<Ident>` — ALTREP class registration fn.
120pub(crate) fn altrep_reg_fn_ident(ident: &syn::Ident) -> syn::Ident {
121    let prefix = crate_prefix();
122    quote::format_ident!("__mx_altrep_reg_{prefix}_{ident}")
123}
124
125/// `__VTABLE_<CRATE>_<TRAIT>_FOR_<TYPE>` — trait-impl vtable static.
126///
127/// `trait_name_upper` / `type_name_str` are already uppercased by the
128/// caller (matching the pre-existing convention); the crate prefix is
129/// uppercased here to match.
130pub(crate) fn vtable_static_ident(trait_name_upper: &str, type_name_str: &str) -> syn::Ident {
131    let crate_upper = crate_prefix().to_uppercase();
132    quote::format_ident!("__VTABLE_{crate_upper}_{trait_name_upper}_FOR_{type_name_str}")
133}
134
135/// `__vtshim_<crate>_<Type>__<Trait>__<method>` — trait-impl vtable shim fn.
136pub(crate) fn vtshim_ident(
137    type_ident: &syn::Ident,
138    trait_name: &syn::Ident,
139    method_ident: &syn::Ident,
140) -> syn::Ident {
141    let prefix = crate_prefix();
142    quote::format_ident!("__vtshim_{prefix}_{type_ident}__{trait_name}__{method_ident}")
143}
144// endregion
145
146/// Convert a PascalCase string to snake_case.
147///
148/// Inserts an underscore before each uppercase letter (except the first),
149/// then lowercases the entire result. For example, `"InProgress"` becomes
150/// `"in_progress"`.
151pub(crate) fn to_snake_case(s: &str) -> String {
152    let mut out = String::new();
153    for (i, c) in s.char_indices() {
154        if c.is_uppercase() && i > 0 {
155            out.push('_');
156        }
157        out.extend(c.to_lowercase());
158    }
159    out
160}
161
162/// Convert a PascalCase string to kebab-case (`InProgress` → `in-progress`).
163pub(crate) fn to_kebab_case(s: &str) -> String {
164    to_snake_case(s).replace('_', "-")
165}
166
167/// Apply a `rename_all` transformation to a variant name.
168///
169/// Supports `"snake_case"`, `"kebab-case"`, `"lower"`, `"upper"`. Returns the
170/// name unchanged if `rename_all` is `None` or unrecognised.
171pub(crate) fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
172    match rename_all {
173        Some("snake_case") => to_snake_case(name),
174        Some("kebab-case") => to_kebab_case(name),
175        Some("lower") => name.to_lowercase(),
176        Some("upper") => name.to_uppercase(),
177        _ => name.to_string(),
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    // At unit-test runtime cargo does not forward CARGO_CRATE_NAME to the
186    // test binary's process env, so crate_prefix() takes the CARGO_PKG_NAME
187    // fallback: "miniextendr-macros" → "miniextendr_macros". Real macro
188    // expansion (inside rustc) always sees CARGO_CRATE_NAME. The insta
189    // snapshots pin the same value; if this test fails the snapshots are
190    // stale too.
191    #[test]
192    fn crate_prefix_fallback_is_normalized_pkg_name() {
193        assert_eq!(crate_prefix(), "miniextendr_macros");
194    }
195
196    #[test]
197    fn c_symbols_are_crate_prefixed() {
198        let ty = quote::format_ident!("Counter");
199        let tr = quote::format_ident!("Resettable");
200        let m = quote::format_ident!("reset");
201        assert_eq!(
202            bare_fn_c_wrapper_ident(&m).to_string(),
203            "C_miniextendr_macros_reset"
204        );
205        assert_eq!(
206            impl_method_c_wrapper_ident(&ty, None, &m).to_string(),
207            "C_miniextendr_macros_Counter__reset"
208        );
209        assert_eq!(
210            impl_method_c_wrapper_ident(&ty, Some("basic"), &m).to_string(),
211            "C_miniextendr_macros_Counter_basic_reset"
212        );
213        assert_eq!(
214            trait_member_c_wrapper_string(&ty, &tr, &m),
215            "C_miniextendr_macros_Counter__Resettable__reset"
216        );
217        assert_eq!(
218            trait_member_c_wrapper_ident(&ty, &tr, &m).to_string(),
219            trait_member_c_wrapper_string(&ty, &tr, &m)
220        );
221        assert_eq!(
222            sidecar_getter_c_name("Counter", "count"),
223            "C_miniextendr_macros__mx_rdata_get_Counter_count"
224        );
225        assert_eq!(
226            sidecar_setter_c_name("Counter", "count"),
227            "C_miniextendr_macros__mx_rdata_set_Counter_count"
228        );
229        assert_eq!(
230            altrep_reg_fn_ident(&ty).to_string(),
231            "__mx_altrep_reg_miniextendr_macros_Counter"
232        );
233        assert_eq!(
234            vtable_static_ident("RESETTABLE", "COUNTER").to_string(),
235            "__VTABLE_MINIEXTENDR_MACROS_RESETTABLE_FOR_COUNTER"
236        );
237        assert_eq!(
238            vtshim_ident(&ty, &tr, &m).to_string(),
239            "__vtshim_miniextendr_macros_Counter__Resettable__reset"
240        );
241    }
242
243    #[test]
244    fn snake_case() {
245        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
246        assert_eq!(to_snake_case("InProgress"), "in_progress");
247        assert_eq!(to_snake_case("ABC"), "a_b_c");
248        assert_eq!(to_snake_case("Red"), "red");
249    }
250
251    #[test]
252    fn kebab_case() {
253        assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
254        assert_eq!(to_kebab_case("InProgress"), "in-progress");
255    }
256
257    #[test]
258    fn rename_all() {
259        assert_eq!(
260            apply_rename_all("HelloWorld", Some("snake_case")),
261            "hello_world"
262        );
263        assert_eq!(
264            apply_rename_all("HelloWorld", Some("kebab-case")),
265            "hello-world"
266        );
267        assert_eq!(apply_rename_all("HelloWorld", Some("lower")), "helloworld");
268        assert_eq!(apply_rename_all("HelloWorld", Some("upper")), "HELLOWORLD");
269        assert_eq!(apply_rename_all("HelloWorld", None), "HelloWorld");
270    }
271}