Modernization
summary for code analysis and auto-refactor。《代码分析与自动化重构》 - 如何自己动手设计源码解析、构建代码的代码模型、可视化代码、以及如何进行自动化的重构和守护。
Install / Use
/learn @phodal/ModernizationREADME
《代码分析与自动化重构》
PS:根据过去编写 Modernizing 相关的开源工具里,编写的《代码分析与自动化重构》指南。
遗留系统的现代化演进是一门艺术。在日常的软件开发里,我们经常会遇到一系列的问题:
- 如何解决人类智商不够的问题?模式、原则和工具
- 谁应该去解决代码的问题?代码
- ……
应对于这些问题,其中的一个解决方案就是:自动化的工具,有些人喜欢称之为器。支撑这些工具的便是一系列的原则与模式,将它们融入到工具之中。另外一个解决人成长的方案就是:元元(meta-meta),这是另外一个故事。应对于日常编码而言,它便是代码的分析,以及后续的自动化重构。
代码分析与自动化重构流程:
简介
Why 开源 + 遗留系统现代化工具
遗留系统是常态。在大多数公司里,我们所遇到的系统里多数是是遗留系统,来到一个新项目时,可能就需要对他们快速的分析,以提供洞见 —— 写 PPT 汇报。所以,在过去的几年里,我们也沉淀了一系列的遗留系统分析和重构的工具,比如新哥的 Tequila、正在开源的架构分析和守护工具 ArchGuard 等等。除此,在有些重构项目里,还要编写定制的工具来进行分析,诸如于先前我的同事覃宇和俊斌等所写的「移动应用遗留系统重构」 系列。
技术热情发电。对多数人而言,我们面临的一个重要挑战则是:拿自己的业余时间来完善工具。
既然要用自己的时间来开发,还和项目没有关系,这种用爱发电的事情,用开源的方式最合适了。
我们需要怎样的工具?
从对于使用工具的结果来看,我们需要这个现代化工具是:
- 可视化驱动。快速生成项目的分析结果,并展示出来给开发人员了解现状,还有编写 PPT。
- 必要的交互性。用于在重构的过程中,寻找合适的切入点。
- 定制化开发。
- 特定坏味道。不同的开发团队会有不同的坏味道,有些坏味道是无法由 SonarQube 这样的工具识别的。
- 自动化重构。基于已知的坏味道,对应的代码位置信息,对代码进行自动化重构。
- 适当的语法精准度。更高的语法精准度,意味着更高的开发成本,需要有针对地平衡它们。
- 多平台。我们用的是 macOS,而多数时候,客户使用的是 Windows。
如何开发这样的工具?
这里定义的遗留系统现代化工具包含了这么几部分:语法分析、结果及可视化、自动化重构、架构守护。
语法分析
对代码进行语法分析,生成特定的语言的数据结构。常用的工具有:Antlr、Ctags、Tree-sitter、Doxygen、CodeQuery 等。一个大致的对比(拍脑袋订的)如下表所示:
| 工具 | 精确度量化 | 开发难度 | 跨语言学习成本 | 添加新语言成本 | 可自动化重构 | |----|----|----|----|----|----| | 语言编译器 | 完美 | 低 | 高 | - | Yes | | Antlr | 极高 | 中 | 中 | 中 | Yes | | Ctags | 中 | 低 | 低 | 高 | Yes(成本高) | | Tree-sitter | 高 | 高 | 中 | 高 | Yes(成本高,S) | | Doxygen | 中 | 低 | 低 | 高 | No | | CodeQuery | 极高 | 中 | 中 | 高 | Yes(成本高)|
结果及可视化
通常来说,我们会出于以下的一些情况,来对遗留系统进行可视化:
- 数值化。如针对于特定的 smell 进行自动化重构,类似于 SonarQube,常见的模式和原则源自于《重构》一书。在 Coca 里,还引入了在一些论文里看到了测试的 bad smell,诸如于没有断言的测试等。
- 可视化依赖。如针对于代码中的类、包等的依赖情况进行可视化,主要用于分析分层架构等。常用的工具有:PlantUML、Graphviz、D3.js、Echarts 等。
- **代码属性可视化。**如针对于文件的修改频率、大小等属性进行可视化,可以获取诸如于单位时间内的文件变化频率。一个文件经常修改,还大量被引用,那说明它是一个不稳定的类、文件,除了业务变化,最有可能就是设计不合理。
- 其它。
自动化重构
这一步是可选的,它取决于我们的场景。通常来说,编写这样的功能主要弥补是现代化的 IDE 无法完成的工作,诸如于:
- 多代码库间的未使用类删除。
- 多代码库间的聚类。
- 针对于 CSS 颜色的重构。
架构守护
编写架构的守护规则,以对于系统的架构进行守护,用的工具有:ArchUnit、ArchGuard 等。在参考了 ArchUnit 的语法之后,我们也设计了一个多语言的架构守护工具:Guarding。
遗留系统现代化工具集
在 Modernizing 里,我们集合了先前开发的一系列工具。并创建了:awesome-modernization 用于对其它的一系列相关的工具进行收集。
在 Modernizing 里,针对于单个编程语言的工具有:
- 针对于 Java 语言的系统重构、系统迁移和系统分析的工具:Coca,Go 语言,GitHub stars:691。Coca 是一个“全功能”的重构工具,基于 Antlr 进行语法分析的,除了常规的可视化、调用分析,还可以进行自动化重构。Coca 一名的由来是:对标新哥写的 Tequila —— 龙舌兰酒 vs 快乐水。
- 针对于 CSS/LESS/CSS 的分析和自动化重构工具:Lemonj,TypeScript 语言,GitHub stars:128。当时设计的主要目的是:用来对 CSS 中的颜色进行提取,基于 Antlr 的语法树分析,可以用于进行自动化的重构。
- 针对于 MySQL 代码进行自动化分析,并从中构建中 UML,并生成其关系的:SQLing,Go 语言,使用 PingCap 的 SQL 解析器解析。当然了,还有一个初始化的针对于 PL/SQL 的版本:pling。
- 适用于 Ant 转 Maven 的半自动化工具:Merry,Go 语言 + Antlr。
- 前端规范化改造工具:Clij,用于一键添加 eslint、husky、lint-staged 等,TypeScript 语言。
针对于多语言的工具,我们有:
- 基于 Antlr 的多语言的语言模型分析工具:Chapi,Kotlin 语言。其设计的初衷是用于生成 Coca 相同的数据结构,以接入更多的可视化工具。在语法分析上,采用的是 Antlr 进行分析。
- 基于 Doxygen 的多语言分析和可视化工具:Go mod 版本的新哥的 Tequila。其中,还有一系列的迷之代码,需要重构掉。
- 基于 Ctags 的多语言模型分析和可视化工具:Modeling,Rust 语言。分析源码,并生成基于模型的可视化依赖。
- 基于 Tree-sitter 的多语言架构守护工具:Guarding,Rust 语言。通过自制的 DSL,来对系统架构进行守护。
除此,还有一个在 Inherd 开源小组下开源的:Coco,它主要是通过代码的物理属性:修改频率 + 目录 + 行数来分析系统的工具。以及现在紧锣密鼓开源中的 ArchGuard。
我们使用一系列不同的语言和工具来开发这些软件,因为不同的场景之下,都会有不同的选择。
自动化重构:代码分析
代码分析是我们编写自动化重构、架构守护等一系列工具的第一步。而代码分析的方式有多种不同的形态,最常见的是基于源码以及基于编译后的字节码(常见于 Java 语言)的静态程序分析。
通常来说,根据我们的目标获取的信息是不同的,如:类/结构体、成员、函数(含参数、返回值、注解)、引用(import)、表达式等。因此,所选的工具也是不同的:
| 目标 | 语法信息级别 | 可选 工具 | |----|----|----| | HTTP API | @注解、参数、类、方法 | 语法分析器(语言自带、三方、Antlr) | | 领域模型 | 类/结构体、成员等 | 根据不同精度,可以考虑 Ctags、Tree-sitter等 | | 包、类依赖关系 | 引用、函数调用等。 | Doxygen、 语法分析器等 | | 调用链 | 全部信息 | 语法分析器(语言自带、三方、Antlr) |
根据我们的不同需求,我们还需要记录语法的位置信息。比如,同样是 HTTP API 的情况下,我们想获取:
- API URI 列表。只需要解析注解即可。
- API 的输入和输出参数。注解 + 解析函数签名。
- API 输入到数据库。注解 + 解析函数签名 + 调用链。
因此,是不是使用语言自带的语法分析器,生成一个完整的模型就行了,如 Java 使用 Javaparser。事情并不是这么简单,如今是微服务时代,每个服务都可能使用不同语言,一个二三十人的研发团队,可能使用 7~8 种语言 —— 为每个服务挑选合适的语言,老系统 C#、新系统 Java、大数据 Scala、AI 用 Python 等。除此,为某个语言写一个成本也是颇高的,并且用处可能还不大。
所以 ,在不断平衡之间,我们有了一系列的工具选型。
编译器前端
编译器粗略分为词法分析,语法分析,类型检查,中间代码生成,代码优化,目标代码生成,目标代码优化。
基于语法分析器(parser)
从实现的层面来看,使用官方的 parser 是最准确的 —— 前提是它提供了便利的接口,像 Java 语言好像就没有这样的接口。
- 官方支持。如 Coca 早期在解析 Golang 时,使用的是 Go 的 parser 包。
- 三方。在 SQLing 中,我们使用的 TiDB 的 parser,它宣称与 MySQL 完全兼容,并尽可能兼容 MySQL 的语法。
使用这一类 parser 比较麻烦的是在于跨语言的支持,每实现一个新的语言,就需要实现一套,不能复用。
自制 parser
为了实现更好的跨平台,以及更好玩,选用一个合适的解析器生成器就更“科学” 了。在这一方面,除了传统的 Flex 和 Bison,Antlr 也是一个不错的选择 —— 多语言支持:JavaScript、Golang、Java、Rust 等。
Antlr 社区维护了一个语法库:https://github.com/antlr/grammars-v4/,内置了几十种编程语言的 Antlr 语法文件。虽然,部份语法可能不太准确,需要我们手动进行修改,但是依旧可以大大减少我们的编写成本 —— 除了学习 Antlr 是个成本。Antlr 之类工具的迷人之处在于:你可以重温一下《编译原理》,又或者是《计算机程序的构造和解释(SICP)》,毕竟它是编译器的前端部分。你再掌握一下 LLVM 的 API,就可以开发个语言了。它的挑战之处在于,你需要知道语言的各类语法细节,所以也是一个不错的学习新语言语法的机会。
不过,诸如 Java、C++ 等支持在编译时进行代码生成的语言,也会遇到一系列的挫折。诸如于:
- 引用推断。最难受的
junit.*需要做一些推断 - 生成工具推断。如 lombok 等
所以,我们需要通过编译过程中的中间表示,来做一些额外的处理。
基于中间表示(IR)
IR-Intermediate Representation(中间表示)是程序编译过程中,源代码与目标代码之间翻译的中介。
为了提升语法分析的精准度,就需要应对编译其的代码生成,于是,就需要分析 IR。如:Java 里的 ASM。能对 .class 文件进行分析。只是,IR 处理了一些信息,所以如 class 文件里有些内容(如 annotation)好像并不会被记录行号信息,详见:LineNumberTable Attribute。

