作者用 ts.createProgram 和类型检查器为 2570 文件的 Next.js 前端构建跨栈依赖图,验证了 Roslyn 无法独立完成前后端链接时的确定性方案。
我维护着 slnmap,这是一个为 AI 编码智能体提供编译器级精确的 .NET 代码库关系图的工具。它建立在 Roslyn 之上,所以如果编译器看到了某个引用,图里就会有这条边。但它只到 C# 边界就停了。我一直收到的问题是:它能穿透到前端吗?能让"改了这个 handler 会坏什么"以"这三个 React 页面"结尾吗?
我曾经以为诚实的答案是不能。我有证据——我之前的一个原型尝试过完全相同的事情,用的是启发式匹配。按名称相等做连接、以文件名当真相、把取第一个的猜测写成图中真实的边。结果连它自己的文档都把结果标记为不可靠。一个靠猜测的图比没有图更糟,因为总有人最终会信任它。
所以这次在动手做任何东西之前,我给自己一天和一个问题:跨栈链接是确定性的,还是无论怎样都注定要靠启发式?
实验设置是一个大约 200 行的用完即弃脚本,使用 TypeScript Compiler API——ts.createProgram 加上类型检查器,真的那个,不是正则解析器。我把它指向一个生产环境的 React/Next.js 前端,2,570 个文件,然后把发现的结果与我 Roslyn 端已经提取的路由数据做匹配。
结果有三件事。
类型检查器解决了那个干掉旧原型的确切问题。之前的尝试在 23% 的调用点放弃,标记为"变量 URL"——比如 apiClient.get(API_ROUTES.VENDORS) 这类,URL 藏在 barrel 文件后面的常量里,隔了三层 import。检查器的字面量类型会折叠那些常量。不是"大概是这个字符串"——是这个字符串,以编译器解析它的方式解析。那 23% 的桶大部分直接蒸发了。
数字站得住脚。675 个前端 HTTP 调用点,96.6% 解析到了确定性的路由模板。剩下的 3.4%(运行时计算的分段、依赖环境的 base)是可检测且可计数的,所以工具可以说"这 23 个点我无法解析,原因如下"而不是猜测。这已经是整个工具的设计规则:任何不能在静态时解析的东西都要被计数并披露,绝不猜测。
还有我最喜欢的部分——这个实验在这个功能甚至还不存在之前就发现了一个真实的 bug。在把前端调用与后端路由做匹配时,脚本发现一个线上的对话框正在 POST 到 /organizationusers。那个端点在后端根本不存在。一个生产环境的页面悄无声息地把请求发向了虚空。grep 永远找不出这个,因为 bug 的两部分生活在不同的语言里。
回看过去,旧原型不是不该尝试,而是错了——它禁用了类型检查器(为了速度),然后用启发式来掩盖缺口。这个教训可以推广:静态分析中,一旦你开始靠猜测来提高覆盖率,就已经出卖了这个工具唯一值得信赖的东西。
所以这是可以构建的。C# 那半已经发版了——HTTP 端点从 v0.7/v0.8 开始就是图的节点,Minimal APIs 和属性路由的控制器,在我参考代码库上 658/658 个注册都解析了。TypeScript 提取器是我现在正在构建的东西。当两半合在一起时,对 C# handler 做 impact_analysis 会把链路追踪到会坏的 React 组件为止。
slnmap 是免费开源的,MIT 许可:dotnet tool install --global slnmap——仓库在 github.com/EMahmoudNabil/slnmap。如果你尝试过跨栈分析并撞上了启发式的墙,我想听听你走到了多远。