编译器视角下的代码结构
如果把代码可视化理解成“把代码画成图”,很容易低估前置工作的难度。工具不能直接理解一段源码文本。它必须先知道哪些字符组成关键字,哪些字符组成变量名,哪些代码是一个方法,哪些调用属于某个对象,哪些类型之间存在继承或实现关系。
编译器提供了一条成熟路径:把文本逐步转换成结构化表示。代码可视化并不需要完整实现一个编译器,但它大量复用编译器前端和程序分析中的思想。本章先建立这个视角,后面再分别展开 AST、符号表、IR、CFG 和 DFG。
源码不是稳定的分析对象
人读代码时可以凭经验跳过空格、注释和格式差异,也可以根据上下文推断变量含义。但工具如果只把源码当普通字符串处理,很快会遇到问题。
例如下面两行都出现了 UserService:
private UserService userService;
String text = "UserService";如果只做字符串搜索,它们看起来都命中了 UserService。但从代码理解角度看,第一处是类型引用,第二处只是字符串字面量。再比如一个方法名可能出现在注释、文档、测试名称、日志文本和真实调用表达式中,只有后者才应该进入调用图。
所以,代码可视化的第一步不是画图,而是把源码转换成工具能稳定识别的结构化数据。
编译器的基本流程
一个典型编译器会经历多个阶段:
源码文本
-> 词法分析
-> 语法分析
-> 语义分析
-> 中间表示
-> 优化
-> 目标代码不同语言和工具链的实现细节不同,但这条链路反映了一个共同思路:越往后,表示越脱离原始文本,也越接近程序的语义和行为。
对代码可视化最有用的是前四个阶段:
词法分析:识别 Token,例如关键字、标识符、字面量、操作符。
语法分析:把 Token 组织成语法树或抽象语法树。
语义分析:建立符号、作用域、类型、引用和重载解析。
中间表示:把程序转换成更适合控制流、数据流和优化分析的形式。
目标代码生成和机器级优化对本书不是重点。我们关心的是如何把源码变成可分析、可查询、可视化的事实。
词法分析:把字符变成 Token
词法分析解决的是“这段文本由哪些基本语法单元组成”。例如下面的 Java 代码:
词法分析后可以得到类似这样的 Token 序列:
Token 本身还不理解程序含义,但它已经比原始字符串更稳定。工具可以区分关键字、变量名和操作符,也可以忽略空格、换行和注释等格式差异。
在代码可视化中,Token 层很少直接成为最终图形,但它是后续语法树的基础。正则表达式可视化、语法高亮、简单代码扫描也会使用类似思想。
语法分析:把 Token 变成树
语法分析根据语言规则,把 Token 组织成结构。对于上面的表达式,语法分析会识别出变量声明、赋值表达式、乘法表达式等节点。
抽象语法树(AST)保留的是对代码理解有用的结构。例如一个方法声明节点会包含方法名、参数、返回类型和方法体;一个调用表达式节点会包含调用目标、参数列表和源码位置。
AST 让工具可以回答很多结构问题:
一个文件里有哪些类和方法。
一个方法体里有哪些语句。
哪些地方出现了方法调用表达式。
哪些字段被声明为某种类型。
哪些注解或装饰器标记了入口。
如果没有 AST,工具只能在文本里猜。如果有 AST,工具就能基于语言结构进行可靠遍历。
语义分析:把结构变成意义
AST 只告诉我们代码“长什么样”,还不能完全回答“名字指向谁”。例如:
AST 可以识别这是一次方法调用,方法名是 create,接收者是 service。但它不知道 service 具体是什么类型,也不知道 create 最终会解析到哪个类的方法。要回答这些问题,需要语义分析。
语义分析会建立:
符号表:记录类、方法、变量等有名字的实体。
作用域:判断某个名字在当前位置指向哪个定义。
类型关系:判断变量、字段和表达式的类型。
引用关系:把使用位置连接到定义位置。
这些信息是 IDE 跳转、引用查找、重命名重构、调用图构建和影响面分析的基础。
中间表示:更适合分析程序行为
AST 更接近源码结构,但很多分析需要更接近执行行为。例如一个 if 语句包含两个分支,循环会让执行路径回到前面,异常会改变正常控制路径。直接在 AST 上分析这些路径并不方便。
中间表示(IR)把程序转换成更规整的形式。基于 IR,工具可以构建控制流图(CFG)和数据流图(DFG),进一步分析:
某段代码是否可达。
一个变量的值从哪里来。
用户输入是否流向危险 API。
修改一个条件会影响哪些路径。
哪些路径被测试覆盖。
对代码可视化来说,中间表示的价值在于把语法结构转化为行为关系。后续的安全分析、测试选择、影响面分析和性能路径解释,都可能依赖这些表示。
代码可视化需要哪些编译器产物
不同可视化场景需要的产物不同:
代码大纲
AST、声明节点、源码位置
类图和继承图
符号表、类型关系、继承和实现关系
调用图
AST 调用表达式、类型解析、符号引用
影响面分析
调用图、依赖图、变更实体映射
数据流安全分析
CFG、DFG、污点源和危险 Sink
自动重构
AST、符号表、引用关系、类型检查
AI Agent 上下文
文件结构、符号、调用关系、相关测试、架构规则
这张表说明一个关键点:代码可视化不是单一技术,而是多个分析产物的组合。
静态语言与动态语言的差异
Java、C#、Go、Rust 这类静态类型语言通常更容易获得稳定的类型和调用关系。Python、JavaScript、Ruby 这类动态语言更灵活,但静态分析难度更高。
动态语言中,函数、对象和模块关系可能在运行时改变。框架也可能通过约定、装饰器、依赖注入或反射建立关系。此时,工具需要结合更多信号:运行时 Trace、测试执行、类型注解、配置文件和框架规则。
这并不意味着动态语言不能做代码可视化,而是要更谨慎地标注不确定性。比如调用图中的某些边是确定解析出来的,某些边是根据命名约定推断的,某些边需要运行时数据确认。
和 AI 时代的关系
AI Agent 读代码时,也会遇到同样的问题。它可以读取文本,但如果没有结构化索引,就很容易遗漏相关文件、误判调用关系或忽略架构边界。
编译器视角提供的是一种上下文工程能力:把仓库解析成符号、关系和路径,让 AI 在修改代码前可以查询事实,而不是盲目读取大量文件。
例如 Agent 可以先查询:
目标方法在哪里定义。
谁调用了这个方法。
它调用了哪些下游方法。
相关测试有哪些。
它所在模块有哪些依赖约束。
这些查询都建立在源码结构化和语义分析之上。
小结
编译器视角告诉我们:代码可视化的起点不是图,而是结构化。词法分析让工具识别基本语法单元,语法分析让工具获得 AST,语义分析让工具理解符号和类型,中间表示让工具进一步分析控制流和数据流。
后续章节会依次展开这些基础能力。理解它们之后,代码图谱、影响面分析、AI Agent 上下文和 Review 证据层都会变得更容易解释。
Last updated