For the complete documentation index, see llms.txt. This page is also available as Markdown.

编译器视角下的代码结构

如果把代码可视化理解成“把代码画成图”,很容易低估前置工作的难度。工具不能直接理解一段源码文本。它必须先知道哪些字符组成关键字,哪些字符组成变量名,哪些代码是一个方法,哪些调用属于某个对象,哪些类型之间存在继承或实现关系。

编译器提供了一条成熟路径:把文本逐步转换成结构化表示。代码可视化并不需要完整实现一个编译器,但它大量复用编译器前端和程序分析中的思想。本章先建立这个视角,后面再分别展开 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