set_span/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(clippy::needless_doctest_main)]
3
4use std::convert::identity;
5
6use proc_macro::{Delimiter, Group, Span, TokenStream, TokenTree as TT};
7use proc_macro_tool::{
8    err, rerr, try_pfunc, GetSpan, ParseIter, ParseIterExt,
9    SetSpan, TokenStreamExt, TokenTreeExt, WalkExt,
10};
11
12fn eoi(iter: impl IntoIterator<Item = TT>) -> Result<(), TokenStream> {
13    if let Some(tt) = iter.into_iter().next() {
14        Err(err("unexpected token, expected end of input", tt))
15    } else {
16        Ok(())
17    }
18}
19
20fn fmt(g: Group) -> String {
21    if g.is_delimiter(Delimiter::None) {
22        format!("Ø{g}Ø")
23    } else {
24        g.to_string()
25    }
26}
27
28const NUM_SUFFIXS: &[&str] = &[
29    "i8", "i16", "i32", "i64", "i128",
30    "u8", "u16", "u32", "u64", "u128",
31];
32trait StrExt {
33    fn remove_number_suffix(&self) -> &Self;
34}
35impl StrExt for str {
36    fn remove_number_suffix(&self) -> &Self {
37        for &suffix in NUM_SUFFIXS {
38            if let Some(s) = self.strip_suffix(suffix) {
39                return s;
40            }
41        }
42        return self;
43    }
44}
45
46#[must_use]
47fn index_tt<I>(mut tt: TT, iter: &mut ParseIter<I>) -> Result<TT, TokenStream>
48where I: Iterator<Item = TT>,
49{
50    while let Some((mut span, mut param)) = iter
51        .next_if(|tt| tt.is_delimiter(Delimiter::Bracket))
52        .map(|tt| tt.into_group().unwrap())
53        .map(|g| (g.span_close(), g.stream().into_iter()))
54    {
55        let i = param.next()
56            .ok_or_else(|| err!("unexpected token, expected literal", span))?
57            .span_as(&mut span)
58            .into_literal()
59            .map_err(|_| err!("unexpected token, expected literal", span))?
60            .to_string()
61            .remove_number_suffix()
62            .parse()
63            .map_err(|e| err!(@("parse number {e}"), span))?;
64        let g = tt.into_group()
65            .map_err(|t| err!(@("cannot index {t}, e.g [...]"), span))?;
66        tt = g.stream().into_iter().nth(i)
67            .ok_or_else(|| err!(@("index {i} out of range, of {}", fmt(g)), span))?
68    };
69    Ok(tt)
70}
71
72fn parse_input_span<I>(iter: I) -> Result<TT, TokenStream>
73where I: Iterator<Item = TT>,
74{
75    let mut iter = iter.parse_iter();
76    if iter.peek_puncts("#").is_some()
77    && iter.peek_i_is(1, |t| t.is_keyword("mixed"))
78    {
79        return Ok(iter.next().unwrap().set_spaned(Span::mixed_site()));
80    }
81
82    let mut tt = iter.next()
83        .ok_or_else(|| err!("unexpected comma of input start"))?;
84
85    tt = index_tt(tt, &mut iter)?;
86
87    if let Some(end) = iter.next() {
88        rerr!("unexpected token, expected [...] or comma", end)
89    }
90
91    Ok(tt)
92}
93
94fn extract_expand_body<I>(
95    input: &mut ParseIter<I>,
96    span: Span,
97) -> Result<TokenStream, TokenStream>
98where I: Iterator<Item = TT>,
99{
100    let Some(tt) = input.next() else {
101        rerr!("unexpected end of input, expected a brace {...}", span)
102    };
103    let Some(group) = tt.as_group() else {
104        rerr!("unexpected token, expected a brace {...}", tt)
105    };
106
107    let out = if group.is_solid_group() {
108        group.stream()
109    } else {
110        extract_expand_body(input, group.span())?
111    };
112
113    eoi(input)?;
114
115    Ok(out)
116}
117
118fn do_operation(
119    input: TokenStream,
120    spant: TT,
121) -> Result<TokenStream, TokenStream> {
122    try_pfunc(input, false, [
123        "set_span",
124        "set_index_span",
125    ], |i, param| {
126        Ok(match &*i.to_string() {
127            "set_span" => {
128                param.stream()
129                    .walk(|tt| tt.set_spaned(spant.span()))
130            },
131            "set_index_span" => {
132                let iter = &mut param.stream().parse_iter();
133                let spant = index_tt(spant.clone(), iter)?;
134                let result = iter.next()
135                    .ok_or_else(|| err!("unexpected end of input, expected {...}", param))?
136                    .into_group()
137                    .map_err(|t| err!("expected {...}", t))?
138                    .stream()
139                    .walk(|tt| tt.set_spaned(spant.span()));
140                eoi(iter)?;
141                result
142            },
143            _ => unreachable!(),
144        })
145    })
146}
147
148fn set_span_impl(input: TokenStream) -> Result<TokenStream, TokenStream> {
149    let Some((spani, mut input)) = input.split_puncts(",") else {
150        rerr!("unexpected end of input, expected comma");
151    };
152    let span = parse_input_span(spani.into_iter())?;
153    let input = extract_expand_body(&mut input, Span::call_site())?;
154    do_operation(input, span)
155}
156
157/// Set the span of certain tokens in the code block to the span of the input token
158///
159/// - grammar := span `,` `{` code\* `}`
160/// - span := `#mixed` / any-token index\*
161/// - index := `[` num-literal `]`
162/// - code := `#set_span` `{` ... `}`<br>
163///         / `#set_index_span` `{` index\* `{` ... `}` `}`<br>
164///         / any-token
165///
166/// `{`, `}` is general bracket, contain `()` `[]` `{}`
167///
168/// - `set_span!((a, b, (c))[0], {...})` set `a` span
169/// - `set_span!((a, b, (c))[1], {...})` set `b` span
170/// - `set_span!((a, b, (c))[2], {...})` set `(c)` span
171/// - `set_span!((a, b, (c))[2][0], {...})` set `c` span
172///
173/// Similarly, there is also
174/// `set_span!((a), {#set_index_span([0]{...})})` set `a` span
175///
176///
177/// # Example
178/// ```
179/// macro_rules! foo {
180///     ($t:tt) => {
181///         foo! { ($t) ($t) }
182///     };
183///     ($t:tt (0)) => {
184///         set_span::set_span! {$t[0], {
185///             #set_span {
186///                 compile_error! {"input by zero"}
187///             }
188///         }}
189///     };
190///     ($_:tt ($lit:literal)) => { /*...*/ };
191/// }
192/// // foo!(0); // test error msg
193/// ```
194#[proc_macro]
195pub fn set_span(input: TokenStream) -> TokenStream {
196    set_span_impl(input).unwrap_or_else(identity)
197}