OpenRewrite 学习笔记(二):无损语义树 LST
在上一篇文章中我们提到,LST 是 OpenRewrite 实现精准、可控代码修改的关键支柱。本篇将更深入地探讨 OpenRewrite 在代码解析过程中,究竟是如何保留代码原有的精确语义结构的。
什么是 LST
LST 是 Lossless Semantic Trees,无损语义树的缩写。
先上总结,说说我对这三个单词的理解:
- Lossless 无损:代码解析和结构化过程中不丢失源代码中的任何信息(包括空格、注释、格式等),最终的属性结构中可以保留代码的原始细节。
- Semantic(语义):该树不仅仅是语法分析的产物,还能体现代码片段之间的语义关系。
- Trees(树):以树形结构组织代码元素,使得对代码片段的遍历、查询和变换更为直观且层次清晰。
说起语义树,就不得不提一下 AST。
抽象语法树 AST
AST(Abstract Sytax Tree,抽象语法树),又常被称为语法树(Sytax Tree),是以树状的形式来表示编程语言的语法结构。AST 是编译器中广泛使用的的数据结构,用于表示程序代码的结构。
像下面的 AST 图是这段代码(欧几里得算法)的抽象语法树结构展示,树上的每个节点都表示源码中的一种结构。
while b ≠ 0:
if a > b:
a := a - b
else:
b := b - a
return a
无损语义树 LST
OpenRewrite 的 LST(无损语义树)具备一系列独特特性,使得在跨存储库的场景中依然能实现精确的代码搜索和转换:
- 类型信息保留:每个 LST 都包含详尽的类型信息。举例来说,在源码中你也许只看得到字段引用
myField
,但在 OpenRewrite 的 LST 中,该字段的类型信息会被一并保留。即使该类型定义不在当前文件,甚至不在同一个项目里,这些类型属性依然能够被检索到。 - 无损格式保留:LST 会完整保留代码中的空格、缩进和换行等格式信息,使得在打印树结构时能够还原源代码格式。同时,当对代码进行插入或重构时,新增的代码片段会自动适应周边代码的本地编码风格,从而保持整体风格一致。
类型信息对于精准匹配模式至关重要。例如,当你需要寻找特定的 SLF4J 日志调用语句时,如果没有类型信息,你无法判断 logger
变量究竟是 SLF4J 的实例,还是其他日志框架(如 Logback)的实例。类型信息的存在让这种区分和匹配成为可能。
logger.info("Hi");
AST vs LST
通过 LST 特性介绍和示例来看,相比 AST,LST 有很大的优势:
- 完整保留代码信息:传统的 AST 通常关注程序的语法与语义结构,会在解析过程中舍弃格式、注释和特定的源代码布局等信息,导致从 AST 回写(再生成源代码)时可能无法还原源码的外观布局。而 LST 就不会有这种问题。
- 更精细的变更控制:因为 LST 保留了所有细节,当对代码进行重构或自动化变更时,你可以在不破坏原有风格的前提下精确地修改局部结构。对代码的微调变得更容易,合并冲突更少,也会大大降低代码 review 的难度。
- 更丰富的语义信息:虽然传统 AST 已经包含了大量语法和基本语义信息,但 LST 在设计上更加强调语义保真度,并在树中以更细粒度的方式呈现代码之间的关系。这为后续的配方(Recipe)提供更准确的信息基础,从而实现更智能、更上下文化的代码变换。
LST 示例
这里提供两个示例展示 Java 语言的 LST 结构。在 OpenRewrite 代码中,所有与 Java 语言 LST 类型都实现了接口 J
,而 J
的父接口是 Tree
。所有语言、数据类型的 LST 接口是继承了 Tree
,因此从代码层面可以理解为 Tree
就是 LST 概念的实现。
示例一
这是官方文档中的示例,一段包含了类、字段和方法定义的 Java 类的无损语义树。树的根是 J.CompilationUnit
类型的 LST,每个节点也都是 J.XXX
类型的 LST。
示例二
接着定义一个稍微复杂的 Java 类,在上一个的基础上加上构造方法、方法调用、字段访问等,以此来展示更多的 JavaLST 类型。这回,通过 OpenRewrite 提供的方法 TreeVisitingPrinter.printTree(tree)
打印出该类的 LST。
package com.atbug.demo;
class FooBar {
private String greeting = "world";
public FooBar(String greeting) {this.greeting = greeting;}
public String hello() { return this.greeting; }
public String greeting() { return "Hello, " + this.hello() + "!"; }
}
在每个 LST 节点中都包含了详细的信息,比如代码块、变量类型、方法声明、方法调用以及方法返回类型等等。
总结
LST 是 OpenRewrite 的核心,为配方实现精细粒度控制奠定了基础。当我们编写配方时,只有深入理解 LST 的结构,并为特定的代码路径提供充分的条件判断,才能实现精准的自动化调整。
借助 TreeVisitingPrinter.printTree(Tree)
随时查看语义结构树,我们可以更直观地掌握代码结构,从而编写出更健壮、更可控的配方逻辑。