no-unused-binary-expressions: 从代码审查的细微之处到生态系统的改进

实现 ESLint 规则如何导致人们编写 JavaScript 的方式发生改变

四年前,在工作中进行代码审查时,我惊讶地发现 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 项目通过其广泛的采用、推荐的规则集和博客,将这个想法带给了广大受众。随着该想法被记录和大规模验证,另一家营利性公司 Microsoft 的工程师能够通过他们的开源项目 TypeScript 将验证带给更大的受众。

每个组织都根据其优势和地位发挥了不同但关键的作用。我认为贯穿整个过程,实现这种规模协作的共同主线是思想的积极社会化。从在代码审查期间提出问题,到向本地专家提问,再到在公共博客文章和推文中分享想法,每一次扩大的社会化圈子都改进了这个想法,并最终帮助它达到了今天如此广泛的受众。


感谢 Brad Zacher 的最初关键观察和持续鼓励,感谢 Nicholas C. ZakasMilos Djermanovic 在代码审查期间对该规则的重大贡献,以及感谢 Ryan Cavanaugh 将相同类型的验证引入 TypeScript 生态系统。

最新的 ESLint 新闻、案例研究、教程和资源。

Evolving flat config with extends
5 分钟阅读

使用 extends 演进扁平配置

您的 eslint.config.js 文件现在可以使用 extends 来简化您的配置。

ESLint v9.22.0 released
1 分钟阅读

ESLint v9.22.0 发布

我们刚刚推送了 ESLint v9.22.0,这是一个 ESLint 的小版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。

ESLint v9.21.0 released
2 分钟阅读

ESLint v9.21.0 发布

我们刚刚推送了 ESLint v9.21.0,这是一个 ESLint 的小版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。