Java、Android 在编译过程中对于 Annotation 的操作,又或者是在编译后的骚优化,也是 666。
不过,它能完成大部分我们所需要的工作。
编辑器语法树
编辑器在做语法高亮的时候,也在做类似的事情。正好,我先前在某 spike 过编辑器 / IDE 的架构和实现。
- Atom/VSCode。主要由 JSON/PList 格式的 TMLanguage(源自 TextMate) + 正则表达式实现,即 VSCode TextMate 和 Oniguruma 共同构成了 VSCode 的一部分语法高亮功能。吐槽一句,非常难以维护。
- Eclipse。需要手写解析器,FAQ How do I write an editor for my own language?。
- Intellij IDEA。可以通过 BNF 来添加相应的功能:Custom Language Support Tutorial。
- Vim。由自带的 Vim 脚本 + 正则表达式(类似)来实现,示例:Rust.vim
- Emacs。由 Emacs Lisp 语言 + 正则表达式(类似)来实现,示例:rust-mode
只是呢,上述的工具,在离开了编辑器之后,这个 API 嘛,就有些难用了。于是,有一些独立的工具出现了。
基于语言服务器(LSP)
虽然,我还没有尝试过使用 LSP 来实现语法分析,但是我尝试构建过一个语言及其 LSP。因此,从理论来说,LSP 也能达成此目的。并且与 Antlr 类似,Microsoft 也维护了一个 LSP 的目录:https://microsoft.github.io/language-server-protocol/implementors/servers/。
麻烦的是,不同语言的 LSP 可能由不同的语言来实现,在系统的集成上会比较困难。其所需要的语言运行环境比较多,比如 Java 的就需要一个 JDK/SDK,在编写分析工具时,自动化测试环境搭建起来也比较麻烦。
Ctags:有限的解析
Ctags 可以快速实现对类、成员的解析,所以它经常被用在 Vim 的语法高亮上。只是呢,使用 Ctags 难以实现支持:某个函数调用了哪些函数、哪些函数被某个函数调用。从流程上,先用 ctags 生成 tags 文件,然后解析这个 tags 文件即可。如下是一个 tags 文件(部分):
MethodInfo src/coco_struct.rs /^pub struct MethodInfo {$/;\" struct line:21 language:Rust
name src/coco_struct.rs /^ pub name: String,$/;\" field line:22 language:Rust struct:MethodInf
然后,再写几个正则表达式 match 一下:
Regex::new(r"(?x)/\^([\s]*)
([A-Za-z0-9_.]+)
(,(\s|\t)*([A-Za-z0-9_.]+))*(\s|\t)*
(?P<datatype>[A-Za-z0-9_.<>\[\]]+)").unwrap();
因此,在不考虑正则表达式难写和代码精准度的情况下,使用 Ctags 还会存在一些小问题:
- 版本冲突,如 macOS 环境自带了一个 ctags,需要 override,或者自定义路径。
- 下载 ctags。特别是如果客户是在内网环境时,又会比较麻烦。
所以,Tree-sitter 成了一个更好的选择:平衡。
Tree-sitter
Tree-sitter 是一个解析器生成工具和增量解析库。 它可以为源文件构建具体的语法树,并在编辑源文件时有效地更新语法树。这个工具最初是为 Atom 编辑器设计的。Tree-sitter 内置了一个 S 表达式,可以快速构建出我们想要的模型。如下是一个 C# 代码:
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
[ApiController]
public class SharpingClassVisitor {
}
对应的 S 表达式如下:
(using_directive
(qualified_name) @import-name)
(class_declaration
(attribute_list (attribute name: (identifier) @annotation.name))?
name: (identifier) @class-name
)
我们在 Guarding 中使用了 Tree-sitter 来实现,示例:[Guarding Ident](https://github.com/modernizing/guarding/tree/master/guarding_ident/src/identify),与 Ctags 相比,没有这个环境依赖,会比较清爽。
其在线 Playground:https://tree-sitter.github.io/tree-sitter/playground 。
其它生成工具
除了上述的几类,还有一些可选的工具。
文档生成器:Doxygen
Doxygen 是一个适用于 C++、C、Java、Objective-C、Python、IDL、Fortran、VHDL、PHP、C# 和 D 语言的文档生成器。为了生成代码的文档,它需要能支持对于代码进行语法分析。所以,它也内置了有限的语法分析功能。
在 Tequila 中,是通过分析 Doxygen 生成的文档结果,从而构建出内部的依赖关系。如下是一个 Doxygen 生成的 Graphviz 文件:
digraph "Domain::AggregateRootB"
{
// LATEX_PDF_SIZE
edge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10"];
node [fontname="Helvetica",fontsize="10",shape=record];
Node1 [label="Domain::AggregateRootB",height=0.2,width=0.4,color="black", fillcolor="grey75", style="filled", fontcolor="black",tooltip=" "];
Node2 -> Node1 [dir="back",color="midnightblue",fontsize="10",style="solid",fontname="Helvetica"];
Node2 [label="Domain::AggregateRoot",height=0.2,width=0.4,color="black", fillcolor="white", style="filled",URL="$class_domain_1_1_aggregate_root.html",tooltip=" "];
Node3 -> Node2 [dir="back",color="midnightblue",fontsize="10",style="solid",fontname="Helvetica"];
Node3 [label="Domain::Entity",height=0.2,width=0
Security Score
Audited on Mar 6, 2026
