最近,因为任务需要,要找一个可以方便地解析Rust源代码的库。本来以为找到一个趁手的解析器应该是非常简单的,谷歌半天找到了rust-lang团队官方构建的解析库syntex_syntax。结果刚刚导入cargo项目就发现这个库两年前就已经停止维护了。

​ 虽然不报什么期待,我还是试着用了一下syntex_syntax来解析源文件。解析第一个小crate倒还挺顺利,但到了第二个crate直接就栈溢出了,再多试几个样品,发现这个库连dyn关键字都识别不出来,于是乎。。自然是不能用了。

​ 考虑到在写过程宏时有一个同样有解析功能的辅助库syn。我的第一想法便是能不能将syn独立使用(而不是写过程宏来处理syn的输出结果),但在查询许久之后无果,最后就有了在stackoverflow上问的这个蠢问题

导入syn库

​ 总之,库算是找到了,但syn的设计初衷毕竟不是直接用来做源码解析,需要稍作调整才能正常使用。一开始,我写的Cargo.toml像下面这样:

1
2
3
4
[dependencies.syn]
version = "0.15"
default-features = false
features = ["full", "printing", "parsing"]

​ 这里把default-features给关闭,是为了不让syn把过程宏一类默认开启的无用功能导入。后面的features属性则是手动指定了导入的功能。等cargo配置完成以后,可以用一段代码来测试一下解析功能

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let f = "<path to file>";
let mut file = File::open(f).unwrap();
let mut content = String::new();
file.read_to_string(&mut content);
// 这前面都是标准的文件IO,把源文件的代码读入
let ast = syn::parse_file(&content).unwrap();
// 解析文件的抽象语法树
println!("{:?}", ast); // 输出抽象语法树
println!("{} items", ast.items.len());
}

​ 然后。。讲道理,编译运行就可以看到文件对应的抽象语法树了,但rustc好像不这么想:

1
2
3
4
5
6
7
8
9
10
error[E0277]: `syn::file::File` doesn't implement `std::fmt::Debug`
--> src\main.rs:62:22
|
62 | println!("{:?}", ast);
| ^^^ `syn::file::File` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
|
= help: the trait `std::fmt::Debug` is not implemented for `syn::file::File`
= note: required by `std::fmt::Debug::fmt`

error: aborting due to previous error

syn::file::File没有实现Debug?不对劲啊。。文档上明明清清楚楚写着:

impl Debug for File[src][]

​ 于是又倒腾了一阵,我发现一位老哥在issue里提出了同样的疑问。问题的原因,简而言之,就是syn用了一套features来控制它的各种扩展功能实现(虽然看起来好像都是基础功能),所有的功能在该项目的文档里都能找到,不过我还是在这里列一个小表:

feature 用途
full Rust 全套源代码的AST结构
parsing 用于解析源代码
printing 将语法树以Rust源码形式输出
visit 用于访问抽象语法树
visit-mut 用于访问时更改抽象语法树
extra-traits 一些特殊的特性(如Debug, Eq)实现

​ 对于一个语法解析器来说,我们有下面的feature就足够了

1
2
3
4
[dependencies.syn]
version = "0.15"
default-features = false
features = ["full", "printing", "parsing", "extra-traits", "visit"]

​ 配置完成以后,我们上面的代码就可以正常运行了。

遍历抽象语法树

​ 对语法树进行分析的第一步就是要遍历整棵抽象语法树,syn库的visit功能为我们提供了快速访问语法树的便利,只需要实现一个syn::visit::Visit的特性,并重载其中的访问函数,我们就能用这个结构体对语法树进行访问:

1
2
3
4
5
struct FnVisitor;

impl syn::visit::Visit<'_> for FnVisitor {

}

​ 比如,如果说想实现一个visitor来找出并打印所有析构用的let语句,只需要重载Visit特性中的visit_expr函数,形式如下

