四年前,在工作中进行代码审查时,我惊讶地发现 Flow 没有警告一个不必要的空值检查。上个月,TypeScript 5.6 发布了禁止无用空值和真值检查的验证规则,这在 GitHub 上排名前 800 的 TypeScript 仓库中发现了近 100 个现有错误。
这两件事是相关的,因为四年前代码审查中的那一刻促使我编写了 no-constant-binary-expressions
规则,该规则可以捕获各种各样的错误。示例包括
// Expecting empty objects to be falsy
const foo = { ...config } || {};
// Confusing precedence of !
const foo1 = !x == null;
// Confusing ?? or || precedence
const foo2 = x === y ?? true;
no-constant-binary-expression
规则反过来帮助启发了新添加的 TypeScript 验证。
鉴于漫长的时间线和许多中间步骤,我认为反思我们是如何走到这一步的会很有趣。在一个代码审查中的这个观察是如何变成对开发人员产生重大积极影响的?并且这个雪球如何继续增长?
要回答这些问题,回顾事件的时间线会有所帮助。
时间线
- 2020年5月:我正在审查工作中一个将可空值改为不可空值的拉取请求。我注意到作者遗留了一个
if
条件,该条件处理现在不可能出现的值为null
的情况。我开始想知道为什么 Flow 没有自动指出这一点。 - 2020年5月:我在一个内部小组中发布了一个帖子,询问 Flow 团队关于此事。答案是 Flow 与 TypeScript 一样,不是健全的。例如,
arr[x]
的类型为不可空,但在运行时可能实际上未定义。Flow 过去曾实现过这些检查,但它们会导致重大问题,因为它们告诉人们他们的空值检查可以安全地移除,而实际上并非如此,因此他们移除了这些检查。 Brad Zacher 碰巧看到了这个帖子,并补充了一个观察结果,即即使语法方法不如基于类型的方法强大,但它也可能是安全的。 - 2020年8月:我将语法验证方法实现为一个内部 ESLint 规则,并意识到它不仅限于空值检查,而是适用于所有常量比较。我在 Meta 的单体仓库中运行它,发现它识别出了数百个现有的错误。
- 2020年10月:Brad Zacher 建议我将其作为 ESLint 中的一个新核心规则提出。 我做到了,他们 喜欢这个主意。
- 2021年11月 - 2022年4月:我 为开源重写了这个规则,由于对 JSX 和样式等事物的不同立场,这花费了惊人的精力。
- 2022年7月:我决定写一篇关于这个新规则的博文。ESLint 团队当时正在重新设计他们的网站,并正在寻找更多博客内容,因此他们问我是否想在 官方博客上发布文章。
- 2023年11月:这篇博文登上了 Hacker News 的首页。这篇文章很可能是由某人分享的,他阅读了我发布的一条 推文,该推文分享了从 ESLint v9.0.0 开始,它将默认启用在
eslint:recommended
中。 - 2024年4月:该规则最终作为默认启用在 ESLint v9.0.0 中发布。此更改必须等待新的大版本,因为在
eslint:recommended
中启用新规则是重大更改。 - 2024年7月:TypeScript 团队希望扩展检查特定类型常量条件的想法,发现了这篇博文和代码。他们扩展了他们执行的验证集,以包含 ESLint 规则执行的大多数检查。他们发现了类似的结果。 排名前 800 个 TS 仓库中的 94 个真实错误。ESLint 规则的成功使他们有信心在
tsc
中默认启用该检查。 - 2024年9月:TypeScript 5.6 发布了默认验证,以禁止常量空值和真值检查。
反思
代码审查中的一小点观察有助于推动整个生态系统产生有意义的改进,这其中有许多关键因素。
- Meta 的内部文化赋予了我,一个普通的工程师,与 Flow 团队成员进行直接对话的能力。这次对话发生在一个 Brad Zacher(一位 ESLint 专家)碰巧可以参与其中的场所。
- Meta 的单体仓库使我能够直接访问一个庞大的代码库,这使我能够轻松地运行规则的早期草稿,以评估验证对大量真实代码的影响。
- Meta 的自主文化赋予我自由,让我可以承担编写此规则的额外任务,尽管它不属于我团队的职责范围。
- ESLint 的可扩展架构使我能够编写自己的规则,并轻松地将其部署到整个公司,而无需说服任何守门人。
- ESLint 团队对新贡献者添加新核心规则持开放态度并积极支持,尽管 2020 年的政策 规定,只有当新规则与新语言特性相关时才会接受新规则。
- 由我发起并由 ESLint 团队扩大的关于这项工作的积极沟通,以博文和推文的形式。这些使 TypeScript 团队能够将他们收到的关于禁止
if (/regex/)
的更具体请求与检测常量条件的更广泛的想法联系起来。 - 在代码审查中,我不满足于仅仅指出一个无用的空值检查,因为我怀疑这类问题存在根本的解决方案。我有点偏执,直到这个解决方案不仅对我、我的团队或我的公司启用,而且对更广泛的生态系统启用,我才感到满意。
下一步
Brad 在那个内部帖子中最初的观察花了四年时间才达到这一点,但我认为这里提出的想法有潜力产生更大的影响。
- TypeScript 和 Flow 可以内部跟踪它们碰巧知道是健全的类型,并根据这些数据酌情报告错误,从而允许在更多情况下执行此类检查。
- 其他以前出于与 Flow 相同的原因而避免报告不必要检查的不健全语言可以使用相同的方法来捕获逻辑错误。
- 更广泛地说,死代码消除是编译器设计中一个众所周知的领域,其中包含许多编码的技术和方法。但是,它们几乎总是作为编译器后端的优化应用。我怀疑许多这些相同的死代码消除技术可以应用于编译器的前端,并用于检测和报告错误。
结论
这项工作跨越了数年和多个组织。
作为 Meta(一家营利性公司)中的一名积极的个人,我能够引发一场对话,从而产生了一个想法。然后,我能够利用 ESLint 在内部验证该想法。一旦得到验证,ESLint 项目(一个非营利组织)通过其广泛的采用、推荐规则集和博客将该想法传播给大量受众。随着该想法在规模上得到记录和验证,微软(另一家营利性公司)的工程师能够通过其开源项目 TypeScript 将这些验证传播给更广泛的受众。
每个组织都根据其优势和地位发挥了不同的但至关重要的作用。我相信贯穿这一过程的一个共同点,即促成了这种规模的协作,是思想的积极社会化。从代码审查中提出问题,到向当地专家提问,再到在公开的博文和推文中分享想法,每个不断扩大的社会化圈子都改善了这个想法,并最终帮助它触及了今天已经达到的广泛受众。
感谢 Brad Zacher 的最初关键观察和持续鼓励,感谢 Nicholas C. Zakas 和 Milos Djermanovic 在代码审查期间对规则做出的重大贡献,以及感谢 Ryan Cavanaugh 将这些类型的验证带入 TypeScript 生态系统。