summaryrefslogtreecommitdiff
path: root/rust/macros/fmt.rs
blob: 2f4b9f6e2211040bf784b19af5913b11d9d55e2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// SPDX-License-Identifier: GPL-2.0

use proc_macro::{Ident, TokenStream, TokenTree};
use std::collections::BTreeSet;

/// Please see [`crate::fmt`] for documentation.
pub(crate) fn fmt(input: TokenStream) -> TokenStream {
    let mut input = input.into_iter();

    let first_opt = input.next();
    let first_owned_str;
    let mut names = BTreeSet::new();
    let first_span = {
        let Some((mut first_str, first_span)) = (match first_opt.as_ref() {
            Some(TokenTree::Literal(first_lit)) => {
                first_owned_str = first_lit.to_string();
                Some(first_owned_str.as_str()).and_then(|first| {
                    let first = first.strip_prefix('"')?;
                    let first = first.strip_suffix('"')?;
                    Some((first, first_lit.span()))
                })
            }
            _ => None,
        }) else {
            return first_opt.into_iter().chain(input).collect();
        };

        // Parse `identifier`s from the format string.
        //
        // See https://doc.rust-lang.org/std/fmt/index.html#syntax.
        while let Some((_, rest)) = first_str.split_once('{') {
            first_str = rest;
            if let Some(rest) = first_str.strip_prefix('{') {
                first_str = rest;
                continue;
            }
            if let Some((name, rest)) = first_str.split_once('}') {
                first_str = rest;
                let name = name.split_once(':').map_or(name, |(name, _)| name);
                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
                    names.insert(name);
                }
            }
        }
        first_span
    };

    let adapter = quote_spanned!(first_span => ::kernel::fmt::Adapter);

    let mut args = TokenStream::from_iter(first_opt);
    {
        let mut flush = |args: &mut TokenStream, current: &mut TokenStream| {
            let current = std::mem::take(current);
            if !current.is_empty() {
                let (lhs, rhs) = (|| {
                    let mut current = current.into_iter();
                    let mut acc = TokenStream::new();
                    while let Some(tt) = current.next() {
                        // Split on `=` only once to handle cases like `a = b = c`.
                        if matches!(&tt, TokenTree::Punct(p) if p.as_char() == '=') {
                            names.remove(acc.to_string().as_str());
                            // Include the `=` itself to keep the handling below uniform.
                            acc.extend([tt]);
                            return (Some(acc), current.collect::<TokenStream>());
                        }
                        acc.extend([tt]);
                    }
                    (None, acc)
                })();
                args.extend(quote_spanned!(first_span => #lhs #adapter(&#rhs)));
            }
        };

        let mut current = TokenStream::new();
        for tt in input {
            match &tt {
                TokenTree::Punct(p) if p.as_char() == ',' => {
                    flush(&mut args, &mut current);
                    &mut args
                }
                _ => &mut current,
            }
            .extend([tt]);
        }
        flush(&mut args, &mut current);
    }

    for name in names {
        let name = Ident::new(name, first_span);
        args.extend(quote_spanned!(first_span => , #name = #adapter(&#name)));
    }

    quote_spanned!(first_span => ::core::format_args!(#args))
}