1
2
3
4
5
6
7
8
9
fn visit_expr(&mut self, i: &'_ Expr) {
match i {
Expr::Let(exprLet) => {
println!("{}", quote::quote!(#exprLet).to_string());
},
_ => {}
}
syn::visit::visit_expr(self, i)
}

​ 这个实现中有以下两点需要注意:

  • 因为Visitor的实现是递归式的,因此在函数调用的最后我们一定要调用visit提供的相应静态函数(比如这里的visit_expr),否则语法树更深层的遍历会在此终止。
  • 这里用到了一个经常和syn一起使用的quote库,用于将抽象语法树格式化成源码字符串,使用quote只需要在Cargo.toml中直接导入
1
2
[dependencies]
quote = "0.6"

​ 写出上述实现以后,便可以直接使用实现的Visitor来遍历代码,只需要使用任意syn::visit中的静态访问函数,并将Visitor作为参数即可,比如,接第一节的例子,可以这么写:

1
2
let ast = syn::parse_file(&content).unwrap();
syn::visit::visit_file(&mut FnVisitor, &ast);

​ 运行代码,可以看到访问器达到了预期的输出:

1
2
3
4
5
6
let Some ( ref mut fut ) = self . fut_ex
let Some ( ref mut fut ) = self . fut_upg
let Some ( ref on_connect ) = self . on_connect
let Some ( ref mut item ) = data
let Some ( ref mut item ) = data
let Some ( mut bytes ) = self . unread . take ( )

​ 其它的访问器函数,虽然访问的语法树对象不同,但实际情况也大同小异。

syn的解析器,由于在实现上仅仅起到了简单的解析作用,因此在遇到诸如函数调用和类型声明时,并不能为我们定位到有效的上下文,因此在进行复杂处理时,需要多费一些功夫。下面的例子中,我们重载了一个访问函数,用于输出所有返回值类型为Result的方法(不关心泛型参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn visit_method_sig(&mut self, i: &'_ MethodSig) {
let ret = &i.decl.output;
match ret {
ReturnType::Default => {}
ReturnType::Type(_, ty) => {
match ty.as_ref() {
Type::Path(path) => {
for segment in path.path.segments.iter() {
if segment.ident.to_string() == String::from("Result") {
println!("{}", quote::quote!(#i).to_string())
}
}
}
_ => {}
}
}
}
syn::visit::visit_method_sig(self, i)
}

​ 乍一看,你可能会被冗长的逻辑吓到。但实际上,这里的代码只进行了三步主要操作,其中有两步是模式匹配:

  • 捕捉方法签名,并获取其返回值(output)

  • 将返回值匹配到实际类型,确认其不是默认类型(即需要编译器推导返回值)

  • 将实际类型匹配到类型路径(即复合类型对应的结构),遍历类型路径的最浅层,寻找是否有Result标识符

    上面的代码在运行结束后,同样会给出令人满意的结果。

1
2
3
4
5
6
fn read ( & mut self , buf : & mut [ u8 ] ) -> io :: Result < usize >
fn write ( & mut self , buf : & [ u8 ] ) -> io :: Result < usize >
fn flush ( & mut self ) -> io :: Result < ( ) >
fn set_nodelay ( & mut self , nodelay : bool ) -> io :: Result < ( ) >
fn set_linger ( & mut self , dur : Option < std :: time :: Duration > ) -> io :: Result < ( ) >
fn set_keepalive ( & mut self , dur : Option < std :: time :: Duration > ) -> io :: Result < ( ) >

​ 其实,上面代码的逻辑并不是正确的,考虑以下函数

1
fn process<Param, Result>(p: Param) -> Result

​ 在这里的上下文当中,Result已经不是我们期望的类型,而是一个泛型(Generic)。要解决这种情况,可以事先检查方法上下文中的泛型标识符(更加好的方法是,收集一套完整的类型上下文,但这样会让代码不必要地复杂),在这里不作过多赘述。

捕捉必要信息

​ 在遍历抽象语法树的过程中,如果要收集特定的信息,有时不得不借助已有的上下文。所幸visit的过程是以深度搜索的形式进行的,只需要稍作调整就能追踪上下文信息。

​ 考虑之前的FnVisitor,如果我们希望能够在分析函数中的表达式时获取表达式所在的函数信息,可以作以下更改:

1
2
3
4
5
6
7
8
9
10
11
12
struct FnVisitor { 
scope:
Option<std::result::Result<ImplItemMethod,ItemFn>>
}

impl FnVisitor {
fn new() -> FnVisitor {
FnVisitor {
scope: None
}
}
}

​ 在syn的抽象语法树结构里,ImplItemMethodItemFn是唯二(噗)两个具有函数体(body)的函数对象,分别代表impl块中的方法实现和impl外的函数实现。因此。通过一个Option + Result,就能恰当表示当前表达式所在的函数。

​ 最后,不要忘记scope变量是我们自己定义的,其值也需要在实现中手动维护,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn visit_impl_item_method(&mut self, i: &'_ ImplItemMethod) {
self.scope = Some(Ok((*i).clone())); // 在遍历函数体的节点之前,将函数设为当前的scope
syn::visit::visit_impl_item_method(self, i);
if self.scope.is_some() {
self.scope = None; // 函数体节点遍历完成,将scope置空
}
}

fn visit_item_fn(&mut self, i: &'_ ItemFn) { // 同上
self.scope = Some(Err((*i).clone()));
syn::visit::visit_item_fn(self, i);
if self.scope.is_some() {
self.scope = None;
}
}

​ 通过类似的实现方式,可以在有需要时保存更多的相关信息,比如类型推断(Type Inference)的上下文以及其它库(crates)和文件的导入信息等,以此完善解析器的功能,甚至构造出完整的Rust类型检查器(Type Checker),静态分析器等。

思考题(为什么会有这种东西(#°Д°)):上述用于记录当前函数的实现有一个显而易见的问题,请指出问题的来源,并提出一种解决方案。

实现分析器

咕咕咕咕咕咕咕