Skip to main content

announce_core/
options.rs

1use proc_macro2::{Ident, TokenStream};
2use syn::parse::{Parse, ParseStream};
3use syn::{Error, LitBool, Path, Result, Token};
4use template_quote::{quote, ToTokens};
5
6/// One check or `meta` entry of the attribute: `path` or `path(args)`.
7///
8/// As a check (`#[announce(my_lib::check(args))]`) it runs at the definition, as
9/// `path! { @announce_format "1" (args), ITEM }`. As a `meta` entry it is only listed in
10/// `META`.
11#[derive(Clone)]
12pub struct Check {
13    /// The macro path.
14    pub path: Path,
15    /// The tokens inside the parentheses, if there were any.
16    pub args: Option<TokenStream>,
17}
18
19impl ToTokens for Check {
20    fn to_tokens(&self, tokens: &mut TokenStream) {
21        let p = &self.path;
22        match &self.args {
23            Some(a) => tokens.extend(quote! { #p(#a) }),
24            None => tokens.extend(quote! { #p }),
25        }
26    }
27}
28
29/// How the generated trampoline holds the item (`layout = ..`, advanced).
30///
31/// The default, [`Layout::Auto`], is the right choice almost always.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum Layout {
35    /// [`Layout::Direct`] for a small item, [`Layout::Single`] for a large one
36    /// (more than [`SINGLE_COPY_THRESHOLD`](crate::SINGLE_COPY_THRESHOLD)
37    /// tokens with its `META`).
38    Auto,
39    /// The item is held once, so the macro's metadata is about the size of one
40    /// item, and one call takes three expansions (two through the derive route)
41    /// instead of one.
42    Single,
43    /// One expansion per call; the item is copied into each of the macro's
44    /// three output arms.
45    Direct,
46}
47
48/// The parsed arguments of `#[announce(..)]`.
49///
50/// Parse it from the attribute's tokens with [`syn::parse2`]; the grammar and
51/// the meaning of every option are those of the `#[announce]` attribute
52/// (`announce::announce`). Build one in code with [`Options::default`] and
53/// the public fields. Turn it back into attribute arguments with
54/// [`ToTokens`] (`opts.to_token_stream()`), for example to hand it to
55/// [`announce()`](crate::announce).
56///
57/// The most useful field for a library is `krate`: the path to the `announce`
58/// crate that the generated code uses (see [`Options::krate()`]). Without it the
59/// path is read from the `Cargo.toml` of the crate being compiled, which only
60/// works when that crate depends on `announce` itself.
61#[derive(Clone)]
62#[non_exhaustive]
63pub struct Options {
64    /// `macro_name = Name`: name of the emitted macro. Default: the item's name. All
65    /// the announcements of one item must give the same name, the item's name where
66    /// it is not set: one item has one trampoline.
67    pub macro_name: Option<Ident>,
68    /// `mock = path`: the item is a stand-in for the item at `path`.
69    pub mock: Option<Path>,
70    /// `repeater = path`: emit the type-leak table against this repeater trait.
71    pub repeater: Option<Path>,
72    /// type-leak: primitives resolve everywhere (default `true`).
73    pub allow_primitive: bool,
74    /// type-leak: std prelude names resolve everywhere (default `false`).
75    pub allow_std_prelude: bool,
76    /// `crate = path`: path to the `announce` crate in generated code. When it
77    /// is `None`, [`Options::krate()`] looks the crate up in `Cargo.toml`.
78    pub krate: Option<Path>,
79    /// `emit_item = false`: do not re-emit the item (it is emitted by someone else).
80    /// Default `true`.
81    pub emit_item: bool,
82    /// `layout = auto | single | direct`: how the trampoline holds the item.
83    /// Default `auto`: by the size of the item (advanced).
84    pub layout: Layout,
85    /// `docs = true`: keep the doc attributes (`#[doc = ".."]`, so `///` and
86    /// `//!` too) in the item that callbacks and checks receive. Default
87    /// `false`: they are removed there, which makes the trampoline smaller.
88    /// The item that is compiled keeps its docs either way.
89    pub docs: bool,
90    /// The option keys that were written (a value that is set is told from a default).
91    pub(crate) given: Vec<String>,
92    /// Checks: bare `path` / `path(args)` entries. They run at the definition.
93    pub checks: Vec<Check>,
94    /// `meta = path(args)`: listed in META like a check, but never invoked.
95    pub metas: Vec<Check>,
96}
97
98impl Default for Options {
99    fn default() -> Self {
100        Options {
101            macro_name: None,
102            mock: None,
103            repeater: None,
104            allow_primitive: true,
105            allow_std_prelude: false,
106            krate: None,
107            emit_item: true,
108            layout: Layout::Auto,
109            docs: false,
110            given: Vec::new(),
111            checks: Vec::new(),
112            metas: Vec::new(),
113        }
114    }
115}
116
117impl Options {
118    /// The path to the `announce` crate that the generated code uses.
119    ///
120    /// `crate = ..` (the `krate` field) wins. Otherwise it is
121    /// the name that the `Cargo.toml` of the crate being compiled
122    /// (`CARGO_MANIFEST_DIR`) gives to its `announce` dependency, renames
123    /// included, else `::announce`.
124    pub fn krate(&self) -> Path {
125        self.krate
126            .clone()
127            .unwrap_or_else(crate::cratepath::default_crate_path)
128    }
129}
130
131/// Two announcements of one item disagree on an option that has one value only
132/// (see [`Options::merge`]).
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub(crate) struct MergeConflict {
135    /// The name of the option, as written in the attribute. For `macro_name` the
136    /// values are the effective trampoline names: the item's name where it is not set.
137    pub option: &'static str,
138    /// Its value in the announcement that is merged into.
139    pub target: String,
140    /// Its value in the announcement that is merged.
141    pub folder: String,
142}
143
144impl MergeConflict {
145    /// The message of the error, for the attribute that folds (`here`).
146    pub fn message(&self) -> String {
147        if self.option == "macro_name" {
148            return format!(
149                "announce: this item is announced twice with different trampoline names (`{}` \
150                 and `{}`). One item has one trampoline, so use the same `macro_name` (or none) \
151                 in every announcement",
152                self.folder, self.target
153            );
154        }
155        format!(
156            "announce: this announcement sets `{} = {}`, but another announcement of the same \
157             item sets `{} = {}`. The announcements of one item are merged into one trampoline, \
158             so they must agree: remove one of them or make them equal",
159            self.option, self.folder, self.option, self.target
160        )
161    }
162}
163
164fn tokens_text(t: &dyn ToTokens) -> String {
165    t.to_token_stream().to_string()
166}
167
168/// The value of an option that has one value only, from two announcements: the one
169/// that is set, or the common value; a conflict when both are set and differ.
170fn agree<T: ToTokens + Clone>(
171    option: &'static str,
172    target: &Option<T>,
173    folder: &Option<T>,
174) -> std::result::Result<Option<T>, MergeConflict> {
175    match (target, folder) {
176        (Some(a), Some(b)) if tokens_text(a) != tokens_text(b) => Err(MergeConflict {
177            option,
178            target: tokens_text(a).split_whitespace().collect(),
179            folder: tokens_text(b).split_whitespace().collect(),
180        }),
181        (Some(a), _) => Ok(Some(a.clone())),
182        (None, b) => Ok(b.clone()),
183    }
184}
185
186/// `target` followed by the entries of `folder` that are not in it yet.
187fn union(target: &[Check], folder: &[Check]) -> Vec<Check> {
188    let mut out = target.to_vec();
189    for c in folder {
190        let text = tokens_text(c);
191        if !out.iter().any(|o| tokens_text(o) == text) {
192            out.push(c.clone());
193        }
194    }
195    out
196}
197
198impl Options {
199    /// Whether the key was written (or, for options built in code, has a value
200    /// other than the default).
201    fn is_given(&self, key: &str, not_default: bool) -> bool {
202        not_default || self.given.iter().any(|g| g == key)
203    }
204
205    /// `emit_item` if it is set: written, or `false`.
206    fn explicit_emit_item(&self) -> Option<bool> {
207        self.is_given("emit_item", !self.emit_item)
208            .then(|| self.emit_item)
209    }
210
211    /// The name of the trampoline: `macro_name`, else the name of the item (`item`).
212    /// A raw identifier names the same macro as the plain one.
213    fn trampoline_name(&self, item: &Ident) -> String {
214        use syn::ext::IdentExt;
215        self.macro_name.as_ref().unwrap_or(item).unraw().to_string()
216    }
217
218    /// The options of two announcements of one item, as one announcement.
219    ///
220    /// `self` is the announcement that stays (the *target*), `folder` the one that
221    /// is merged into it, `item` the name of the item. The result is what the target
222    /// would be if it had been written with the options of both. A pure function: it
223    /// does not read the item or the environment.
224    ///
225    /// | option | result |
226    /// |---|---|
227    /// | checks, `meta` | the target's, then the folder's that are not in it yet (exact duplicates dropped) |
228    /// | `allow_primitive`, `allow_std_prelude`, `docs` | the OR of the two effective values (the default of `allow_primitive` is `true`, so it is `false` only when both say so) |
229    /// | `crate` | the target's if set, else the folder's: two paths that both name `announce` do not conflict |
230    /// | `macro_name` | the effective names must be equal (`macro_name`, else `item`): an explicit one is kept; different names are a conflict, as one item has one trampoline |
231    /// | `mock`, `repeater` | the one that is set; both set and different is a conflict |
232    /// | `layout` | the one that is not `auto`; both and different is a conflict |
233    /// | `emit_item` | the one that is set (written, or `false`); both and different is a conflict |
234    ///
235    /// The written keys (`given`) are united. The attributes that checks own are not an option: they
236    /// come from the checks' answers to the query (the runner carries them).
237    pub(crate) fn merge(
238        &self,
239        folder: &Options,
240        item: &Ident,
241    ) -> std::result::Result<Options, MergeConflict> {
242        let (target_name, folder_name) = (self.trampoline_name(item), folder.trampoline_name(item));
243        if target_name != folder_name {
244            return Err(MergeConflict {
245                option: "macro_name",
246                target: target_name,
247                folder: folder_name,
248            });
249        }
250        let mut out = self.clone();
251        if out.macro_name.is_none() {
252            out.macro_name = folder.macro_name.clone();
253        }
254        out.mock = agree("mock", &self.mock, &folder.mock)?;
255        out.repeater = agree("repeater", &self.repeater, &folder.repeater)?;
256        if out.layout == Layout::Auto {
257            out.layout = folder.layout;
258        } else if folder.layout != Layout::Auto && folder.layout != self.layout {
259            return Err(MergeConflict {
260                option: "layout",
261                target: format!("{:?}", self.layout).to_lowercase(),
262                folder: format!("{:?}", folder.layout).to_lowercase(),
263            });
264        }
265        out.emit_item = match (self.explicit_emit_item(), folder.explicit_emit_item()) {
266            (Some(a), Some(b)) if a != b => {
267                return Err(MergeConflict {
268                    option: "emit_item",
269                    target: a.to_string(),
270                    folder: b.to_string(),
271                })
272            }
273            (Some(a), _) | (None, Some(a)) => a,
274            (None, None) => true,
275        };
276        if out.krate.is_none() {
277            out.krate = folder.krate.clone();
278        }
279        out.allow_primitive = self.allow_primitive || folder.allow_primitive;
280        out.allow_std_prelude = self.allow_std_prelude || folder.allow_std_prelude;
281        out.docs = self.docs || folder.docs;
282        out.checks = union(&self.checks, &folder.checks);
283        out.metas = union(&self.metas, &folder.metas);
284        for key in &folder.given {
285            if !out.given.contains(key) {
286                out.given.push(key.clone());
287            }
288        }
289        Ok(out)
290    }
291}
292
293impl ToTokens for Options {
294    fn to_tokens(&self, tokens: &mut TokenStream) {
295        let mut parts: Vec<TokenStream> = Vec::new();
296        if let Some(k) = &self.krate {
297            parts.push(quote! { crate = #k });
298        }
299        if let Some(n) = &self.macro_name {
300            parts.push(quote! { macro_name = #n });
301        }
302        if let Some(m) = &self.mock {
303            parts.push(quote! { mock = #m });
304        }
305        if let Some(r) = &self.repeater {
306            parts.push(quote! { repeater = #r });
307        }
308        if !self.allow_primitive {
309            parts.push(quote! { allow_primitive = false });
310        }
311        if self.allow_std_prelude {
312            parts.push(quote! { allow_std_prelude = true });
313        }
314        if !self.emit_item {
315            parts.push(quote! { emit_item = false });
316        }
317        match self.layout {
318            Layout::Auto => {}
319            Layout::Single => parts.push(quote! { layout = single }),
320            Layout::Direct => parts.push(quote! { layout = direct }),
321        }
322        if self.docs {
323            parts.push(quote! { docs = true });
324        }
325        for m in &self.metas {
326            parts.push(quote! { meta = #m });
327        }
328        for c in &self.checks {
329            parts.push(quote! { #c });
330        }
331        tokens.extend(quote! { #(#parts),* });
332    }
333}
334
335impl Parse for Options {
336    fn parse(input: ParseStream) -> Result<Self> {
337        let mut opts = Options::default();
338        while !input.is_empty() {
339            if input.peek(Token![crate]) && input.peek2(Token![=]) {
340                input.parse::<Token![crate]>()?;
341                input.parse::<Token![=]>()?;
342                opts.krate = Some(Path::parse_mod_style(input)?);
343            } else if input.peek(syn::Ident) && input.peek2(Token![=]) && !input.peek2(Token![==]) {
344                let key: Ident = input.parse()?;
345                input.parse::<Token![=]>()?;
346                opts.given.push(key.to_string());
347                match key.to_string().as_str() {
348                    "macro_name" => opts.macro_name = Some(input.parse()?),
349                    "export_name" | "marker" => {
350                        return Err(Error::new(
351                            key.span(),
352                            if key == "export_name" {
353                                "`export_name` was removed: the hidden macro name is generated; \
354                                 use `macro_name = ..` to choose the public name"
355                            } else {
356                                "`marker` was removed: the `Announce` impl follows the item \
357                                 (none with `emit_item = false` or `mock`)"
358                            },
359                        ))
360                    }
361                    "mock" => opts.mock = Some(Path::parse_mod_style(input)?),
362                    "repeater" => {
363                        let p = Path::parse_mod_style(input)?;
364                        crate::leak::check_repeater(&p)?;
365                        opts.repeater = Some(p);
366                    }
367                    "signature_only" | "transform" => {
368                        return Err(Error::new(
369                            key.span(),
370                            format!(
371                                "`{}` is not a definition-side option: transformers are chosen by the caller, \
372                                 e.g. `F! {{ [F!, ::announce::signature_only!, callback!] .. }}`",
373                                key
374                            ),
375                        ))
376                    }
377                    "emit_item" => opts.emit_item = input.parse::<LitBool>()?.value,
378                    "layout" => {
379                        let v: Ident = input.parse()?;
380                        opts.layout = match v.to_string().as_str() {
381                            "auto" => Layout::Auto,
382                            "single" => Layout::Single,
383                            "direct" => Layout::Direct,
384                            _ => {
385                                return Err(Error::new(
386                                    v.span(),
387                                    "`layout` is `auto`, `single` or `direct`",
388                                ))
389                            }
390                        };
391                    }
392                    "docs" => opts.docs = input.parse::<LitBool>()?.value,
393                    "allow_primitive" => opts.allow_primitive = input.parse::<LitBool>()?.value,
394                    "allow_std_prelude" => opts.allow_std_prelude = input.parse::<LitBool>()?.value,
395                    "meta" => {
396                        let path = Path::parse_mod_style(input)?;
397                        let args = if input.peek(syn::token::Paren) {
398                            let content;
399                            syn::parenthesized!(content in input);
400                            Some(content.parse::<TokenStream>()?)
401                        } else {
402                            None
403                        };
404                        opts.metas.push(Check { path, args });
405                    }
406                    _ => {
407                        return Err(Error::new(
408                            key.span(),
409                            "unknown announce option; expected one of `macro_name`, \
410                             `mock`, `repeater`, `allow_primitive`, `allow_std_prelude`, `crate`, `emit_item`, `layout`, `docs`, `meta` (a bare path is a check)",
411                        ))
412                    }
413                }
414            } else {
415                let path = Path::parse_mod_style(input)?;
416                let args = if input.peek(syn::token::Paren) {
417                    let content;
418                    syn::parenthesized!(content in input);
419                    Some(content.parse::<TokenStream>()?)
420                } else {
421                    None
422                };
423                opts.checks.push(Check { path, args });
424            }
425            if input.is_empty() {
426                break;
427            }
428            input.parse::<Token![,]>()?;
429        }
430        Ok(opts)
431    }
432}
433
434/// Arguments of the hidden `#[__announce(RANDOM; [OWNED;] OPTIONS)]` helper
435/// attribute that `#[announce]` puts on the capture dummy. `OWNED` is a bracketed,
436/// comma-separated list of the attribute names that the checks own; it is left
437/// out when there are none.
438pub struct CaptureArgs {
439    pub random: u64,
440    /// The sentinel of the announcement, if it emitted one (see `runner::Sentinel`).
441    pub sentinel: Option<crate::runner::Sentinel>,
442    pub owned: crate::helper::OwnedAttrs,
443    pub opts: Options,
444}
445
446impl Parse for CaptureArgs {
447    fn parse(input: ParseStream) -> Result<Self> {
448        let lit: syn::LitInt = input.parse()?;
449        input.parse::<Token![;]>()?;
450        let mut sentinel = None;
451        if input.peek(syn::Ident) && input.peek2(Token![;]) {
452            let word: Ident = input.parse()?;
453            sentinel = Some(
454                crate::runner::Sentinel::from_capture_word(&word.to_string())
455                    .ok_or_else(|| Error::new(word.span(), "announce: malformed capture dummy"))?,
456            );
457            input.parse::<Token![;]>()?;
458        }
459        let owned = if input.peek(syn::token::Bracket) {
460            let content;
461            syn::bracketed!(content in input);
462            let names: syn::punctuated::Punctuated<Ident, Token![,]> =
463                syn::punctuated::Punctuated::parse_terminated(&content)?;
464            input.parse::<Token![;]>()?;
465            crate::helper::OwnedAttrs::from_idents(names)
466        } else {
467            crate::helper::OwnedAttrs::new()
468        };
469        Ok(CaptureArgs {
470            random: lit.base10_parse()?,
471            sentinel,
472            owned,
473            opts: input.parse()?,
474        })
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn opts(src: &str) -> Options {
483        syn::parse2(src.parse().unwrap()).unwrap_or_else(|e| panic!("{}: {}", src, e))
484    }
485
486    fn text(o: &Options) -> String {
487        o.to_token_stream().to_string().split_whitespace().collect()
488    }
489
490    fn ident(name: &str) -> Ident {
491        syn::parse_str(name).unwrap()
492    }
493
494    /// The merge of two sets of options on the item `S`, printed.
495    fn merged(target: &str, folder: &str) -> String {
496        merged_on("S", target, folder)
497    }
498
499    fn merged_on(item: &str, target: &str, folder: &str) -> String {
500        text(&opts(target).merge(&opts(folder), &ident(item)).unwrap())
501    }
502
503    fn conflict(target: &str, folder: &str) -> MergeConflict {
504        conflict_on("S", target, folder)
505    }
506
507    fn conflict_on(item: &str, target: &str, folder: &str) -> MergeConflict {
508        opts(target)
509            .merge(&opts(folder), &ident(item))
510            .err()
511            .unwrap()
512    }
513
514    #[test]
515    fn lists_are_a_union_that_keeps_the_order() {
516        assert_eq!(
517            merged("a::c1, meta = a::m", "b::c2(x), meta = b::m"),
518            "meta=a::m,meta=b::m,a::c1,b::c2(x)"
519        );
520        // The target's entries come first, whatever the folder wrote first.
521        assert_eq!(merged("meta = a::m", "meta = b::m"), "meta=a::m,meta=b::m");
522        assert_eq!(merged("meta = b::m", "meta = a::m"), "meta=b::m,meta=a::m");
523    }
524
525    #[test]
526    fn exact_duplicates_are_dropped() {
527        assert_eq!(
528            merged(
529                "a::c(1), meta = m::x",
530                "a::c(1), a::c(2), meta = m::x, meta = m::y"
531            ),
532            "meta=m::x,meta=m::y,a::c(1),a::c(2)"
533        );
534        // A check and a meta entry of the same path are different things.
535        assert_eq!(merged("a::c", "meta = a::c"), "meta=a::c,a::c");
536        // Merging with itself changes nothing.
537        let o = "macro_name = N, meta = m::x, a::c(1)";
538        assert_eq!(merged(o, o), text(&opts(o)));
539        assert_eq!(merged("", ""), "");
540    }
541
542    #[test]
543    fn booleans_are_the_or_of_the_effective_values() {
544        // `allow_primitive` is true unless both say false.
545        assert_eq!(merged("allow_primitive = false", ""), "");
546        assert_eq!(merged("", "allow_primitive = false"), "");
547        assert_eq!(
548            merged("allow_primitive = false", "allow_primitive = true"),
549            ""
550        );
551        assert_eq!(
552            merged("allow_primitive = false", "allow_primitive = false"),
553            "allow_primitive=false"
554        );
555        // The others are false unless one says true.
556        assert_eq!(
557            merged("allow_std_prelude = true", ""),
558            "allow_std_prelude=true"
559        );
560        assert_eq!(
561            merged("", "allow_std_prelude = true"),
562            "allow_std_prelude=true"
563        );
564        assert_eq!(merged("allow_std_prelude = false", ""), "");
565        assert_eq!(merged("docs = true", "docs = false"), "docs=true");
566        assert_eq!(merged("docs = false", "docs = true"), "docs=true");
567    }
568
569    #[test]
570    fn crate_is_the_targets_else_the_folders() {
571        assert_eq!(
572            merged("crate = ::a::announce", "crate = ::b::announce"),
573            "crate=::a::announce"
574        );
575        assert_eq!(merged("", "crate = ::b::announce"), "crate=::b::announce");
576        assert_eq!(merged("crate = ::a::announce", ""), "crate=::a::announce");
577    }
578
579    #[test]
580    fn single_valued_options_agree_or_conflict() {
581        for (opt, a, b) in [
582            ("mock", "x::A", "x::B"),
583            ("repeater", "crate::A", "crate::B"),
584            ("layout", "single", "direct"),
585            ("emit_item", "false", "true"),
586        ] {
587            let (ta, tb) = (format!("{} = {}", opt, a), format!("{} = {}", opt, b));
588            let c = conflict(&ta, &tb);
589            assert_eq!(c.option, opt);
590            assert_eq!((c.target.as_str(), c.folder.as_str()), (a, b), "{}", opt);
591            assert!(c.message().contains(opt), "{}", c.message());
592            // Equal values are fine, and a value that is set beats one that is not.
593            assert_eq!(merged(&ta, &ta), text(&opts(&ta)));
594            assert_eq!(merged(&ta, ""), text(&opts(&ta)), "{}", opt);
595            assert_eq!(merged("", &tb), text(&opts(&tb)), "{}", opt);
596        }
597    }
598
599    #[test]
600    fn trampoline_names_must_be_equal() {
601        // Both set: equal names merge, different names are a conflict.
602        assert_eq!(merged("macro_name = A", "macro_name = A"), "macro_name=A");
603        let c = conflict("macro_name = A", "macro_name = B");
604        assert_eq!(c.option, "macro_name");
605        assert_eq!((c.target.as_str(), c.folder.as_str()), ("A", "B"));
606        // Unset is the name of the item: `S` here.
607        assert_eq!(merged("", ""), "");
608        let c = conflict("macro_name = B", "");
609        assert_eq!((c.target.as_str(), c.folder.as_str()), ("B", "S"));
610        let c = conflict("", "macro_name = B");
611        assert_eq!((c.target.as_str(), c.folder.as_str()), ("S", "B"));
612        // Set to the name of the item and unset are the same name; the explicit one stays.
613        assert_eq!(merged("macro_name = S", ""), "macro_name=S");
614        assert_eq!(merged("", "macro_name = S"), "macro_name=S");
615        assert_eq!(merged("macro_name = S", "macro_name = S"), "macro_name=S");
616        assert_eq!(merged_on("A", "macro_name = A", ""), "macro_name=A");
617        assert_eq!(merged_on("A", "", "macro_name = A"), "macro_name=A");
618        // The item's name is not the name of another item.
619        assert_eq!(conflict_on("A", "macro_name = S", "").option, "macro_name");
620        // A raw identifier is the same macro name.
621        assert_eq!(merged("macro_name = r#S", ""), "macro_name=r#S");
622        assert_eq!(
623            merged("macro_name = r#A", "macro_name = A"),
624            "macro_name=r#A"
625        );
626        assert_eq!(merged_on("r#S", "macro_name = S", ""), "macro_name=S");
627    }
628
629    #[test]
630    fn the_trampoline_name_conflict_says_what_to_do() {
631        let m = conflict("", "macro_name = ParseAlt").message();
632        assert!(
633            m.contains("announced twice with different trampoline names (`ParseAlt` and `S`)"),
634            "{}",
635            m
636        );
637        assert!(m.contains("one trampoline"), "{}", m);
638        assert!(
639            m.contains("use the same `macro_name` (or none) in every announcement"),
640            "{}",
641            m
642        );
643        // The message names the expanding announcement's name first.
644        let m = conflict("macro_name = Parse", "macro_name = ParseAlt").message();
645        assert!(m.contains("(`ParseAlt` and `Parse`)"), "{}", m);
646    }
647
648    #[test]
649    fn layout_auto_is_not_a_value() {
650        assert_eq!(merged("layout = auto", "layout = single"), "layout=single");
651        assert_eq!(merged("layout = direct", "layout = auto"), "layout=direct");
652        assert_eq!(
653            merged("layout = single", "layout = single"),
654            "layout=single"
655        );
656    }
657
658    #[test]
659    fn emit_item_true_is_a_value_only_when_written() {
660        // Not written: the other one decides.
661        assert_eq!(merged("", "emit_item = false"), "emit_item=false");
662        assert_eq!(merged("emit_item = false", ""), "emit_item=false");
663        // Written as the default, and `false` from the other: a disagreement.
664        assert_eq!(
665            conflict("emit_item = true", "emit_item = false").option,
666            "emit_item"
667        );
668        // An option built in code has no written keys: `false` counts as set.
669        let built = Options {
670            emit_item: false,
671            ..Options::default()
672        };
673        let s = ident("S");
674        assert_eq!(
675            text(&opts("").merge(&built, &s).unwrap()),
676            "emit_item=false"
677        );
678        assert_eq!(
679            text(&built.merge(&opts(""), &s).unwrap()),
680            "emit_item=false"
681        );
682        assert!(built.merge(&opts("emit_item = true"), &s).is_err());
683    }
684
685    #[test]
686    fn mock_paths_are_compared_as_tokens() {
687        assert_eq!(merged("mock = a::B", "mock = a::B"), "mock=a::B");
688        assert!(opts("mock = a::B")
689            .merge(&opts("mock = ::a::B"), &ident("S"))
690            .is_err());
691    }
692
693    #[test]
694    fn the_written_keys_are_united() {
695        let m = opts("macro_name = N")
696            .merge(&opts("docs = true, macro_name = N"), &ident("S"))
697            .unwrap();
698        assert_eq!(m.given, ["macro_name", "docs"]);
699    }
700
701    #[test]
702    fn the_merge_prints_back_as_options_the_target_can_parse() {
703        let m = opts(
704            "crate = ::a::announce, macro_name = N, allow_std_prelude = true, meta = m::x(1), a::c",
705        )
706        .merge(
707            &opts("docs = true, layout = single, meta = m::y, b::c(2), a::c"),
708            // The folder has no `macro_name`: the item is named `N` as well.
709            &ident("N"),
710        )
711        .unwrap();
712        let printed = m.to_token_stream();
713        let again: Options = syn::parse2(printed.clone()).unwrap();
714        assert_eq!(text(&again), text(&m));
715        assert_eq!(
716            text(&m),
717            "crate=::a::announce,macro_name=N,allow_std_prelude=true,layout=single,docs=true,\
718             meta=m::x(1),meta=m::y,a::c,b::c(2)"
719        );
720    }
721
722    #[test]
723    fn the_merge_is_pure() {
724        let (a, b) = (opts("meta = a::x"), opts("meta = b::x"));
725        let _ = a.merge(&b, &ident("S")).unwrap();
726        assert_eq!(text(&a), "meta=a::x");
727        assert_eq!(text(&b), "meta=b::x");
728    }
729}