返回资讯中心
外部精选
软件工程
#开发者实时流

DocSpring用AI代理四天完成Rails应用剩余九成Sorbet类型标注

来源标题:Going all-in on Sorbet (type-checking for Ruby)

DocSpring在Rails应用中引入Sorbet类型检查18个月后仅完成约一成代码标注,随后借助AI代理在四天内补完其余部分。作者统计Sentry自2017年发来的告警,发现258个进入生产的错误本可由类型检查捕获,其中131个是nil相关错误、127个为其他同类类型错误。

Hacker News 最新nathan_f772026/11/11 · 今天30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

所有文章 2026年11月11日 · 14分钟阅读 · Nathan Broadbent Sorbet在我们的Rails应用里放了18个月,只覆盖了十分之一的代码。后来AI agent用四天时间把剩下的代码都加上了类型,而我们还在继续推进。

2020年5月,我给自己写了一张工单:配置Sorbet,一个Ruby的类型检查器。“我总把特别基础的bug推到生产环境,”工单里写道,“缺少常量、缺少方法之类的问题,类型检查器都能轻松抓到。”

我喜欢Ruby,DocSpring从2017年起就一直是个Rails应用。但Ruby在代码运行之前什么都不检查,而鸭子类型意味着一个方法会接受你传给它的任何东西,直到它调用了那个对象并不具备的方法为止。我们的生产环境错误告警说明了这要付出什么代价。我翻查了Sentry自2017年以来发给我们的告警邮件,数出131个不同的nil错误:undefined method 'x' for nil,或者no implicit conversion of nil into String。另外127个是同一类错误,只是涉及其他值:在一个Integer或String上调用了它并不具备的方法、视图里拼错的变量、不存在的常量、参数数量不对。这就是258个进入生产环境的bug,尽管我们一直保持着非常高的测试覆盖率。类型检查器能报告其中大部分错误,并在代码发布前让CI失败。

那张Sorbet工单等了将近五年,因为给一个大型Ruby代码库添加Sorbet类型是项浩大的工程。当我在2025年4月终于装上Sorbet时,我给15个文件加了类型,并写了一条规则:新文件应当使用strict。十八个月后,我们只有大约十分之一的Ruby代码加了类型。

2026年10月,AI agent用四天时间给我们剩下的代码加上了类型。从那以后,Sorbet在我们的Rails应用里检查的每一个Ruby文件都加了类型,每一个ERB视图也一样。做到这一点,意味着要构建我们自己的Sorbet插件、把HAML视图转成ERB,并教会Sorbet读取模板。11月,一个新插件开始读取Tapioca过去从我们代码里复制到生成文件中的那些事实信息。

截至2026年11月11日的情况是:Ruby类型迁移,以及我们带RSpec、Capybara、rswag、FactoryBot和ActiveAdmin插件的Sorbet构建,自10月24日起已在生产环境使用。HAML转ERB于11月4日上线,ERB类型检查于11月8日上线。rails插件(它为我们自己的代码取代了Tapioca生成的大部分文件)于11月11日上线,包含在我们的Sorbet构建0.6.13583.8中。

最终让我动手的是LogStruct,我们为Rails做的结构化日志gem。我在2025年初在那里用上了Sorbet,我喜欢类型能保护我们的日志免受无效或缺失值的影响。大约同一时间,我开始把Sorbet引入DocSpring应用。

我在2025年4月1日配置了Sorbet和Tapioca。Tapioca生成RBI文件:Ruby Interface文件,用来告诉Sorbet那些它在源码里看不到的方法,比如gem和Rails在运行时定义的方法。第一天我们就有每个gem以及Rails元编程的RBI,而从4月中旬起,每次CI构建都会运行srb tc。

我加类型的第一个代码是我们的JSON schema生成器,正是2020年那张工单提到的两个领域之一。Sorbet让每个文件自行选择接受多少检查。在typed: true下,它会报告普通的类型错误。在typed: strict下,每个方法都需要签名,每个实例变量都需要声明类型。没有标记的文件是typed: false:Sorbet只检查它的常量能否解析。Sorbet的严格度文档里有详细说明。

配置合入时,我们大约1,000个Ruby文件中有15个是typed: true或更严格,它们之间共有51个方法签名。我还在我们的编码agent所读的指令里加了一行:新文件使用typed: strict。

当某个文件违反那条规则时,没有任何机制会让构建失败。新文件可以没有标记,或者显式写上typed: false,CI照样通过。

覆盖率几乎没动。2025年9月,我们给生成API客户端测试的代码加了类型,把加类型的文件从15个增加到81个。大约那时我考虑过用Rust或Go重写后端,后来认定一个有着那么多model、mailer和job的八年老Rails应用不会有什么改变,于是告诉自己还是多在一些地方加上Sorbet吧。

我也尝试过给我们的spec加类型。我找到的基于Tapioca的做法需要在spec中到处写extends,并且每次改动后都要重新生成。我希望Sorbet本身能理解RSpec DSL,在编辑器里即时反馈,于是我研究过插件或fork能否做到这一点。fork看起来可行,但工作量大、维护负担重,所以我搁置了。

到2026年10月1日,我们的1,262个Ruby文件中有129个加了类型,约占10%。在app/下,419个文件中有356个完全没有标记,新controller仍然以typed: false的形式出现。

到那时,agent已经在写我们几乎所有的代码。这改变了我一直拖延的这场迁移的成本。

一项对Reek(一个Ruby代码异味工具)的评估发现了那些在typed: true下本会是类型错误的bug。于是我们做了个实验:把app/、lib/和config/里每个未检查的文件标记为typed: true,然后统计错误。280个文件里有4,657个错误,另有204个文件无需任何改动就通过了。大多数错误是“方法不存在”,而其中大多数是Sorbet对应用的了解存在缺口,并非bug。

大约那时我让一个agent做的一项代码质量研究得出结论:证据还不足以表明给每个文件加类型能收回成本。我还是决定推进,并于10月20日开始了迁移。

agent从那些已经通过的文件开始,它们只需要加个标记。然后它们填补Sorbet知识上的缺口:为gem在运行时提供的方法写声明、为我们的mixin所期望的类写声明、为可能为nil的值写声明。然后是应用的其他部分、命令行工具、数据库迁移以及spec。每次改动后都会运行Sorbet,它的错误会告诉它们接下来修什么。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →