Skip to main content

batch_impl/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2// 库不使用任何 unsafe;缺失文档按错误拒绝(仅作用于 pub 项,内部 pub(crate) 不受限)。
3#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5// MSVC 链接器输出"正在创建库…和对象…"到 stdout,被 rustc 当 linker_messages 告警,
6// 属于无害的 Windows 链接产物提示,全局抑制。
7#![allow(linker_messages)]
8#[cfg(test)]
9mod fuzz;
10use proc_macro2::{TokenStream, TokenTree};
11use quote::quote;
12use syn::{ItemTrait, parse_macro_input};
13
14mod apply;
15mod ast;
16mod batch_trait_entry;
17mod codegen;
18mod diagnostic;
19mod parse;
20mod path_prefix;
21mod preprocess;
22mod scan;
23
24use batch_trait_entry::parse_batch_trait_entry;
25
26use ast::{Op, reset_fresh_counter};
27use diagnostic::compile_error_str;
28use preprocess::where_process;
29use preprocess::{build_from_item, get_trait_item, parse_names_from_tokens};
30use scan::{Cursor, scan_stop};
31
32/// 为 trait 批量生成 `impl` 块的属性宏。
33///
34/// 在 trait 定义上标注 `#[batch_impl(...)]`,宏参数中的每个 impl-spec 都会
35/// 为该 trait 生成一个对应的 `impl` 块。
36///
37/// ## 语法
38///
39/// ```text
40/// #[batch_impl( impl-spec [, impl-spec]* [{ body }]? )]
41/// ```
42///
43/// impl-spec 由三部分组成(均可省略后半部分):
44/// - `<impl-泛型>` — `impl` 块的泛型参数
45/// - `Trait名<trait-泛型>` — trait 的泛型参数与关联类型绑定
46/// - 目标类型 — 用 `[]` 包裹表示并列,用 `^`/`-` 表示泛型应用
47///
48/// ## 示例
49///
50/// ```
51/// # use batch_impl::batch_impl;
52/// #[batch_impl(usize, isize)]
53/// trait Numeric {}
54///
55/// #[batch_impl(<T> Vec<T>)]
56/// trait Collection {}
57///
58/// #[batch_impl(<T> FromValue<T> [i32 { fn wrap(_: T) -> Self { 0 }}, u32 #wrap{0}] )]
59/// trait FromValue<T> { fn wrap(val: T) -> Self; }
60///
61/// // #name{body} 也支持 const 和 type 项
62/// #[batch_impl(usize #MY_CONST{42})]
63/// trait HasConst { const MY_CONST: usize; }
64///
65/// ```
66#[proc_macro_attribute]
67pub fn batch_impl(
68    attr: proc_macro::TokenStream, item: proc_macro::TokenStream,
69) -> proc_macro::TokenStream {
70    let trait_item = parse_macro_input!(item as ItemTrait);
71    expand_attr_macro(attr, trait_item, true).unwrap_or_else(Into::into)
72}
73
74/// 与 `#[batch_impl]` 相同,但丢弃被标注的 trait 定义,只输出 `impl` 块。
75///
76/// 用于 trait 已在别处定义、只需批量生成 impl 的场景。被标注的 trait 仅作为
77/// 指令系统的"签名真相源":`#name`/`#fill`/`#delegate` 从它读取 item 签名,
78/// 开放扩展 `#name(args){body}` 把(方法名列表, body, 整个 trait)一起交给
79/// 用户的同名函数式宏(见 README「指令系统」)。语法与 `#[batch_impl]` 完全一致。
80///
81/// ## 示例
82///
83/// ```
84/// # use batch_impl::batch_impl_only;
85/// trait Greet { fn hello(&self) -> &str; }
86///
87/// #[batch_impl_only(usize #hello{"hi"})]
88/// trait Greet { fn hello(&self) -> &str; } // 此 trait 定义被丢弃,不影响已有的定义
89/// // 这样写而不用batch_trait是为了使用指令系统,建议按trait定义处按原样写
90/// ```
91#[proc_macro_attribute]
92pub fn batch_impl_only(
93    attr: proc_macro::TokenStream, item: proc_macro::TokenStream,
94) -> proc_macro::TokenStream {
95    let trait_item = parse_macro_input!(item as ItemTrait);
96    expand_attr_macro(attr, trait_item, false).unwrap_or_else(Into::into)
97}
98
99/// 两个属性宏的共享实现(错误经 `compile_error!` token 流返回)
100fn expand_attr_macro(
101    attr: proc_macro::TokenStream, trait_item: ItemTrait, include_trait: bool,
102) -> Result<proc_macro::TokenStream, TokenStream> {
103    reset_fresh_counter();
104    let trait_name = trait_item.ident.clone();
105    let attr_vec = TokenStream::from(attr).into_iter().collect::<Vec<_>>();
106    // 入口转换:真实 None 组扁平化(宏变量展开产物,内容即 DSL token)
107    // + 扁平 `<...>` 配对为尖括号组(`->` 箭头不参与)——下游解析不再管 `<>` 深度
108    let attr_vec = preprocess::angle_collect(&attr_vec)?;
109
110    // `#[batch_impl_only]` 专属:attr 起首若是 `# Path: ` 形式
111    // (`#` + `Ident (:: Ident)*` + `:`),则把该路径作为外部 trait 路径,
112    // 余下 attr 作为 DSL spec。`#[batch_impl]` 不支持此前缀
113    // (它输出本地 trait 定义,路径前缀无意义)。
114    let (trait_full_path, trait_last_ident, rest_tokens) = if !include_trait {
115        match path_prefix::try_parse_path_prefix(&attr_vec) {
116            Some((path, last_ident, rest)) => {
117                // 路径前缀的 last ident 必须与本地 dummy trait 名一致,
118                // 否则后续 DSL 中的 `Trait<T>` 匹配会失败。
119                match last_ident {
120                    Some(id) if id == trait_name => {
121                        let path_ts = path.into_iter().collect();
122                        // 此处借用本地 trait_name 作为匹配标识
123                        // (已校验与路径末段同名)。
124                        (path_ts, trait_name.clone(), rest)
125                    }
126                    Some(id) => {
127                        let msg = format!(
128                            "batch-impl: 路径前缀 `#...{}` \
129                                 的末尾标识符与 trait 名 `{}` \
130                                 不一致;二者必须相同",
131                            id, trait_name,
132                        );
133                        return Err(compile_error_str(&msg));
134                    }
135                    None => {
136                        let msg = "batch-impl: 路径前缀 `#` 后 \
137                                 期望至少一个标识符作为 trait 路径";
138                        return Err(compile_error_str(msg));
139                    }
140                }
141            }
142            None => (quote![#trait_name], trait_name.clone(), attr_vec.clone()),
143        }
144    } else {
145        (quote![#trait_name], trait_name.clone(), attr_vec.clone())
146    };
147
148    let mut cursor = Cursor::new(&rest_tokens);
149    let expanded = preprocess::expand_tokens(&mut cursor, &trait_item)?;
150    // 裸 `where 谓词 {body}` 新语法 → 统一改写为旧式 `where{谓词}`
151    // (指令预处理之后、DSL 解析之前;三个接口共用)
152    let expanded = where_process(&mut Cursor::new(&expanded))?;
153    let is_unsafe = trait_item.unsafety.is_some();
154    let trait_bounds = extract_trait_bounds(&trait_item);
155    // `A<>`:trait 泛型照抄(实参与 bound 全部来自 trait 定义)。
156    // 在指令预处理与 where 改写之后、DSL 解析之前展开为
157    // `<'a, T: bounds, const N> A<'a, T, N>`——展开产物与手写完全等价。
158    let expanded = expand_empty_trait_generics(&expanded, &trait_item)?;
159    cursor = Cursor::new(&expanded);
160    let start_trait = if include_trait { trait_item.into() } else { None };
161    let impls = parse_batch_trait_entry(
162        &mut cursor,
163        Op::Comma,
164        &trait_full_path,
165        &trait_last_ident,
166        is_unsafe,
167        start_trait,
168        &trait_bounds,
169    );
170    // 出口转换:尖括号组还原为 `<...>` 扁平 token(rustc 只见扁平)
171    Ok(preprocess::render_angles(impls).into())
172}
173
174/// trait 形参:名字 + 内联 bound + bound 引用的形参名(token 级保守检测)。
175#[derive(Default)]
176pub(crate) struct TraitParam {
177    pub(crate) name: String,
178    pub(crate) bound: Option<TokenStream>,
179    pub(crate) refs: Vec<String>,
180}
181
182/// trait 泛型形参列表(按位置对应 spec 中的 trait 实参),供 codegen 对
183/// **未写 bound 的 impl 泛型参数**按位置 + 同名继承。
184///
185/// 自动化只认同名(`A<>` 照抄 / `<T> A<T>` 同名继承):
186/// - impl 参数按"名字在 trait 实参中的位置"对应形参;形参有 bound 且同名 → 继承;
187/// - 异名 → `compile_error!`(请改名或手写 bound);
188/// - 继承的 bound 引用其他形参名(`T: 'a` 的 `'a`、`U: Vec<T>` 的 `T`)而 impl
189///   未声明同名 → `compile_error!`(请声明同名或手写)。
190///
191/// 写 bound = 用户负责,宏不干预(sub trait 蕴含(`trait B: A` 使 `T: B`
192/// 隐含 `T: A`)宏无法推理)。trait 级 where 子句不继承(第一版范围)。
193#[derive(Default)]
194pub(crate) struct TraitBounds {
195    pub(crate) params: Vec<TraitParam>,
196}
197
198fn extract_trait_bounds(trait_item: &ItemTrait) -> TraitBounds {
199    // 形参名集合(类型 + const 为 Ident,生命周期带 `'` 前缀)
200    let type_const_names: Vec<String> = trait_item
201        .generics
202        .params
203        .iter()
204        .filter_map(|p| match p {
205            syn::GenericParam::Type(tp) => Some(tp.ident.to_string()),
206            syn::GenericParam::Const(cp) => Some(cp.ident.to_string()),
207            _ => None,
208        })
209        .collect();
210    let lt_names: Vec<String> = trait_item
211        .generics
212        .params
213        .iter()
214        .filter_map(|p| match p {
215            syn::GenericParam::Lifetime(ld) => {
216                Some(format!("'{}", ld.lifetime.ident))
217            }
218            _ => None,
219        })
220        .collect();
221    let mut params = vec![];
222    for p in &trait_item.generics.params {
223        match p {
224            syn::GenericParam::Type(tp) => {
225                let bound = if tp.bounds.is_empty() {
226                    None
227                } else {
228                    // 注意:quote 插值只支持 `#ident`,不支持字段访问
229                    // `#tp.bounds`(会把 `.bounds` 当字面量输出)
230                    let b = &tp.bounds;
231                    Some(quote!(#b))
232                };
233                let refs = bound
234                    .as_ref()
235                    .map(|b| bound_refs(b, &type_const_names, &lt_names))
236                    .unwrap_or_default();
237                params.push(TraitParam { name: tp.ident.to_string(), bound, refs });
238            }
239            syn::GenericParam::Lifetime(ld) => params.push(TraitParam {
240                name: format!("'{}", ld.lifetime.ident),
241                bound: None,
242                refs: vec![],
243            }),
244            syn::GenericParam::Const(cp) => params.push(TraitParam {
245                name: cp.ident.to_string(),
246                bound: None,
247                refs: vec![],
248            }),
249        }
250    }
251    TraitBounds { params }
252}
253
254/// 保守的 bound 形参引用检测:收集 bound token 中出现的形参名。
255/// 宁可误报(HRTB 局部名与形参撞名等)——误报只导致"拒绝自动继承、引导手写",
256/// 绝不生成引用错误名字的代码。
257fn bound_refs(
258    bound: &TokenStream, type_const_names: &[String], lt_names: &[String],
259) -> Vec<String> {
260    let mut refs = vec![];
261    let mut iter = bound.clone().into_iter().peekable();
262    while let Some(tt) = iter.next() {
263        match tt {
264            TokenTree::Ident(id) if type_const_names.contains(&id.to_string()) => {
265                refs.push(id.to_string())
266            }
267            TokenTree::Punct(p) if p.as_char() == '\'' => {
268                if let Some(TokenTree::Ident(id)) = iter.peek() {
269                    let name = format!("'{}", id);
270                    if lt_names.contains(&name) {
271                        refs.push(name);
272                    }
273                }
274            }
275            _ => {}
276        }
277    }
278    refs
279}
280
281/// 实参段是否"纯绑定"(`Item = T, K = U`:每个顶层逗号段都含 `=`)。
282/// 判定为纯绑定才允许 `A<绑定们>` 照抄展开;含位置参数的 `A<T, Item=U>`
283/// 是普通 DSL 语法(不展开,位置参数由用户声明)。
284fn args_all_bindings(args: &[TokenTree]) -> bool {
285    let mut rest = args;
286    while let Some(idx) = scan_stop(rest, &[',']) {
287        // 段必须含顶层 `=`(绑定)
288        if scan_stop(&rest[..idx], &['=']).is_none() {
289            return false;
290        }
291        rest = &rest[idx + 1..];
292    }
293    scan_stop(rest, &['=']).is_some()
294}
295
296/// `A<>` / `A<绑定们>` 预处理:扫描顶层 token 流中的 `Ident` + 尖括号组
297/// (`angle_collect` 配对产物,空实参或纯绑定实参),展开为
298/// `Group(None, <'a, T: bounds, const N>) Ident Group(None, <'a, T, N, Item = T>)`
299/// ——与 `angle_collect` 的配对产物形态一致,parse 层无需区分来源。
300///
301/// - 只处理**顶层**的 `Ident` + 尖括号组(`B<A<>>` 嵌套在组内,不展开;
302///   含位置参数的 `A<T, Item=U>` 是普通 DSL 语法,不展开);
303/// - trait 无泛型参数时透传(`A<>` 由 DSL 解析为空实参,渲染 `A`);
304/// - 仅 `#[batch_impl]` / `#[batch_impl_only]` 可用(需要 trait 定义渲染形参);
305///   `batch_trait!` 无 trait 定义,`A<>` 原样透传。
306fn expand_empty_trait_generics(
307    tokens: &[TokenTree], trait_def: &ItemTrait,
308) -> Result<Vec<TokenTree>, TokenStream> {
309    if trait_def.generics.params.is_empty() {
310        return Ok(tokens.to_vec());
311    }
312    // 预渲染实参名列表(展开时作为尖括号组的实参段)
313    let mut arg_names: Vec<TokenStream> = vec![];
314    for p in &trait_def.generics.params {
315        match p {
316            syn::GenericParam::Lifetime(ld) => arg_names.push(quote!(#ld)),
317            syn::GenericParam::Type(tp) => {
318                let id = &tp.ident;
319                arg_names.push(quote!(#id));
320            }
321            syn::GenericParam::Const(cp) => {
322                let id = &cp.ident;
323                arg_names.push(quote!(#id));
324            }
325        }
326    }
327    let mut out = vec![];
328    let mut i = 0;
329    while i < tokens.len() {
330        match &tokens[i] {
331            // `Ident` + 尖括号组(`angle_collect` 配对产物)——顶层才展开:
332            // 空实参(`A<>`)或**纯绑定实参**(`A<Item=T>`)→ 位置实参照抄
333            // trait 形参,绑定原样保留;含位置参数的 `A<T, Item=U>` 是普通
334            // DSL 语法(不展开)。组内的 `Ident<>`(嵌套如 `B<A<>>`)不处理。
335            TokenTree::Ident(id) => {
336                let group = match tokens.get(i + 1) {
337                    Some(TokenTree::Group(g))
338                        if g.delimiter() == proc_macro2::Delimiter::None =>
339                    {
340                        g
341                    }
342                    _ => {
343                        out.push(tokens[i].clone());
344                        i += 1;
345                        continue;
346                    }
347                };
348                let args: Vec<TokenTree> = group.stream().into_iter().collect();
349                let bindings_only = !args.is_empty() && args_all_bindings(&args);
350                if args.is_empty() || bindings_only {
351                    // 展开为尖括号组序列(`angle_collect` 的配对产物形态):
352                    // `Group(None, <'a, T: bounds, const N>) A Group(None, <'a, T, N, Item = T>)`
353                    let all_params: Vec<_> =
354                        trait_def.generics.params.iter().collect();
355                    out.push(
356                        proc_macro2::Group::new(
357                            proc_macro2::Delimiter::None,
358                            quote!(#(#all_params),*),
359                        )
360                        .into(),
361                    );
362                    out.extend(quote!(#id));
363                    let args_ts: TokenStream = if args.is_empty() {
364                        quote!(#(#arg_names),*)
365                    } else {
366                        let bind_ts: TokenStream = args.iter().cloned().collect();
367                        quote!(#(#arg_names),* , #bind_ts)
368                    };
369                    out.push(
370                        proc_macro2::Group::new(
371                            proc_macro2::Delimiter::None,
372                            args_ts,
373                        )
374                        .into(),
375                    );
376                    i += 2;
377                } else {
378                    out.push(tokens[i].clone());
379                    i += 1;
380                }
381            }
382            _ => {
383                out.push(tokens[i].clone());
384                i += 1;
385            }
386        }
387    }
388    Ok(out)
389}
390
391/// 对已声明的 trait 批量生成 `impl` 块的函数式宏。
392///
393/// 语法:`unsafe? Trait路径: impl-specs;`,以 `;` 分隔多个 trait 段。
394/// 每段的 `:` 之后是 DSL 表达式,与 `#[batch_impl]` 接受相同的语法。
395///
396/// ## 示例
397///
398/// ```
399/// # use batch_impl::batch_trait;
400/// trait A {}
401/// trait B<T> {}
402/// unsafe trait UnsafeTrait{}
403///
404/// batch_trait!(
405///     A: usize, isize;
406///     B: <T> B<T> Vec<T>;
407///     unsafe UnsafeTrait: usize
408/// );
409/// ```
410///
411/// 路径 trait(如 `foo::C`)同样支持,见 tests/regression.rs。
412#[proc_macro]
413pub fn batch_trait(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
414    expand_batch_trait(input).unwrap_or_else(Into::into)
415}
416
417/// `batch_trait!` 的实际展开(错误经 `compile_error!` token 流返回)
418fn expand_batch_trait(
419    input: proc_macro::TokenStream,
420) -> Result<proc_macro::TokenStream, TokenStream> {
421    reset_fresh_counter();
422    let tokens = TokenStream::from(input).into_iter().collect::<Vec<_>>();
423    let tokens = preprocess::angle_collect(&tokens)?;
424    let tokens = where_process(&mut Cursor::new(&tokens))?;
425    let mut cursor = Cursor::new(&tokens);
426    let mut result = quote![];
427    loop {
428        // 跳过前导 `;`(允许连续多个分号,尾随分号)
429        while cursor.is_punct(';') {
430            cursor.bump();
431        }
432        if cursor.at_end() {
433            break;
434        }
435
436        // `unsafe` 前缀:标记该段所有 impl 为 unsafe impl
437        let is_unsafe = if matches!(cursor.peek(), Some(TokenTree::Ident(id)) if *id == "unsafe")
438        {
439            cursor.bump();
440            true
441        } else {
442            false
443        };
444
445        // 收集 trait 路径(遇到 `:` 停止;`::` 路径分隔符一并收集)。
446        // 尖括号已由 angle_collect 配对为不透明组,无需跟踪 `<>` 深度。
447        let path_start = cursor.pos();
448        while let Some(token) = cursor.peek() {
449            match token {
450                TokenTree::Punct(p) if p.as_char() == ':' => {
451                    if cursor.is_single_colon() {
452                        break;
453                    } else {
454                        cursor.bump();
455                        cursor.bump();
456                    }
457                }
458                _ => cursor.bump(),
459            }
460        }
461        let trait_path = cursor.slice_since(path_start);
462        if trait_path.is_empty() {
463            result.extend(compile_error_str("batch_trait! 中期望 trait 名称"));
464            break;
465        }
466        // trait 完整路径:原样收集 trait_path 的 token 流即可
467        let trait_full_path = trait_path.iter().cloned().collect();
468        // 取路径中的最后一个标识符作为 `trait_name` 匹配用
469        let trait_last_ident =
470            match trait_path
471                .iter()
472                .filter_map(|tt| {
473                    if let TokenTree::Ident(id) = tt { id.into() } else { None }
474                })
475                .next_back()
476            {
477                Some(ident) => ident,
478                None => {
479                    result.extend(compile_error_str(
480                        "batch_trait! 中期望标识符作为 trait 名称",
481                    ));
482                    break;
483                }
484            };
485        if !cursor.is_punct(':') {
486            result.extend(compile_error_str(
487                "batch_trait! 中期望 ':' 分隔 trait 名称和 impl-specs",
488            ));
489            break;
490        }
491        cursor.bump();
492        let impl_code = parse_batch_trait_entry(
493            &mut cursor,
494            Op::Semi,
495            &trait_full_path,
496            trait_last_ident,
497            is_unsafe,
498            None,
499            // batch_trait! 无 trait 定义,无法继承泛型 bound
500            &Default::default(),
501        );
502        result.extend(impl_code);
503    }
504    Ok(preprocess::render_angles(result).into())
505}
506
507/// 测试用开放扩展宏(函数式):`name!{(方法名列表){body} trait T {...}}`。
508///
509/// 从宏输入解析方法名列表、body 与 trait 定义,为每个方法生成
510/// `fn 签名 { body }`(沿用 trait 签名)——等价于把 `#fill` 的实现交给用户。
511///
512/// 用于验证开放指令扩展:`#name(args){body}` 展开为 `{name!{(args){body} trait ...}}`,
513/// 宏调用落在 impl body 中,由用户宏根据 trait 展开为需要的 fn 定义
514/// (见 `tests/dsl.rs` 第 28 节)。
515///
516/// 设计要点:这里必须是**函数式宏调用** `name!{...}`,不能是 `#[name[...]] trait ...`
517/// 属性——trait 不是 impl 块内的合法项(`#[attr] trait` 无法出现在 impl 中),
518/// 而函数式宏在 impl body 位置会被 rustc 展开成关联项。
519#[doc(hidden)]
520#[proc_macro]
521pub fn batch_preprocess_test(
522    input: proc_macro::TokenStream,
523) -> proc_macro::TokenStream {
524    let tokens = TokenStream::from(input).into_iter().collect::<Vec<_>>();
525    let tokens = match preprocess::angle_collect(&tokens) {
526        Ok(v) => v,
527        Err(e) => return e.into(),
528    };
529    // 形如:`(add, inc) {*self+1} trait AddInc {...}`
530    let Some(TokenTree::Group(names_group)) = tokens.first() else {
531        return compile_error_str(
532            "batch-impl: batch_preprocess_test 期望 `(方法名列表){body} trait ...`",
533        )
534        .into();
535    };
536    if names_group.delimiter() != proc_macro2::Delimiter::Parenthesis {
537        return compile_error_str(
538            "batch-impl: batch_preprocess_test 期望 `(方法名列表){body} trait ...`",
539        )
540        .into();
541    }
542    let Some(TokenTree::Group(body_group)) = tokens.get(1) else {
543        return compile_error_str(
544            "batch-impl: batch_preprocess_test 期望 `(方法名列表){body} trait ...`",
545        )
546        .into();
547    };
548    if body_group.delimiter() != proc_macro2::Delimiter::Brace {
549        return compile_error_str(
550            "batch-impl: batch_preprocess_test 期望 `(方法名列表){body} trait ...`",
551        )
552        .into();
553    }
554    let trait_ts = tokens[2..].iter().cloned().collect();
555    let trait_item = match syn::parse2(trait_ts) {
556        Ok(t) => t,
557        Err(_) => {
558            return compile_error_str(
559                "batch-impl: batch_preprocess_test 无法解析 trait 定义",
560            )
561            .into();
562        }
563    };
564    let names = match parse_names_from_tokens(
565        &names_group.stream().into_iter().collect::<Vec<_>>(),
566        &trait_item,
567    ) {
568        Ok(names) => names,
569        Err(e) => return e.into(),
570    };
571    let body = body_group.stream();
572    let mut methods = TokenStream::new();
573    for name in &names {
574        let item = match get_trait_item(&trait_item, name) {
575            Ok(item) => item,
576            Err(e) => return e.into(),
577        };
578        methods.extend(build_from_item(item, &body));
579    }
580    preprocess::render_angles(methods).into()
581}
582
583#[cfg(test)]
584mod angle_tests {
585    use super::*;
586    use proc_macro2::TokenStream as TS2;
587    use std::str::FromStr;
588
589    /// 入口收集 + 出口还原的往返:<...> 配对成组再还原为扁平,token 等价。
590    fn roundtrip(s: &str) -> String {
591        let ts: TS2 = FromStr::from_str(s).unwrap();
592        let v: Vec<_> = ts.into_iter().collect();
593        let collected = preprocess::angle_collect(&v).unwrap();
594        preprocess::render_angles(collected.into_iter().collect()).to_string()
595    }
596
597    #[test]
598    fn angle_roundtrip() {
599        assert_eq!(roundtrip("Vec<T>"), "Vec < T >");
600        assert_eq!(roundtrip("A<B<C>>"), "A < B < C > >");
601        assert_eq!(
602            roundtrip("Box<dyn Fn() + Send>"),
603            "Box < dyn Fn () + Send >"
604        );
605        assert_eq!(roundtrip("<T: Clone> A<T>"), "< T : Clone > A < T >");
606        assert_eq!(roundtrip("A<Item=T>"), "A < Item = T >");
607        // -> 箭头的 > 不参与配对
608        assert_eq!(roundtrip("fn(A) -> B"), "fn (A) -> B");
609    }
610
611    #[test]
612    fn angle_unmatched_errors() {
613        // 孤立的 < / > 是非法输入:报 compile_error!(不再透传)
614        let ts: TS2 = FromStr::from_str("A <").unwrap();
615        assert!(
616            preprocess::angle_collect(&ts.into_iter().collect::<Vec<_>>()).is_err()
617        );
618        let ts: TS2 = FromStr::from_str("A >").unwrap();
619        assert!(
620            preprocess::angle_collect(&ts.into_iter().collect::<Vec<_>>()).is_err()
621        );
622        // `ident![...]` 宏体不进入:内部比较 < 不报错
623        let ts: TS2 = FromStr::from_str("m![a < b]").unwrap();
624        assert!(
625            preprocess::angle_collect(&ts.into_iter().collect::<Vec<_>>()).is_ok()
626        );
627    }
628
629    #[test]
630    fn none_group_flattened() {
631        // 真实 None 组(宏变量展开产物):扁平化后内容里的 <...> 照常配对
632        let inner: TS2 = FromStr::from_str("Vec<T>").unwrap();
633        let none = proc_macro2::Group::new(proc_macro2::Delimiter::None, inner);
634        let collected = preprocess::angle_collect(&[none.into()]).unwrap();
635        let rendered = preprocess::render_angles(collected.into_iter().collect());
636        assert_eq!(rendered.to_string(), "Vec < T >");
637    }
638}