构建 Rust 分析器的正确操作
- 2019-08-09
- Phosphorus15
最近,因为任务需要,要找一个可以方便地解析Rust
源代码的库。本来以为找到一个趁手的解析器应该是非常简单的,谷歌半天找到了rust-lang
团队官方构建的解析库syntex_syntax。结果刚刚导入cargo项目就发现这个库两年前就已经停止维护了。
虽然不报什么期待,我还是试着用了一下syntex_syntax
来解析源文件。解析第一个小crate
倒还挺顺利,但到了第二个crate
直接就栈溢出了,再多试几个样品,发现这个库连dyn
关键字都识别不出来,于是乎。。自然是不能用了。
考虑到在写过程宏时有一个同样有解析功能的辅助库syn
。我的第一想法便是能不能将syn
独立使用(而不是写过程宏来处理syn
的输出结果),但在查询许久之后无果,最后就有了在stackoverflow
上问的这个蠢问题。
导入syn库
总之,库算是找到了,但syn
的设计初衷毕竟不是直接用来做源码解析,需要稍作调整才能正常使用。一开始,我写的Cargo.toml
像下面这样:
1 | [dependencies.syn] |
这里把default-features
给关闭,是为了不让syn
把过程宏一类默认开启的无用功能导入。后面的features
属性则是手动指定了导入的功能。等cargo配置完成以后,可以用一段代码来测试一下解析功能
1 | fn main() { |
然后。。讲道理,编译运行就可以看到文件对应的抽象语法树了,但rustc
好像不这么想:
1 | error[E0277]: `syn::file::File` doesn't implement `std::fmt::Debug` |
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 | [dependencies.syn] |
配置完成以后,我们上面的代码就可以正常运行了。
遍历抽象语法树
对语法树进行分析的第一步就是要遍历整棵抽象语法树,syn
库的visit
功能为我们提供了快速访问语法树的便利,只需要实现一个syn::visit::Visit
的特性,并重载其中的访问函数,我们就能用这个结构体对语法树进行访问:
1 | struct FnVisitor; |
比如,如果说想实现一个visitor
来找出并打印所有析构用的let
语句,只需要重载Visit
特性中的visit_expr
函数,形式如下
1 | fn visit_expr(&mut self, i: &'_ Expr) { |
这个实现中有以下两点需要注意:
- 因为
Visitor
的实现是递归式的,因此在函数调用的最后我们一定要调用visit
提供的相应静态函数(比如这里的visit_expr
),否则语法树更深层的遍历会在此终止。 - 这里用到了一个经常和
syn
一起使用的quote
库,用于将抽象语法树格式化成源码字符串,使用quote
只需要在Cargo.toml
中直接导入
1 | [dependencies] |
写出上述实现以后,便可以直接使用实现的Visitor
来遍历代码,只需要使用任意syn::visit
中的静态访问函数,并将Visitor
作为参数即可,比如,接第一节的例子,可以这么写:
1 | let ast = syn::parse_file(&content).unwrap(); |
运行代码,可以看到访问器达到了预期的输出:
1 | let Some ( ref mut fut ) = self . fut_ex |
其它的访问器函数,虽然访问的语法树对象不同,但实际情况也大同小异。
syn
的解析器,由于在实现上仅仅起到了简单的解析作用,因此在遇到诸如函数调用和类型声明时,并不能为我们定位到有效的上下文,因此在进行复杂处理时,需要多费一些功夫。下面的例子中,我们重载了一个访问函数,用于输出所有返回值类型为Result
的方法(不关心泛型参数)
1 | fn visit_method_sig(&mut self, i: &'_ MethodSig) { |
乍一看,你可能会被冗长的逻辑吓到。但实际上,这里的代码只进行了三步主要操作,其中有两步是模式匹配:
捕捉方法签名,并获取其返回值(output)
将返回值匹配到实际类型,确认其不是默认类型(即需要编译器推导返回值)
将实际类型匹配到类型路径(即复合类型对应的结构),遍历类型路径的最浅层,寻找是否有
Result
标识符上面的代码在运行结束后,同样会给出令人满意的结果。
1 | fn read ( & mut self , buf : & mut [ u8 ] ) -> io :: Result < usize > |
其实,上面代码的逻辑并不是正确的,考虑以下函数
1 | fn process<Param, Result>(p: Param) -> Result |
在这里的上下文当中,Result
已经不是我们期望的类型,而是一个泛型(Generic)。要解决这种情况,可以事先检查方法上下文中的泛型标识符(更加好的方法是,收集一套完整的类型上下文,但这样会让代码不必要地复杂),在这里不作过多赘述。
捕捉必要信息
在遍历抽象语法树的过程中,如果要收集特定的信息,有时不得不借助已有的上下文。所幸visit
的过程是以深度搜索的形式进行的,只需要稍作调整就能追踪上下文信息。
考虑之前的FnVisitor
,如果我们希望能够在分析函数中的表达式时获取表达式所在的函数信息,可以作以下更改:
1 | struct FnVisitor { |
在syn
的抽象语法树结构里,ImplItemMethod
和ItemFn
是唯二(噗)两个具有函数体(body)的函数对象,分别代表impl
块中的方法实现和impl
外的函数实现。因此。通过一个Option + Result
,就能恰当表示当前表达式所在的函数。
最后,不要忘记scope
变量是我们自己定义的,其值也需要在实现中手动维护,示例代码如下
1 | fn visit_impl_item_method(&mut self, i: &'_ ImplItemMethod) { |
通过类似的实现方式,可以在有需要时保存更多的相关信息,比如类型推断(Type Inference)的上下文以及其它库(crates)和文件的导入信息等,以此完善解析器的功能,甚至构造出完整的Rust
类型检查器(Type Checker),静态分析器等。
思考题(为什么会有这种东西(#°Д°)):上述用于记录当前函数的实现有一个显而易见的问题,请指出问题的来源,并提出一种解决方案。
实现分析器
咕咕咕咕咕咕咕