zng_app_proc_macros/
util.rs

1use std::{borrow::Cow, env, fmt, fs, path::PathBuf};
2
3use proc_macro2::*;
4use quote::{ToTokens, quote_spanned};
5use syn::{
6    self, Attribute, LitStr, Token,
7    parse::{Parse, ParseStream, discouraged::Speculative},
8    parse_quote, parse_quote_spanned,
9    punctuated::Punctuated,
10    spanned::Spanned,
11};
12
13use once_cell::sync::OnceCell;
14
15/// `Ident` with custom span.
16macro_rules! ident_spanned {
17    ($span:expr=> $($format_name:tt)+) => {
18        proc_macro2::Ident::new(&format!($($format_name)+), $span)
19    };
20}
21
22/// `Ident` with call_site span.
23macro_rules! ident {
24    ($($tt:tt)*) => {
25        ident_spanned!(proc_macro2::Span::call_site()=> $($tt)*)
26    };
27}
28
29pub fn parse_braces<'a>(input: &syn::parse::ParseBuffer<'a>) -> syn::Result<(syn::token::Brace, syn::parse::ParseBuffer<'a>)> {
30    let r;
31    let b = syn::braced!(r in input);
32    Ok((b, r))
33}
34
35/// Returns `true` if the proc-macro is running in one of the rust-analyzer proc-macro servers.
36#[expect(unexpected_cfgs)] // rust_analyzer exists: https://github.com/rust-lang/rust-analyzer/pull/15528
37pub fn is_rust_analyzer() -> bool {
38    cfg!(rust_analyzer)
39}
40
41/// Return the equivalent of `$crate`.
42pub fn crate_core() -> TokenStream {
43    let (ident, module) = if is_rust_analyzer() {
44        // rust-analyzer gets the wrong crate sometimes if we cache, maybe they use the same server instance
45        // for the entire workspace?
46        let (ident, module) = crate_core_parts();
47        (Cow::Owned(ident), module)
48    } else {
49        static CRATE: OnceCell<(String, &'static str)> = OnceCell::new();
50
51        let (ident, module) = CRATE.get_or_init(crate_core_parts);
52        (Cow::Borrowed(ident.as_str()), *module)
53    };
54
55    let ident = Ident::new(&ident, Span::call_site());
56    if !module.is_empty() {
57        let module = Ident::new(module, Span::call_site());
58        quote! { #ident::#module }
59    } else {
60        ident.to_token_stream()
61    }
62}
63fn crate_core_parts() -> (String, &'static str) {
64    if let Ok(ident) = crate_name("zng") {
65        // using the main crate.
66        match ident {
67            FoundCrate::Name(name) => (name, "__proc_macro_util"),
68            FoundCrate::Itself => ("zng".to_owned(), "__proc_macro_util"),
69        }
70    } else if let Ok(ident) = crate_name("zng-wgt") {
71        // using the wgt crate.
72        match ident {
73            FoundCrate::Name(name) => (name, "__proc_macro_util"),
74            FoundCrate::Itself => ("zng_wgt".to_owned(), "__proc_macro_util"),
75        }
76    } else if let Ok(ident) = crate_name("zng-app") {
77        // using the core crate only.
78        match ident {
79            FoundCrate::Name(name) => (name, ""),
80            FoundCrate::Itself => ("zng_app".to_owned(), ""),
81        }
82    } else {
83        // failed, at least shows "zng" in the compile error.
84        ("zng".to_owned(), "__proc_macro_util")
85    }
86}
87
88#[derive(PartialEq, Debug)]
89enum FoundCrate {
90    Name(String),
91    Itself,
92}
93
94/// Gets the module name of a given crate name (same behavior as $crate).
95fn crate_name(orig_name: &str) -> Result<FoundCrate, ()> {
96    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|_| ())?);
97
98    let toml = fs::read_to_string(manifest_dir.join("Cargo.toml")).map_err(|_| ())?;
99
100    crate_name_impl(orig_name, &toml)
101}
102fn crate_name_impl(orig_name: &str, toml: &str) -> Result<FoundCrate, ()> {
103    // some of this code is based on the crate `proc-macro-crate` code, we
104    // don't depend on that crate to speedup compile time.
105    enum State<'a> {
106        Seeking,
107        Package,
108        Dependencies,
109        Dependency(&'a str),
110    }
111
112    let mut state = State::Seeking;
113
114    for line in toml.lines() {
115        let line = line.trim();
116
117        let new_state = if line == "[package]" {
118            Some(State::Package)
119        } else if line.contains("dependencies.") && line.ends_with(']') {
120            let name_start = line.rfind('.').unwrap();
121            let name = line[name_start + 1..].trim_end_matches(']');
122            Some(State::Dependency(name))
123        } else if line.ends_with("dependencies]") {
124            Some(State::Dependencies)
125        } else if line.starts_with('[') {
126            Some(State::Seeking)
127        } else {
128            None
129        };
130
131        if let Some(new_state) = new_state {
132            if let State::Dependency(name) = state {
133                if name == orig_name {
134                    // finished `[*dependencies.<name>]` without finding a `package = "other"`
135                    return Ok(FoundCrate::Name(orig_name.replace('-', "_")));
136                }
137            }
138
139            state = new_state;
140            continue;
141        }
142
143        match state {
144            State::Seeking => continue,
145            // Check if it is the crate itself, or one of its tests.
146            State::Package => {
147                if line.starts_with("name ") || line.starts_with("name=") {
148                    if let Some(name_start) = line.find('"') {
149                        if let Some(name_end) = line.rfind('"') {
150                            let name = &line[name_start + 1..name_end];
151
152                            if name == orig_name {
153                                return Ok(if env::var_os("CARGO_TARGET_TMPDIR").is_none() {
154                                    FoundCrate::Itself
155                                } else {
156                                    FoundCrate::Name(orig_name.replace('-', "_"))
157                                });
158                            }
159                        }
160                    }
161                }
162            }
163            // Check dependencies, dev-dependencies, target.`..`.dependencies
164            State::Dependencies => {
165                if let Some(eq) = line.find('=') {
166                    let name = line[..eq].trim();
167                    let value = line[eq + 1..].trim();
168
169                    if value.starts_with('"') {
170                        if name == orig_name {
171                            return Ok(FoundCrate::Name(orig_name.replace('-', "_")));
172                        }
173                    } else if value.starts_with('{') {
174                        let value = value.replace(' ', "");
175                        if let Some(pkg) = value.find("package=\"") {
176                            let pkg = &value[pkg + "package=\"".len()..];
177                            if let Some(pkg_name_end) = pkg.find('"') {
178                                let pkg_name = &pkg[..pkg_name_end];
179                                if pkg_name == orig_name {
180                                    return Ok(FoundCrate::Name(name.replace('-', "_")));
181                                }
182                            }
183                        } else if name == orig_name {
184                            return Ok(FoundCrate::Name(orig_name.replace('-', "_")));
185                        }
186                    }
187                }
188            }
189            // Check a dependency in the style [dependency.foo]
190            State::Dependency(name) => {
191                if line.starts_with("package ") || line.starts_with("package=") {
192                    if let Some(pkg_name_start) = line.find('"') {
193                        if let Some(pkg_name_end) = line.rfind('"') {
194                            let pkg_name = &line[pkg_name_start + 1..pkg_name_end];
195
196                            if pkg_name == orig_name {
197                                return Ok(FoundCrate::Name(name.replace('-', "_")));
198                            }
199                        }
200                    }
201                }
202            }
203        }
204    }
205
206    if let State::Dependency(name) = state {
207        if name == orig_name {
208            // finished `[*dependencies.<name>]` without finding a `package = "other"`
209            return Ok(FoundCrate::Name(orig_name.replace('-', "_")));
210        }
211    }
212
213    Err(())
214}
215
216/// Generates a return of a compile_error message in the given span.
217macro_rules! abort {
218    ($span:expr, $($tt:tt)*) => {{
219        let error = format!($($tt)*);
220        let error = syn::LitStr::new(&error, proc_macro2::Span::call_site());
221
222        return quote_spanned!($span=> compile_error!{#error}).into();
223    }};
224}
225
226/// Generates a return of a compile_error message in the call_site span.
227macro_rules! abort_call_site {
228    ($($tt:tt)*) => {
229        abort!(proc_macro2::Span::call_site(), $($tt)*)
230    };
231}
232
233/// Input error not caused by the user.
234macro_rules! non_user_error {
235    ($e:expr) => {
236        panic!("[{}:{}] invalid non-user input: {}", file!(), line!(), $e)
237    };
238    ($fmt:tt, $($args:tt)+) => {
239        non_user_error! {
240            format_args!($fmt, $($args)+)
241        }
242    }
243}
244
245macro_rules! non_user_group {
246    ($group_kind:ident, $input:expr) => {
247        {
248            fn inner(input: syn::parse::ParseStream) -> syn::Result<syn::parse::ParseBuffer> {
249                let inner;
250                // this macro inserts a return Err(..) but we want to panic
251                syn::$group_kind!(inner in input);
252                Ok(inner)
253            }
254            inner($input).unwrap_or_else(|e| non_user_error!(e))
255        }
256    };
257    ($group_kind:ident, $input:expr, $ident:expr) => {
258        {
259            let id: syn::Ident = $input.parse().unwrap_or_else(|e| non_user_error!(e));
260            let ident = $ident;
261            if id != ident {
262                non_user_error!(format!("expected `{ident}`"));
263            }
264            non_user_group! { $group_kind, $input }
265        }
266    }
267}
268/// Does a `braced!` parse but panics with [`non_user_error!()`](non_user_error) if the parsing fails.
269macro_rules! non_user_braced {
270    ($input:expr) => {
271        non_user_group! { braced, $input }
272    };
273    ($input:expr, $ident:expr) => {
274        non_user_group! { braced, $input, $ident }
275    };
276}
277
278/// Does a `parenthesized!` parse but panics with [`non_user_error!()`](non_user_error) if the parsing fails.
279#[allow(unused)] // depends on cfg
280macro_rules! non_user_parenthesized {
281    ($input:expr) => {
282        non_user_group! { parenthesized, $input }
283    };
284}
285
286/// Does a `bracketed!` parse but panics with [`non_user_error!()`](non_user_error) if the parsing fails.
287macro_rules! non_user_bracketed {
288    ($input:expr) => {
289        non_user_group! { bracketed, $input }
290    };
291}
292
293/// Collection of compile errors.
294#[derive(Default)]
295pub struct Errors {
296    tokens: TokenStream,
297}
298impl Errors {
299    /// Push a compile error.
300    pub fn push(&mut self, error: impl ToString, span: Span) {
301        let error = error.to_string();
302        self.tokens.extend(quote_spanned! {span=>
303            compile_error!{#error}
304        })
305    }
306
307    /// Push all compile errors in `error`.
308    pub fn push_syn(&mut self, error: syn::Error) {
309        for error in error {
310            let span = error.span();
311            let msg = error.to_string();
312            if msg != RECOVERABLE_TAG {
313                self.push(error, span);
314            }
315        }
316    }
317
318    pub fn is_empty(&self) -> bool {
319        self.tokens.is_empty()
320    }
321}
322impl ToTokens for Errors {
323    fn to_tokens(&self, tokens: &mut TokenStream) {
324        tokens.extend(self.tokens.clone())
325    }
326    fn to_token_stream(&self) -> TokenStream {
327        self.tokens.clone()
328    }
329    fn into_token_stream(self) -> TokenStream {
330        self.tokens
331    }
332}
333
334/// Separated attributes.
335#[derive(Clone)]
336pub struct Attributes {
337    pub docs: Vec<Attribute>,
338    pub inline: Option<Attribute>,
339    pub cfg: Option<Attribute>,
340    pub deprecated: Option<Attribute>,
341    pub lints: Vec<Attribute>,
342    pub others: Vec<Attribute>,
343}
344impl Attributes {
345    pub fn new(attrs: Vec<Attribute>) -> Self {
346        let mut docs = vec![];
347        let mut inline = None;
348        let mut cfg = None;
349        let mut deprecated = None;
350        let mut lints = vec![];
351        let mut others = vec![];
352
353        for attr in attrs {
354            if let Some(ident) = attr.path().get_ident() {
355                if ident == "doc" {
356                    docs.push(attr);
357                    continue;
358                } else if ident == "inline" {
359                    inline = Some(attr);
360                } else if ident == "cfg" {
361                    cfg = Some(attr);
362                } else if ident == "deprecated" {
363                    deprecated = Some(attr);
364                } else if ident == "allow" || ident == "expect" || ident == "warn" || ident == "deny" || ident == "forbid" {
365                    lints.push(attr);
366                } else {
367                    others.push(attr);
368                }
369            } else {
370                others.push(attr);
371            }
372        }
373
374        Attributes {
375            docs,
376            inline,
377            cfg,
378            deprecated,
379            lints,
380            others,
381        }
382    }
383
384    /// Insert a tag on the first doc line, does nothing if docs are missing (to cause a doc missing warning).
385    pub fn tag_doc(&mut self, text: &str, help: &str) {
386        let txt = format!("<strong title='{help}' data-tag='{text}'><code>{text}</code></strong> ");
387        for first in self.docs.iter_mut() {
388            match syn::parse2::<DocAttr>(first.tokens()) {
389                Ok(doc) => {
390                    let mut msg = doc.msg.value();
391                    msg.insert_str(0, &txt);
392                    *first = parse_quote_spanned! {first.span()=>
393                        #[doc = #msg]
394                    };
395
396                    return;
397                }
398                Err(_) => continue,
399            }
400        }
401    }
402
403    pub(crate) fn cfg_and_lints(&self) -> TokenStream {
404        let mut tts = self.cfg.to_token_stream();
405        for l in &self.lints {
406            l.to_tokens(&mut tts);
407        }
408        tts
409    }
410}
411impl ToTokens for Attributes {
412    fn to_tokens(&self, tokens: &mut TokenStream) {
413        for attr in self
414            .docs
415            .iter()
416            .chain(&self.inline)
417            .chain(&self.cfg)
418            .chain(&self.deprecated)
419            .chain(&self.lints)
420            .chain(&self.others)
421        {
422            attr.to_tokens(tokens);
423        }
424    }
425}
426
427struct DocAttr {
428    msg: LitStr,
429}
430impl Parse for DocAttr {
431    fn parse(input: ParseStream) -> syn::Result<Self> {
432        input.parse::<Token![=]>()?;
433        Ok(DocAttr { msg: input.parse()? })
434    }
435}
436
437/// Convert a [`Path`] to a formatted [`String`].
438pub fn display_path(path: &syn::Path) -> String {
439    path.to_token_stream().to_string().replace(' ', "")
440}
441
442/// Gets a span that best represent the path.
443pub fn path_span(path: &syn::Path) -> Span {
444    path.segments.last().map(|s| s.span()).unwrap_or_else(|| path.span())
445}
446
447struct OuterAttr {
448    pound_token: Token![#],
449    style: syn::AttrStyle,
450    bracket_token: syn::token::Bracket,
451    path: syn::Path,
452    tokens: TokenStream,
453}
454impl syn::parse::Parse for OuterAttr {
455    fn parse(input: ParseStream) -> syn::Result<Self> {
456        let inner;
457
458        Ok(OuterAttr {
459            pound_token: input.parse()?,
460            style: if input.peek(Token![!]) {
461                syn::AttrStyle::Inner(input.parse()?)
462            } else {
463                syn::AttrStyle::Outer
464            },
465            bracket_token: syn::bracketed!(inner in input),
466            path: inner.parse()?,
467            tokens: inner.parse()?,
468        })
469    }
470}
471impl From<OuterAttr> for Attribute {
472    fn from(s: OuterAttr) -> Self {
473        Attribute {
474            pound_token: s.pound_token,
475            style: s.style,
476            bracket_token: s.bracket_token,
477            meta: {
478                let path = s.path;
479                let tokens = s.tokens;
480                parse_quote!(#path #tokens)
481            },
482        }
483    }
484}
485
486/// Runs `rustfmt` in the `expr`.
487pub fn format_rust_expr(value: String) -> String {
488    // credits: https://github.com/rust-lang/rustfmt/issues/3257#issuecomment-523573838
489    use std::io::Write;
490    use std::process::{Command, Stdio};
491    const PREFIX: &str = "const x:() = ";
492    const SUFFIX: &str = ";\n";
493    if let Ok(mut proc) = Command::new("rustfmt")
494        .arg("--emit=stdout")
495        .arg("--edition=2018")
496        .stdin(Stdio::piped())
497        .stdout(Stdio::piped())
498        .stderr(Stdio::null())
499        .spawn()
500    {
501        {
502            let stdin = proc.stdin.as_mut().unwrap();
503            stdin.write_all(PREFIX.as_bytes()).unwrap();
504            stdin.write_all(value.as_bytes()).unwrap();
505            stdin.write_all(SUFFIX.as_bytes()).unwrap();
506        }
507        if let Ok(output) = proc.wait_with_output() {
508            if output.status.success() {
509                // slice between after the prefix and before the suffix
510                // (currently 14 from the start and 2 before the end, respectively)
511                let start = PREFIX.len() + 1;
512                let end = output.stdout.len() - SUFFIX.len();
513                return std::str::from_utf8(&output.stdout[start..end]).unwrap().to_owned();
514            }
515        }
516    }
517    value
518}
519
520/// Gets the span of the last item or the span_close if the last item is a group.
521pub fn last_span(tts: TokenStream) -> Span {
522    if let Some(tt) = tts.into_iter().last() {
523        if let proc_macro2::TokenTree::Group(g) = tt {
524            g.span_close()
525        } else {
526            tt.span()
527        }
528    } else {
529        Span::call_site()
530    }
531}
532
533/// A lint level.
534///
535/// NOTE: We add an underline `_` after the lint display name because rustc validates
536/// custom tools even for lint attributes removed by proc-macros.
537#[derive(Clone, Copy, PartialEq, Eq)]
538pub enum LintLevel {
539    Allow,
540    Warn,
541    Deny,
542    Forbid,
543}
544impl fmt::Display for LintLevel {
545    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546        match self {
547            LintLevel::Allow => write!(f, "allow_"),
548            LintLevel::Warn => write!(f, "warn_"),
549            LintLevel::Deny => write!(f, "deny_"),
550            LintLevel::Forbid => write!(f, "forbid_"),
551        }
552    }
553}
554
555/// Takes lint attributes in the `zng::` namespace.
556///
557/// Pushes `errors` for unsupported `warn` and already attempt of setting
558/// level of forbidden zng lints.
559///
560/// NOTE: We add an underline `_` after the lint ident because rustc validates
561/// custom tools even for lint attributes removed by proc-macros.
562pub fn take_zng_lints(
563    attrs: &mut Vec<Attribute>,
564    errors: &mut Errors,
565    forbidden: &std::collections::HashSet<&Ident>,
566) -> Vec<(Ident, LintLevel, Attribute)> {
567    let mut r = vec![];
568    let mut i = 0;
569    while i < attrs.len() {
570        if let Some(ident) = attrs[i].path().get_ident() {
571            let level = if ident == "allow_" {
572                LintLevel::Allow
573            } else if ident == "warn_" {
574                LintLevel::Warn
575            } else if ident == "deny_" {
576                LintLevel::Deny
577            } else if ident == "forbid_" {
578                LintLevel::Forbid
579            } else {
580                i += 1;
581                continue;
582            };
583            if let Ok(path) = syn::parse2::<LintPath>(attrs[i].tokens()) {
584                let path = path.path;
585                if path.segments.len() == 2 && path.segments[0].ident == "zng" {
586                    let attr = attrs.remove(i);
587                    let lint_ident = path.segments[1].ident.clone();
588                    match level {
589                        LintLevel::Warn => errors.push(
590                            "cannot set zng lints to warn because warning diagnostics are not stable",
591                            attr.path().span(),
592                        ),
593                        LintLevel::Allow if forbidden.contains(&lint_ident) => {
594                            errors.push(format_args!("lint `zng::{lint_ident}` is `forbid` in this context"), attr.span())
595                        }
596                        _ => {
597                            r.push((lint_ident, level, attr));
598                        }
599                    }
600
601                    continue; // same i new attribute
602                }
603            }
604        }
605
606        i += 1;
607    }
608    r
609}
610struct LintPath {
611    _paren: syn::token::Paren,
612    path: syn::Path,
613}
614impl syn::parse::Parse for LintPath {
615    fn parse(input: ParseStream) -> syn::Result<Self> {
616        let inner;
617        Ok(LintPath {
618            _paren: syn::parenthesized!(inner in input),
619            path: inner.parse()?,
620        })
621    }
622}
623
624pub fn span_is_call_site(a: proc_macro2::Span) -> bool {
625    span_eq(a, proc_macro2::Span::call_site())
626}
627
628pub fn span_eq(a: proc_macro2::Span, b: proc_macro2::Span) -> bool {
629    format!("{a:?}") == format!("{b:?}")
630}
631
632/// Parses all outer attributes and stores any parsing errors in `errors`.
633/// Note: If a malformed attribute is passed, only the attributes after that one will be returned.
634pub fn parse_outer_attrs(input: ParseStream, errors: &mut Errors) -> Vec<Attribute> {
635    let mut attrs;
636    loop {
637        let fork = input.fork();
638        let mut parsed = true;
639
640        attrs = Attribute::parse_outer(&fork).unwrap_or_else(|e| {
641            parsed = false;
642            errors.push_syn(e);
643            vec![]
644        });
645        if parsed {
646            input.advance_to(&fork);
647            break;
648        } else {
649            let _ = input.parse::<Token![#]>();
650            if input.peek(Token![!]) {
651                let _ = input.parse::<Token![!]>();
652            }
653            let _ = non_user_bracketed!(input).parse::<TokenStream>();
654        }
655    }
656
657    attrs
658}
659
660/// New [`syn::Error`] marked [recoverable](ErrorRecoverable).
661pub fn recoverable_err(span: Span, msg: impl std::fmt::Display) -> syn::Error {
662    syn::Error::new(span, msg).set_recoverable()
663}
664
665const RECOVERABLE_TAG: &str = "<recoverable>";
666fn recoverable_tag() -> syn::Error {
667    syn::Error::new(Span::call_site(), RECOVERABLE_TAG)
668}
669
670/// Extension to [`syn::Error`] that lets you mark an error as recoverable,
671/// meaning that a sequence of the parse stream is not correct but the parser
672/// manage to skip to the end of what was expected to be parsed.
673pub trait ErrorRecoverable {
674    /// Returns a new error that contains all the errors in `self` but is also marked recoverable.
675    fn set_recoverable(self) -> Self;
676    /// Returns if `self` is recoverable and all the errors in `self`.
677    ///
678    /// Note: An error is considered recoverable only if all inner errors are marked recoverable.
679    fn recoverable(self) -> (bool, Self);
680}
681impl ErrorRecoverable for syn::Error {
682    fn set_recoverable(self) -> Self {
683        let mut errors = self.into_iter();
684        let mut e = errors.next().unwrap();
685
686        debug_assert!(e.to_string() != RECOVERABLE_TAG);
687
688        e.combine(recoverable_tag());
689
690        for error in errors {
691            if e.to_string() != RECOVERABLE_TAG {
692                e.combine(error);
693                e.combine(recoverable_tag());
694            }
695        }
696
697        e
698    }
699    fn recoverable(self) -> (bool, Self) {
700        let mut errors = self.into_iter();
701        let mut e = errors.next().unwrap();
702
703        debug_assert!(e.to_string() != RECOVERABLE_TAG);
704
705        let mut errors_count = 1;
706        let mut tags_count = 0;
707
708        for error in errors {
709            if error.to_string() == RECOVERABLE_TAG {
710                tags_count += 1;
711            } else {
712                errors_count += 1;
713                e.combine(error);
714            }
715        }
716
717        (errors_count == tags_count, e)
718    }
719}
720
721// Debug tracing if it was enabled during run-time.
722//
723// This is useful for debugging say the widget macros but only for a widget.
724//
725// Use [`enable_trace!`] and [`trace!`].
726#[allow(unused)] // depends on cfg
727#[cfg(debug_assertions)]
728pub mod debug_trace {
729    use std::sync::atomic::{AtomicBool, Ordering};
730
731    static ENABLED: AtomicBool = AtomicBool::new(false);
732
733    pub fn enable(enable: bool) {
734        let prev = ENABLED.swap(enable, Ordering::SeqCst);
735        if prev != enable {
736            eprintln!("zng-proc-macros::debug_trace {}", if enable { "enabled" } else { "disabled" });
737        }
738    }
739
740    pub fn display(msg: impl std::fmt::Display) {
741        if ENABLED.load(Ordering::SeqCst) {
742            eprintln!("{msg}");
743        }
744    }
745}
746
747#[allow(unused)] // depends on cfg
748#[cfg(debug_assertions)]
749macro_rules! enable_trace {
750    () => {
751        $crate::util::debug_trace::enable(true);
752    };
753    (if $bool_expr:expr) => {
754        $crate::util::debug_trace::enable($bool_expr);
755    };
756}
757#[allow(unused)] // depends on cfg
758#[cfg(debug_assertions)]
759macro_rules! trace {
760    ($msg:tt) => {
761        $crate::util::debug_trace::display($msg);
762    };
763    ($fmt:tt, $($args:tt)+) => {
764        $crate::util::debug_trace::display(format_args!($fmt, $($args)+));
765    };
766}
767
768/// `Punctuated::parse_terminated` from a `TokenStream`.
769pub fn parse_punct_terminated2<T: Parse, P: syn::token::Token + Parse>(input: TokenStream) -> syn::Result<Punctuated<T, P>> {
770    struct PunctTerm<T: Parse, P: syn::token::Token + Parse>(Punctuated<T, P>);
771
772    impl<T: Parse, P: syn::token::Token + Parse> Parse for PunctTerm<T, P> {
773        fn parse(input: ParseStream) -> syn::Result<Self> {
774            Ok(Self(Punctuated::parse_terminated(input)?))
775        }
776    }
777
778    syn::parse2::<PunctTerm<T, P>>(input).map(|p| p.0)
779}
780
781/// Returns `true` if the stream has at least 3 more tokens.
782pub fn peek_any3(stream: ParseStream) -> bool {
783    let mut cursor = stream.cursor();
784
785    if let Some(group) = stream.cursor().group(Delimiter::None) {
786        cursor = group.0;
787    }
788
789    if let Some((_, cursor)) = cursor.token_tree() {
790        if let Some((_, cursor)) = cursor.token_tree() {
791            if let Some((_tt, _)) = cursor.token_tree() {
792                return true;
793            }
794        }
795    }
796
797    false
798}
799
800/// Set the span for each token-tree in the stream.
801pub fn set_stream_span(stream: TokenStream, span: Span) -> TokenStream {
802    stream
803        .into_iter()
804        .map(|mut tt| {
805            tt.set_span(span);
806            tt
807        })
808        .collect()
809}
810
811pub trait AttributeExt {
812    fn tokens(&self) -> TokenStream;
813}
814impl AttributeExt for Attribute {
815    fn tokens(&self) -> TokenStream {
816        match &self.meta {
817            syn::Meta::Path(_) => quote!(),
818            syn::Meta::List(m) => {
819                let t = &m.tokens;
820                match &m.delimiter {
821                    syn::MacroDelimiter::Paren(p) => quote_spanned!(p.span.join()=> (#t)),
822                    syn::MacroDelimiter::Brace(b) => quote_spanned!(b.span.join()=> {#t}),
823                    syn::MacroDelimiter::Bracket(b) => quote_spanned!(b.span.join()=> [#t]),
824                }
825            }
826            syn::Meta::NameValue(m) => {
827                let eq = &m.eq_token;
828                let tk = &m.value;
829                quote!(#eq #tk)
830            }
831        }
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    #[test]
840    fn crate_name_itself_1() {
841        let toml = r#"
842        [package]
843        name = "crate-name"
844        version = "0.1.0"
845        edition = "2024"
846        license = "Apache-2.0"
847        "#;
848
849        let r = crate_name_impl("crate-name", toml).unwrap();
850        assert_eq!(FoundCrate::Itself, r);
851    }
852
853    #[test]
854    fn crate_name_itself_2() {
855        let toml = r#"
856        [package]
857        version = "0.1.0"
858        edition = "2024"
859        name = "crate-name"
860        license = "Apache-2.0"
861        "#;
862
863        let r = crate_name_impl("crate-name", toml).unwrap();
864        assert_eq!(FoundCrate::Itself, r);
865    }
866
867    #[test]
868    fn crate_name_dependencies_1() {
869        let toml = r#"
870        [package]
871        name = "foo"
872        version = "0.1.0"
873        edition = "2024"
874        license = "Apache-2.0"
875
876        [dependencies]
877        bar = "1.0"
878        crate-name = "*"
879
880        [workspace]
881        "#;
882
883        let r = crate_name_impl("crate-name", toml).unwrap();
884        assert_eq!(FoundCrate::Name("crate_name".to_owned()), r);
885    }
886
887    #[test]
888    fn crate_name_dependencies_2() {
889        let toml = r#"
890        [package]
891        name = "foo"
892        version = "0.1.0"
893        edition = "2024"
894        license = "Apache-2.0"
895
896        [dependencies]
897        zum = "1.0"
898        super-name = { version = "*", package = "crate-name" }
899
900        [workspace]
901        "#;
902
903        let r = crate_name_impl("crate-name", toml).unwrap();
904        assert_eq!(FoundCrate::Name("super_name".to_owned()), r);
905    }
906
907    #[test]
908    fn crate_name_dependencies_3() {
909        let toml = r#"
910        [package]
911        name = "foo"
912        version = "0.1.0"
913        edition = "2024"
914        license = "Apache-2.0"
915
916        [target.'cfg(windows)'.dependencies]
917        zum = "1.0"
918        super-name = { version = "*", package = "crate-name" }
919
920        [workspace]
921        "#;
922
923        let r = crate_name_impl("crate-name", toml).unwrap();
924        assert_eq!(FoundCrate::Name("super_name".to_owned()), r);
925    }
926
927    #[test]
928    fn crate_name_dependencies_4() {
929        let toml = r#"
930        [package]
931        name = "foo"
932        version = "0.1.0"
933        edition = "2024"
934        license = "Apache-2.0"
935
936        [dev-dependencies]
937        zum = "1.0"
938        super-name = { version = "*", package = "crate-name" }
939
940        [workspace]
941        "#;
942
943        let r = crate_name_impl("crate-name", toml).unwrap();
944        assert_eq!(FoundCrate::Name("super_name".to_owned()), r);
945    }
946
947    #[test]
948    fn crate_name_dependency_1() {
949        let toml = r#"
950        [package]
951        name = "foo"
952        version = "0.1.0"
953        edition = "2024"
954        license = "Apache-2.0"
955
956        [dev-dependencies.super-foo]
957        version = "*"
958        package = "crate-name"
959
960        [workspace]
961        "#;
962
963        let r = crate_name_impl("crate-name", toml).unwrap();
964        assert_eq!(FoundCrate::Name("super_foo".to_owned()), r);
965    }
966
967    #[test]
968    fn crate_name_dependency_2() {
969        let toml = r#"
970        [package]
971        name = "foo"
972        version = "0.1.0"
973        edition = "2024"
974        license = "Apache-2.0"
975
976        [dependencies.super-foo]
977        version = "*"
978        package = "crate-name"
979
980        [workspace]
981        "#;
982
983        let r = crate_name_impl("crate-name", toml).unwrap();
984        assert_eq!(FoundCrate::Name("super_foo".to_owned()), r);
985    }
986
987    #[test]
988    fn crate_name_dependency_3() {
989        let toml = r#"
990        [package]
991        name = "foo"
992        version = "0.1.0"
993        edition = "2024"
994        license = "Apache-2.0"
995
996        [dependencies.crate-name]
997        version = "*"
998
999        [workspace]
1000        "#;
1001
1002        let r = crate_name_impl("crate-name", toml).unwrap();
1003        assert_eq!(FoundCrate::Name("crate_name".to_owned()), r);
1004    }
1005}