如何用声明式Rust宏遍历AST提取文档字符串?是否应改用过程宏?
问题描述
我想编写一个宏,接收代码块并展开,同时收集其中的文档字符串(利用文档字符串是属性的特性)。例如输入:
doc_grab! { /// Struct A struct A { /// Field x x: u32, } }
预期展开结果:
const A_DOC: ... = ... "Struct A" ... "Field x" ...; [doc = "Struct A"] struct A { [doc = "Field x"] x: u32, }
我知道过程宏能实现这类功能(比如Clap的derive API),但想确认:能不能用声明式宏实现?还是更适合用过程宏?
我尝试写了一个简单的walker!宏想递归遍历结构,但运行报错:
macro_rules! walker { () => {}; ($body:tt $($rest:tt)*) => { $body walker!($($rest)*) }; } walker! { fn get_three(x): u8 { 1 + 2 } }
错误信息:
error: expected one of `(` or `<`, found `!` --> src/main.rs:60:21 | 60 | $body walker!($($rest)*) | ^ expected one of `(` or `<` ... 64 | / walker! { 65 | | fn get_three(x): u8 { 66 | | 1 + 2 67 | | } 68 | | } | |_- in this macro invocation | = note: this error originates in the macro `walker` (in Nightly builds, run with -Z macro-backtrace for more info)
通过rust-analyzer看到,宏展开第一个fn后就无法处理后续语法:
// Recursive expansion of walker! macro // ===================================== fn walker !(get_three(x):u8 { 1+2 })
如果为每个核心语法写分支,遇到其他宏时会变得异常复杂。
所以我想知道:
- 声明式宏能不能遍历语法树?
- 是不是更适合改用过程宏?
- 如果用过程宏,解析展开后语法树的最佳方式是什么(类似Racket的
local-expand,但Rust里没找到类似功能)?
解答
1. 声明式宏无法可靠遍历语法树
声明式宏(macro_rules!)基于token匹配工作,它不理解Rust的语法结构,只能按token序列做模式匹配。你写的walker!宏报错的原因是:它把fn当成单个tt(token tree)输出后直接拼接walker!(...),导致Rust语法解析器把fn walker当成一个标识符,后面的!就成了语法错误——fn定义后不能直接跟宏调用。
哪怕为每个语法结构(struct、fn、字段等)单独写匹配分支,也会遇到两个致命问题:
- 无法处理嵌套宏调用:声明式宏是先展开外层再处理内层,若代码块里有其他宏,根本无法提前预知其展开后的token,自然没法匹配。
- 维护成本极高:Rust语法复杂,要覆盖所有合法语法场景(泛型、属性、嵌套表达式等)几乎不可能,每加一个场景就要新增大量分支。
因此,用声明式宏实现完整的文档字符串收集+代码展开完全不现实。
2. 优先选择过程宏实现
过程宏(尤其是类函数过程宏)是处理这类需求的正确选择,原因如下:
- 过程宏能拿到完整的语法抽象树(AST):通过
syn库可直接将输入代码解析为结构化的AST节点,能准确识别文档属性(#[doc]或///)、结构体、字段等元素。 - 可灵活修改和生成代码:用
quote库可基于修改后的AST生成符合语法的输出代码,完美实现“收集文档字符串生成常量+保留原代码带文档属性”的需求。
3. 过程宏解析语法树的最佳实践
Rust生态有成熟工具链处理过程宏的AST解析与代码生成:
syn库:负责将输入token流解析为强类型AST节点,支持解析几乎所有Rust语法,包括文档属性、结构体、函数、表达式等。你可以通过syn::Attribute提取文档字符串,遍历结构体字段获取各自的文档。quote库:负责将AST节点或手动构造的代码片段转换为可编译的token流,语法与Rust代码几乎一致,编写直观。- 关于类似Racket
local-expand的功能:Rust过程宏在编译的语法分析阶段运行,编译器会先展开所有内层宏,再把最终token流传给过程宏处理。若需处理未展开的宏,syn也支持解析宏调用节点,可选择保留或做特殊处理。
简单实现思路(基于syn和quote):
- 用
syn::parse_macro_input!将输入解析为对应项的结构体,比如syn::ItemStruct。 - 遍历结构体属性,提取
#[doc]属性内容并收集到常量中。 - 遍历结构体字段,提取每个字段的
#[doc]属性内容,也加入常量。 - 用
quote生成包含文档常量和原结构体(保留文档属性)的代码。
内容的提问来源于stack exchange,提问作者Leif Andersen
相关产品推荐
相关产品推荐

