1use proc_macro2::{Ident, TokenStream};
2use syn::parse::{Parse, ParseStream};
3use syn::{Error, LitBool, Path, Result, Token};
4use template_quote::{quote, ToTokens};
5
6#[derive(Clone)]
12pub struct Check {
13 pub path: Path,
15 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum Layout {
35 Auto,
39 Single,
43 Direct,
46}
47
48#[derive(Clone)]
62#[non_exhaustive]
63pub struct Options {
64 pub macro_name: Option<Ident>,
68 pub mock: Option<Path>,
70 pub repeater: Option<Path>,
72 pub allow_primitive: bool,
74 pub allow_std_prelude: bool,
76 pub krate: Option<Path>,
79 pub emit_item: bool,
82 pub layout: Layout,
85 pub docs: bool,
90 pub(crate) given: Vec<String>,
92 pub checks: Vec<Check>,
94 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 pub fn krate(&self) -> Path {
125 self.krate
126 .clone()
127 .unwrap_or_else(crate::cratepath::default_crate_path)
128 }
129}
130
131#[derive(Clone, Debug, PartialEq, Eq)]
134pub(crate) struct MergeConflict {
135 pub option: &'static str,
138 pub target: String,
140 pub folder: String,
142}
143
144impl MergeConflict {
145 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
168fn 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
186fn 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 fn is_given(&self, key: &str, not_default: bool) -> bool {
202 not_default || self.given.iter().any(|g| g == key)
203 }
204
205 fn explicit_emit_item(&self) -> Option<bool> {
207 self.is_given("emit_item", !self.emit_item)
208 .then(|| self.emit_item)
209 }
210
211 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 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
434pub struct CaptureArgs {
439 pub random: u64,
440 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 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 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 assert_eq!(merged("a::c", "meta = a::c"), "meta=a::c,a::c");
536 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 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 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 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 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 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 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 assert_eq!(conflict_on("A", "macro_name = S", "").option, "macro_name");
620 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 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 assert_eq!(merged("", "emit_item = false"), "emit_item=false");
662 assert_eq!(merged("emit_item = false", ""), "emit_item=false");
663 assert_eq!(
665 conflict("emit_item = true", "emit_item = false").option,
666 "emit_item"
667 );
668 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 &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}