proc-macro-workshop:sorted-5
审题
// Get ready for a challenging step -- this test case is going to be a much
// bigger change than the others so far.
//
// Not only do we want #[sorted] to assert that variants of an enum are written
// in order inside the enum definition, but also inside match-expressions that
// match on that enum.
//
// #[sorted]
// match conference {
// RustBeltRust => "...",
// RustConf => "...",
// RustFest => "...",
// RustLatam => "...",
// RustRush => "...",
// }
//
// Currently, though, procedural macro invocations on expressions are not
// allowed by the stable compiler! To work around this limitation until the
// feature stabilizes, we'll be implementing a new #[sorted::check] macro which
// the user will need to place on whatever function contains such a match.
//
// #[sorted::check]
// fn f() {
// let conference = ...;
//
// #[sorted]
// match conference {
// ...
// }
// }
//
// The #[sorted::check] macro will expand by looking inside the function to find
// any match-expressions carrying a #[sorted] attribute, checking the order of
// the arms in that match-expression, and then stripping away the inner
// #[sorted] attribute to prevent the stable compiler from refusing to compile
// the code.
//
// Note that unlike what we have seen in the previous test cases, stripping away
// the inner #[sorted] attribute will require the new macro to mutate the input
// syntax tree rather than inserting it unchanged into the output TokenStream as
// before.
//
// Overall, the steps to pass this test will be:
//
// - Introduce a new procedural attribute macro called `check`.
//
// - Parse the input as a syn::ItemFn.
//
// - Traverse the function body looking for match-expressions. This part will
// be easiest if you can use the VisitMut trait from Syn and write a visitor
// with a visit_expr_match_mut method.
//
// - For each match-expression, figure out whether it has #[sorted] as one of
// its attributes. If so, check that the match arms are sorted and delete
// the #[sorted] attribute from the list of attributes.
//
// The result should be that we get the expected compile-time error pointing out
// that `Fmt` should come before `Io` in the match-expression.
//
//
// Resources:
//
// - The VisitMut trait to iterate and mutate a syntax tree:
// https://docs.rs/syn/1.0/syn/visit_mut/trait.VisitMut.html
//
// - The ExprMatch struct:
// https://docs.rs/syn/1.0/syn/struct.ExprMatch.html
use sorted::sorted;
use std::fmt::{self, Display};
use std::io;
#[sorted]
pub enum Error {
Fmt(fmt::Error),
Io(io::Error),
}
impl Display for Error {
#[sorted::check]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
#[sorted]
match self {
Io(e) => write!(f, "{}", e),
Fmt(e) => write!(f, "{}", e),
}
}
}
fn main() {}
这里主要有两个惊喜
#[sorted::check]
:这个和#[tokio::main]
何其相像match
并不支持函数式宏,但是fn
支持
从错误提示我们可以知道将要实现什么功能
就是检测error: Fmt should sort before Io --> tests/05-match-expr.rs:88:13 | 88 | Fmt(e) => write!(f, "{}", e), | ^^^
match
中的分支顺序。
因此整体的思路是
- 通过检测
#[sorted::check]
标记的方法,查找其中的#[sorted]
并进行检测。 - 移除
#[sorted]
保证代码解析正确
抽取
我们要查找的被#[sorted]
标记的match
,虽然核心是#[sorted]
,但是match
才是主体,且不确定数量,因此,需要开启visit
,同时,还要移除#[sorted]
,所以真正应该开启的是visit-mut
。
// common.rs
// 递归扫描
impl syn::visit_mut::VisitMut for MatchVisitor {
fn visit_expr_match_mut(&mut self, i: &mut syn::ExprMatch) {
// 标记属性
let mut target_idx: isize = -1;
for (idx, attr) in i.attrs.iter().enumerate() {
// 是否标记了#[sorted]
if path_to_string(&attr.path) == "sorted" {
target_idx = idx as isize;
break;
}
}
// 如果标记了才处理
if target_idx != -1 {
// 移除#[sorted]
i.attrs.remove(target_idx as usize);
// 收集names
let mut match_arm_names: Vec<(String, &dyn quote::ToTokens)> = Vec::new();
for arm in i.arms.iter() {
// 匹配分支
match &arm.pat {
// 路径匹配
syn::Pat::Path(p) => {
match_arm_names.push((path_to_string(&p.path), &p.path));
}
// 元组匹配
syn::Pat::TupleStruct(p) => {
match_arm_names.push((path_to_string(&p.path), &p.path));
}
// 结构匹配
syn::Pat::Struct(p) => {
match_arm_names.push((path_to_string(&p.path), &p.path));
}
// 标识符匹配
syn::Pat::Ident(p) => {
match_arm_names.push((p.ident.to_string(), &p.ident));
}
// _ 券匹配
syn::Pat::Wild(p) => {
match_arm_names.push(("_".to_string(), &p.underscore_token));
}
// 没啥匹配
_ => {
self.err = std::option::Option::Some(syn::Error::new_spanned(
&arm.pat,
"unsupported by #[sorted]",
));
return;
}
}
}
// 检查错误
if let Some(e) = check_order(match_arm_names) {
self.err = std::option::Option::Some(e);
return;
}
}
// 递归查找
syn::visit_mut::visit_expr_match_mut(self, i)
}
}
// 全路径拼接
fn path_to_string(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<String>>()
.join("::")
}
这里主要从match
的角度进行思考,匹配的场景有很多,这里只是列举出了基本的几种。
虽然代码中基本算是TupleStruct
,但是其他的情况可能也会有,算是小小的拓展。
还有一点,就是关于visit
,之前我们也使用过visit
,但是需要主要它的入口和接续。
我们解析这一个之后,其实限定的上下文是在一个fn
里面,因此visit_expr_match_mut
。
题解
// solution5.rs
pub(crate) fn solution(fn_item: &mut syn::ItemFn) -> syn::Result<proc_macro2::TokenStream> {
let mut visitor = crate::common::MatchVisitor {
err: std::option::Option::None,
};
syn::visit_mut::visit_item_fn_mut(&mut visitor, fn_item);
match visitor.err {
Some(e) => syn::Result::Err(e),
None => syn::Result::Ok(crate::common::to_token_stream(fn_item)),
}
}
这里可以看出来,visit
的入口其实是visit_item_fn_mut
。
虽然都是递归遍历,不过需要注意入口和接续的遍历位置。
整体
虽然看起来有两个#[sorted]
,但是不得不打断的是,其中有一个假货。if path_to_string(&attr.path) == "sorted"
,这里解析的其实是一个标记,并不具备排序功能,这里真正生效的应该是#[sorted::check]
,分支下面的#[sorted]
就像是派生宏的惰性绑定一样,归属于#[sorted::check]
之下。
也就是说,我们实际上还定义了一个宏
mod common;
mod solution1;
mod solution2;
mod solution3;
mod solution5;
#[proc_macro_attribute]
pub fn sorted(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = syn::parse_macro_input!(input as syn::Item);
match solution1::solution(&item) {
syn::Result::Ok(stream) => stream,
syn::Result::Err(e) => {
let mut res = e.into_compile_error();
res.extend(crate::common::to_token_stream(item));
res
}
}
.into()
}
#[proc_macro_attribute]
pub fn check(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut fn_item = syn::parse_macro_input!(input as syn::ItemFn);
match solution5::solution(&mut fn_item) {
syn::Result::Ok(stream) => stream,
syn::Result::Err(e) => {
let mut res = e.into_compile_error();
res.extend(crate::common::to_token_stream(fn_item));
res
}
}
.into()
}
这里的#[sorted::check]
还能够给我们一个提示,这里的sorted
并非是一个macro
,而是一个crate
,并不存在关联或者归属的属性宏,固然有惰性绑定的宏,但只不过是一个宏解析时候的标记,绑定的宏并不具备解析入口的功能。
我一直以为
#[tokio::main]
有什么黑魔法,只不过是长路径。
这里比较容易模糊的是标记枚举#[sorted]
和标记match
的#[sorted]
的区别。
对于标记枚举的#[sorted]
可以修改为#[sorted::sorted]
,但是标记match
的#[sorted]
想要修改为#[sorted::sorted]
,需要修改#[sorted::check]
的内部处理方式,因为一个是确定的宏使用不同路劲,而一个是直接更换了解析方式。
开始的时候我还搞混了这两个,还以为
check
下的sorted
解析不完全…
其实,到这里,
sorted
已经结束了,后续娓娓道来。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: