Skip to main content

miniextendr_macros/
util.rs

1//! Small cross-cutting helpers used by multiple macro modules.
2
3/// Extract `#[cfg(...)]` attributes from a list of attributes.
4///
5/// These should be propagated to generated items so they are conditionally
6/// compiled along with the original function.
7pub(crate) fn extract_cfg_attrs(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
8    attrs
9        .iter()
10        .filter(|attr| attr.path().is_ident("cfg"))
11        .cloned()
12        .collect()
13}
14
15/// Rewrites a leading `Self` in `path` to `type_ident`: a path an attribute
16/// names (`#[r_data(get = "Self::f")]`, `fallback = "Self::f"`) is called
17/// from a free function the macro emits outside any impl, where `Self` means
18/// nothing.
19pub(crate) fn resolve_self(path: &mut syn::Path, type_ident: &syn::Ident) {
20    if let Some(first) = path.segments.first_mut()
21        && first.ident == "Self"
22    {
23        first.ident = syn::Ident::new(&type_ident.to_string(), first.ident.span());
24    }
25}
26
27/// Format a human-readable source location note from a syntax span.
28///
29/// Column is reported as 1-based for consistency with editor displays.
30pub(crate) fn source_location_doc(span: proc_macro2::Span) -> String {
31    let start = span.start();
32    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Generated from source location line {0}, column {1}.",
                start.line, start.column + 1))
    })format!(
33        "Generated from source location line {}, column {}.",
34        start.line,
35        start.column + 1
36    )
37}
38
39/// Build a `TokenStream` containing a raw string literal from an R wrapper string.
40///
41/// Every generated wrapper is finalized here, so this is also where calls a
42/// formal would shadow are qualified: `formals` holds every R formal of the
43/// wrappers in `s`, and each call to a base function that shares a name with
44/// one of them is written `base::name(...)`
45/// ([`qualify_shadowed_calls`](crate::r_shadowing::qualify_shadowed_calls)).
46///
47/// The delimiter has one more `#` than the longest run of `#` after a `"` in
48/// the wrapper, so a doc comment quoting `"# a footer"` or a default of `"#"`
49/// can't end the literal early (#2111).
50pub(crate) fn r_wrapper_raw_literal(
51    s: &str,
52    formals: &std::collections::BTreeSet<String>,
53) -> proc_macro2::TokenStream {
54    use std::str::FromStr;
55    let s = crate::r_shadowing::qualify_shadowed_calls(s, formals);
56    let hashes = "#".repeat(raw_string_hashes(&s));
57    let raw = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("r{0}\"\n{1}\n\"{0}", hashes, s))
    })format!("r{hashes}\"\n{s}\n\"{hashes}");
58    proc_macro2::TokenStream::from_str(&raw).expect("valid raw string literal")
59}
60
61/// How many `#` a raw string literal holding `s` needs: one more than the
62/// longest run of `#` that follows a `"` in `s`.
63fn raw_string_hashes(s: &str) -> usize {
64    let longest = s
65        .match_indices('"')
66        .map(|(i, _)| s[i + 1..].bytes().take_while(|&b| b == b'#').count())
67        .max()
68        .unwrap_or(0);
69    longest + 1
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    /// The literal parses back to the wrapper text, newline-framed, however
77    /// many `#` follow a quote in it (#2111).
78    #[test]
79    fn raw_literal_holds_quote_hash_runs() {
80        let formals = std::collections::BTreeSet::new();
81        for text in [
82            "plain",
83            "#' the last line reads \"# Not shown: sd\".",
84            "f <- function(sep = \"#\") sep",
85            "\"##\" and \"###x\"",
86            "ends with a quote\"",
87        ] {
88            let literal: syn::LitStr = syn::parse2(r_wrapper_raw_literal(text, &formals))
89                .unwrap_or_else(|e| panic!("{text:?}: {e}"));
90            assert_eq!(literal.value(), format!("\n{text}\n"));
91        }
92    }
93
94    #[test]
95    fn raw_string_hashes_counts_runs_after_quotes() {
96        assert_eq!(raw_string_hashes("no quotes ##"), 1);
97        assert_eq!(raw_string_hashes("\"x\""), 1);
98        assert_eq!(raw_string_hashes("\"#"), 2);
99        assert_eq!(raw_string_hashes("\"## then \"#"), 3);
100    }
101}