codegraph:用代码图谱让遗留项目依赖关系可视化,辅助架构治理与重构
接手一个代码量不算大的遗留项目有时候比从零写一个新系统更费神。模块之间的引用关系像一团毛线你改了一个公共函数心里根本没底它会波及到哪里。我之前就经历过这么一回花了两个星期梳理调用链画出来的图还是残缺的。后来我花了一个周末写了 codegraph——一个扫描代码库、自动生成依赖关系图谱的命令行工具也是这篇文章的主角。这篇文章把它的设计思路、安装使用、参数细节和踩坑经验一次性讲清楚也顺带聊聊代码图谱怎么真正用进项目治理里。无论你是刚接手老项目想快速建地图的开发者还是负责代码架构的技术负责人或者想在团队里搭一套可视化分析流程都可以往下看看。1. codegraph是什么要解决的核心问题1.1 代码维护的三个典型痛点先说清楚我为什么要写这么个工具。日常开发里代码本身通常是好读的函数写得不至于太反人类真正难的是代码与代码之间的关系。你打开一个文件能看清这个文件内部做了什么但你看不见这个文件在这堆代码里处于什么位置——它被谁依赖它又强依赖谁它是不是整个系统的接线枢纽。这些信息不会写在代码注释里也不会出现在IDE的代码补全里。我归纳下来代码维护有三个绕不开的痛点。第一个是知识只存在于人脑子里。老员工的脑子就是活架构图他清楚这个模块和那个模块不能随便动但这份理解从没落到纸面上。一旦这个人转岗或离职新接手的同事只能对着代码库猜运气好猜出七成运气差就走歪路。第二个是依赖关系完全不可见。IDE能帮你从一个调用跳到另一个调用但没人能回答“我改了utils/format.js里的公共函数到底会影响多少个模块”。这种问题靠肉眼扫是扫不出来的因为调用方散落在几十个目录里。第三个是改动影响范围基本靠问。问一圈同事得到的回答大概率是“应该不会吧”。我见过太多线上事故起因就是有人改了一个公共方法根本不知道它被一个边缘模块里的定时任务调用着。这三个痛点本质上是同一件事代码库里缺少一张“地图”。生活里我们拿到一座陌生城市第一件事是打开导航但在代码库里却默认靠记忆和口口相传。codegraph 就是给代码库画地图的工具它把文件、模块、函数之间的连接关系抽取出来变成一张可以缩放、搜索、点击的图谱。1.2 codegraph能做什么不做什么工具定位这件事我觉得必须一开始就说清楚否则期望管理会出问题。codegraph 能做的事大概这些抽取文件级、模块级、函数级的依赖关系输出成图谱数据。生成交互式 HTML 图谱支持拖拽、缩放、搜索、按模块聚焦。导出结构化 JSON方便你自己写脚本做二次统计。定位循环依赖、孤立模块、高耦合枢纽节点。支持增量扫描能接进 CI提交代码时自动跑一遍架构检查。但 codegraph 不是一个万能工具。它不做代码质量评分不抓 lint 规则不算运行时调用次数也替代不了人工代码审查。它做的是让结构透明——把“哪跟哪连着”这件事用一张图讲清楚。至于“这么连着合理吗”“要不要拆”那是看到图之后人要做出的判断。我见过有人想把 codegraph 用成架构评审机器人看到一张图就指望它吐出重构方案这是不现实的。它的价值在于提供了决策依据而不是替你做决策。后面我会用一个实战案例说明这张“地图”到底怎么辅助判断。2. 快速上手安装与第一个图谱2.1 环境要求与安装codegraph 是基于 Node.js 开发的命令行工具装起来很简单。前提是你本机有 Node.js 18 及以上版本老一点的机器装 Node 16 也能跑但解析多语言混合项目时速度会差一些。安装命令就一条npm install -g codegraph/cli装完顺手验证一下版本codegraph --version看到版本号输出就说明装好了。语言支持方面codegraph 目前对 JavaScript、TypeScript、Python、Java、Go 的支持最完整C/C 和 Rust 处于可用但还不够细的状态。它背后用的是 tree-sitter 解析器每种语言对应一个 grammar所以加语言支持本质上就是加载对应的 grammar后续扩展语言生态是比较容易的。这里有个容易忽略的前提codegraph 扫描的是静态代码结构它不需要代码能编译运行也不需要安装项目的依赖包。这意味着你拿到一份没装 node_modules、没跑构建的纯源码也可以直接出图。我接手老项目时最满意的就是这一点——不用先解决依赖安装问题源码目录扔进去就能扫。2.2 第一次graphify安装完先别急着扫大项目我建议你拿一个小点儿的项目练手。比如我测试时用的一个极简 Node.js 项目my-app/ src/ main.js utils/format.js service/order.jsmain.js 调用了 order.js 和 format.jsorder.js 也调用了 format.js。就这种三层结构够看清基本玩法了。进到项目根目录运行codegraph graphify ./src --entry src/main.js --lang js --output report.html说明一下这几个参数graphify是核心动作命令这名字起得挺传神graph 加 ify就是把代码“图化”的过程。./src是扫描路径工具会递归扫描这个目录下所有支持语言的文件。--entry指定入口文件入口节点会在图谱里被特殊标记方便建立阅读起点。--lang指定语言让解析器只处理对应类型的文件。--output指定输出 HTML 报告路径目录不存在会自动创建。运行完目录里会多一个report.html浏览器打开就是一张可交互的图谱。第一次看到图谱出现的时候会有种很直观的感觉原来这个项目里的依赖流向是这样走的。我自己的体会是第一次运行最好在项目规模小的阶段做这样节点少、边少你能快速理解图谱的展示逻辑。如果一上来就扫几万行代码的项目生成的页面上可能有几百上千个节点反而会懵。2.3 输出文件怎么读HTML 图谱页面大致分三个区域。左边是模块树按目录层级把所有扫描到的文件列出来点击任意节点中间画布会自动聚焦中间是主画布节点和边都在这里呈现支持鼠标拖拽平移、滚轮缩放右边是详情面板选中节点后展示文件路径、代码行数、依赖列表等元信息。节点和边有固定的视觉语义第一次使用需要花两分钟熟悉一下方形节点代表文件或模块圆形节点代表函数。带彩色描边的节点是入口也就是--entry指定的那个。边从调用方指向被调用方A 指向 B 表示 A 调用了 B。边的颜色按关系类型区分导入是蓝色、函数调用是橙色、继承是绿色。我刚用的时候觉得这图“有点乱”后来才明白乱的原因不是工具而是真实代码本身就乱。图谱只是忠实地把混乱展示出来了。所以读图的第一步不是追求清晰而是先找几个关键模块确认关系抽取得对不对。比如点开service/order.js看看它的邻居里有没有utils/format.js如果有方向是不是从 order 指向 format这就验证了工具没漏抽。3. 高级参数与Graphify的完整玩法3.1 语言过滤与路径排除小项目可以直接把整个目录丢进去扫但真实项目里总有一堆不该进图的目录——node_modules、test、dist、coverage还有各种生成代码。这些目录如果不排除图谱会被无关节点淹没扫描时间也会成倍增加。我常用的扫描命令是这种形态codegraph graphify . \ --lang typescript \ --exclude node_modules,test,dist,coverage \ --include src--exclude和--include同时出现时--include优先级更高扫描范围会被收敛到 include 指定的路径里exclude 在这个范围内再做剔除。这样两条规则一夹扫描边界就非常清晰了。关于排除目录我有一条很深刻的教训。早期版本我没有默认排除 node_modules某次扫描一个后端仓库结果生成的 JSON 数据直接膨胀到 300 多 MB浏览器打开 HTML 页面卡到没响应。后来把 node_modules 排除掉扫描时间从 12 分钟降到了 25 秒图谱质量也完全提上来了。所以强烈建议第一件事就是把依赖目录排掉。3.2 深度、方向与视图切换图谱数据天然可以无限展开但人的阅读能力有限所以 codegraph 提供了几个控制“视野”的参数这是用好它的关键。--depth控制展开深度默认是 0表示不限制。设成 2就只展示从当前节点出发两层以内的邻居。这个参数在分析“改动影响面”时非常有用比如你改了某个基础模块只需要看它的直接调用方和被调方没必要把所有间接依赖都拉出来。--direction控制边的展示方向有三个值out只看当前模块调用了哪些下游适合梳理模块的对外依赖。in只看谁调用了当前模块适合评估改动影响范围。both双向都显示适合整体浏览。我排查线上问题前的例行动作是先把疑似根因的模块用--direction in --depth 2扫一遍看看这个模块被哪些上层业务依赖能在几分钟内判断问题的爆炸半径。这个用法后来成了团队的标准动作。--view控制图谱的聚合粒度module视图把同目录的文件聚合为模块节点file视图展示文件级function视图展示函数级。日常团队分享用 module 视图做重构细节分析时再用 function 视图不同场景用不同放大倍率。3.3 增量扫描与CI集成大项目全量扫描很费时间所以 codegraph 会在项目根目录维护一个.codegraphcache缓存文件。首次扫描时做全量解析之后每次运行只会重新解析变化过的文件识别逻辑是基于文件 hash 的文件内容没变就直接用缓存里的图谱子图。这个设计对本地反复调试非常友好改两行代码再出图基本秒级完成。但缓存的坑我也踩过改了codegraph配置参数之后如果没清缓存可能导致部分节点还是旧数据。所以规范做法是配置变更后执行一次codegraph clean把缓存清掉再全量扫一遍。CI 里我建议直接禁用缓存保证每次检查的都是真实代码。接入 CI 的收益是实打实的。我们团队的 GitHub Actions 里挂了一个 job每次 PR 都自动跑一次图谱扫描检测到循环依赖就失败让架构问题在合并前暴露name: codegraph-check on: pull_request: jobs: codegraph: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm install -g codegraph/cli - run: codegraph graphify ./src --lang typescript --exclude node_modules --output dist/graph.html --fail-on-cycle--fail-on-cycle是关键参数检测到循环依赖时 CI 退出码会变成非 0Pipeline 直接红掉。这条规则听上去严格真正救回过两三次事故因为循环依赖在本地开发时通常感知不到合并到主干后才会在特定场景下炸。3.4 JSON导出与二次开发HTML 图谱适合人看但做量化分析时必须用数据。codegraph 的--json参数会把图谱导成结构化 JSONcodegraph graphify ./src --lang js --json graph.jsonJSON 的结构很直接核心就是节点和边两张表{ nodes: [ { id: file://src/payment/index.ts, type: module, name: payment/index.ts, path: src/payment, meta: { lines: 210, language: typescript } } ], edges: [ { from: file://src/payment/index.ts, to: file://src/order/index.ts, relation: calls, source: { line: 12, column: 3 } } ] }有了这份数据什么都能干。我写过一个小脚本统计每个模块的依赖度判断哪些模块是架构上的“接线员”const fs require(fs) const graph JSON.parse(fs.readFileSync(graph.json, utf8)) const degree new Map() for (const edge of graph.edges) { for (const key of [edge.from, edge.to]) { if (!degree.has(key)) degree.set(key, 0) degree.set(key, degree.get(key) 1) } } const sorted [...degree.entries()].sort((a, b) b[1] - a[1]) console.log(sorted.slice(0, 10))这类脚本跑一遍就能输出一份“高耦合模块排行榜”。排行榜不直接告诉你怎么改但能告诉你优先拆谁。后面实战案例里我会详细展示这个用法。4. 核心原理图谱模型与关系抽取4.1 节点与边的设计codegraph 的图谱模型没有搞得很复杂就是用节点和边描述代码结构。节点表示代码实体边表示实体之间的关系。不同粒度的节点对应不同粒度的分析场景节点类型粒度典型用途workspace整个仓库多包项目总览packagenpm/pip 包级别monorepo 的包依赖分析module目录/业务模块架构分层、团队分享file单个文件精确定位跳转function函数/方法重构细节分析边类型也做了一组语义区分边类型含义示例imports模块导入import x from ./utilscalls函数调用formatPrice(amount)creates实例化对象new HttpClient()extends类继承class Admin extends User把粒度分层这件事思路和地图软件类似。城市地图有行政区划、街道、街区、建筑等多个缩放级别你在开车导航时看的路网级别和找具体门牌号时看的建筑级别不可能用同一张图。代码图谱也一样文件级图信息最全但最乱模块级图最适合看宏观结构函数级图则为重构细节服务。4.2 基于AST的关系抽取关系抽取是 codegraph 的核心技术难点。最早的原型阶段我试过用正则表达式匹配import和require很快发现这条路走不通。举一个典型例子下面这段代码里有一行字符串const text callOrder() is not executed here正则匹配函数调用会把它误认为是真实调用但任何编译原理基础的开发者都明白字符串里的内容只是一段文本。要准确区分“真实调用”和“字符串里的伪调用”必须把代码解析成抽象语法树。AST 的概念其实不难理解。编译器拿到源代码后会先做词法分析和语法分析把代码拆成一棵结构化的树。每个节点对应一个语法单元比如import语句、函数声明、变量声明、函数调用。codegraph 遍历这棵树只关心与依赖关系相关的节点类型把这些节点和它们之间的语义关系提取出来就完成了依赖关系的抽取。我选用 tree-sitter 而不是某些编译器的完整 parser主要看中两点。第一是容错能力。tree-sitter 是增量解析器遇到语法错误时能跳过局部继续解析这对分析遗留项目特别重要因为老代码里大量存在ts-ignore、过时语法、甚至半成品文件完整的编译器 parser 碰到语法错误直接罢工tree-sitter 还能坚持产出图谱。第二是速度。tree-sitter 的增量特性使得局部修改后的重新解析非常快这种性能让“保存文件后秒级刷新图谱”成为可能。4.3 分层聚合让大图可读底层的文件级图信息量极大。一个中等规模项目文件级节点可能上千文件间边可能上万。直接展示这种图视觉上基本是一团乱麻。所以聚合是保证图谱可用性的关键。codegraph 的聚合策略基于目录层级。同一目录下的文件在模块视图里被合并成一个模块节点模块之间只要存在任意一对文件之间的依赖就形成一条模块级边。聚合时还会计算边的“强度”如果一个模块 A 下有超过一半的文件都依赖模块 B这条边会被标记为强依赖在图谱里用更粗更深的颜色显示。我见过一个 40 多个文件的项目文件级图谱有近 200 条边肉眼根本理不出头绪。聚合到模块视图后只剩 15 个模块节点和 20 多条边一眼就能看出哪些模块是中间的接线枢纽。聚合的代价是丢失细节但换来的宏观可读性对架构治理来说价值更大。需要细节时再切回 file 或 function 视图即可两种视角本来就是互补的。5. 实战案例用codegraph分析一个中型后端项目5.1 项目情况与分析目标去年我们团队接了个店铺订单服务的重构任务。服务用 TypeScript 写代码量约 8 万行模块目录 48 个典型的历史遗留项目——开发人员换了好几批架构约束早就失效了。重构前技术负责人想搞清楚三件事核心模块到底有哪些、有没有循环依赖、哪些模块可以先拆出来做稳定底座。这正好是 codegraph 的主场。第一步先出全量图谱codegraph graphify services/order \ --lang typescript \ --exclude node_modules,test \ --output order-graph.html \ --json order-graph.json命令跑完大概 40 秒生成了两个文件。接下来就是基于这份图谱逐步回答问题。5.2 找出核心枢纽模块我写的那个依赖度统计脚本在这里派上了用场。它跑出来的排行榜中payment模块的出边和入边数量都遥遥领先比第二名的inventory模块高出将近一倍。这个结果和技术负责人的直觉完全一致。订单流程看起来简单但下单、取消、退款、对账、售后每个场景都要和支付打交道所以payment在代码结构里天然就是高耦合枢纽。图谱的价值不在于告诉我们“payment 很重要”这个结论而在于用数据量化了重要程度让重构优先级有了依据。基于这个结论重构规划第一刀就切向 payment 模块。做法是先把它的对外接口收敛成独立的一层逐步把订单模块与支付实现解耦让 order 只依赖支付抽象不再直接依赖支付细节。如果没有图谱数据支撑这种改动方案在评审会上很容易被质疑“为什么先动这里”有了数据讨论就变成了“怎么动”而非“为什么要动”。5.3 定位循环依赖第二个目标是找循环依赖。codegraph 提供了专门的 cycles 检测命令codegraph cycles order-graph.json --format table跑出来的结果里有一个非常典型的坏味道order 和 payment 互相 importCycle #1: services/order/src/payment/index.ts:12 - services/order/src/order/index.ts:87 services/order/src/order/index.ts:87 - services/order/src/payment/index.ts:12这个循环的害处平时不明显但遇到模块初始化顺序问题时会成倍放大。JavaScript 的模块加载是按依赖图深度优先执行的循环依赖下总有一个模块在初始化时拿不到对方导出的内容具体表现可能是 undefined 变量也可能是“cannot access before initialization”。最恶心的是这类错误不一定稳定复现和环境加载顺序有关排查成本极高。找到这个循环后我们把它列进重构的必改清单。改法不复杂从 order 模块里把只需要 payment 类型定义的部分拆出去消除对 payment 实现的直接引用循环就断了。关键是断点位置不靠图谱很难精准定位到底是谁先打破了单向依赖原则。5.4 辅助重构模块分层与边界划分图谱在重构里的第三个作用是帮我们确定模块分层。我把图谱里的 48 个模块按依赖关系大致分成了三类稳定底座被大量模块调用但不依赖业务逻辑的底层模块比如 logger、config、加密工具。业务核心承载核心领域模型的模块比如 order、payment、inventory。边缘模块被极少模块引用、也很少引用别人的模块通常是独立工具或外部适配器。分层完成后重构策略非常清晰先把稳定底座从业务模块中抽出来建立独立 package再集中精力拆解业务核心之间的循环依赖边缘模块暂时不动等有需求再收拾。对每个待移动的模块我会单独跑一次--direction in --depth 1看它的直接调用来源确认改动涉及哪些上层模块。比如想移动 logger 模块一条命令列出所有直接调用 logger 的模块清单改动的工作量、风险点都在图上了。这个案例给我的最大启发是重构不是靠想象力而是靠结构数据。codegraph 把“代码结构”这件事从一个抽象概念变成了可视化的、可量化的对象。架构讨论会上大家看同一张图争论的都变成了事实层面的问题而不是个人感觉。6. 常见问题与排查技巧实录6.1 扫描慢、内存高怎么处理这是出现频率最高的问题。我见过的大多数情况都是扫描范围没控制好把 node_modules、venv、target 这类依赖目录也扫进去了。第三方依赖代码不仅数量庞大而且很多是压缩过的、模式复杂解析起来格外费资源。处理思路分三步。第一步确认--exclude有没有把依赖目录全部排除。第二步确认--include是否把扫描范围限定在真正的业务代码目录。第三步如果扫描速度还是慢检查是不是有超大文件或生成代码比如自动生成的 SQL 脚本、图片转 base64 的 JS 文件这些文件对图谱分析没有价值直接排除即可。我实测过一个 8 万行的 TypeScript 后端项目排除 node_modules 前后从 12 分钟降到 25 秒差异就是这么夸张。如果你还在为扫描慢头疼先看一眼扫描范围大概率问题在你自己这边不在工具。6.2 图太乱看不清怎么办图谱图一乱第一反应是工具不行其实大多数乱是视图粒度问题。文件级视图在节点多的时候必然乱这不是工具的缺陷而是信息量超出视觉承载极限。我的习惯是三步走先把视图切到模块级别减少节点数量再适当用深度限制让注意力聚焦在两层调用范围内最后用搜索功能定位关键模块图谱会自动以它为视觉焦点隐藏不相关的邻居。操作上记住一个组合拳codegraph graphify ./src --view module --depth 2 --focus src/payment--focus会把视图初始焦点放在指定模块上比手动拖拽搜索省很多事。6.3 动态调用识别不了怎么办静态分析有它的边界动态调用天然识别不了。比如通过字符串拼接动态加载模块const moduleName getModuleName(); const mod await import(./mods/${moduleName});或者基于反射机制调用方法、通过依赖注入容器获取实例这些在运行期才确定的关系静态解析器拿不到完整答案。遇到这种情况codegraph 提供两个补救手段。--aliases参数用于配置路径别名解决 Webpack 或 tsconfig 中路径映射导致的“明明引用了同一个文件图谱里却显示成两个节点”的问题。对于实在无法静态推断的动态关系可以用--manual-edges手动补充比如codegraph graphify ./src --manual-edges src/loader.js - src/plugins/*我的建议是不要把静态图谱当作 100% 完整的真相。它给的是静态结构里九成的事实最后一成需要你对代码运行机制的理解。图谱里找不到某条边不代表运行时没有这条调用只代表静态条件下看不到。6.4 CI与本地结果不一致两种情况会遇到这种问题。第一种是本地有缓存CI 全量跑两边数据自然有差异。表现是本地扫描飞快CI 扫描很慢结果还可能对不上。这种问题的解法很简单本地在变更配置后先codegraph clean清缓存CI 端可以用--no-cache强制全量扫描。第二种是本地和 CI 用的 codegraph 版本不一致。npm 全局安装容易碰到这种情况CI 环境每次装的是最新版本地还是上个月装的旧版解析器更新后关系抽取规则有变化结果自然不同。规范做法是把版本固定下来CI 里安装时锁定版本号或者在 package.json 的 devDependencies 里声明依赖。6.5 问题速查表现象大概率原因解决方式扫描慢、内存高扫描范围包含依赖目录加 exclude限 include图太乱看不清节点过多、视图粒度太细切模块视图限深度用 focus调用关系缺失动态导入、路径别名未配置配置 aliases手动补边CI 与本地结果不同缓存未清理或版本不一致clean / no-cache锁定版本大 JSON 打开卡顿节点边数量过大先聚合再导出避免文件级全量这套排查表基本覆盖了日常使用的绝大多数场景真遇到没覆盖到的问题我建议先跑一遍codegraph debug把扫描日志和解析统计打出来很多问题在日志里一眼就能看出来。我自己调试过的几次诡异问题最后都是靠日志里“skipped X files”这类信息找到根源的。用 codegraph 分析代码库这件事我最大的体会是它给的不是答案而是“看见”的能力。很多架构问题不是没人想解决而是没人说得清问题在哪里。图谱把结构摆到台面上团队讨论的层次立刻就不一样了。后续我还想把运行时调用链和静态图谱做一次关联让“代码本来的样子”和“代码跑起来的样子”出现在同一张图上。但这些都是后话对大多数团队来说先把这张静态地图真正用起来价值就已经足够大了。