有趣 Bug:no-constant-binary-expression 规则捕获

一个新的规则,可以捕获各种出乎意料的逻辑错误。

ESLint v8.14.0 中,我贡献了一个新的核心规则,名为 no-constant-binary-expression。令我惊讶的是,它能够检测到各种各样微妙而有趣的错误。

在这篇文章中,我将解释这个规则的作用,并分享一些它在流行的开源项目中检测到的真实 bug 示例,例如 Material UI、Webpack、VS Code 和 Firefox,以及一些它在 Meta 内部发现的有趣 bug。我希望这些例子能说服你在你工作的项目中启用这个规则!

no-constant-binary-expression 规则的作用是什么?

该规则检查结果在运行时不会变化的比较运算 (==, !==, 等) 以及总是从不短路的逻辑表达式 (&&, ??, ||)。

例如

  • +x == null 将始终为 false,因为 + 会将 x 强制转换为数字,而数字永远不会是 nullish。
  • { ...foo } || DEFAULT 永远不会返回 DEFAULT,因为对象始终是 truthy。

这些都是看起来会影响程序评估方式,但实际上并非如此的表达式示例。

这个规则最初只是试图检测不必要的 null 检查。然而,随着我的深入研究,我意识到无用的 null 检查只是一个更广泛类别的一个特例:无用的代码。最终我明白了:开发人员并非有意编写无用的代码,而与开发人员意图不符的代码,从定义上来说就是一个 bug。因此,任何你可以检测到的无用代码都是一个 bug。

当我针对我们在 Meta 的代码库运行该规则的第一个版本时,我的这种认识得到了证实,它检测到了各种各样微妙而有趣的 bug,而这些 bug 已经通过了代码审查。

使用 no-constant-binary-expressions 规则发现的真实 bug

在本节中,我将分享此规则可以捕获的多种类型的 bug。每个示例都至少包含一个在流行的开源项目中检测到的具体示例。我选择在此处包含真实示例并非为了羞辱任何人或任何项目,而是为了强调这些错误是任何团队都可能轻易犯下的。

混淆运算符优先级

该规则发现的最常见的 bug 类型是开发人员误解了运算符的优先级,特别是像 !+typeof 这样的一元运算符。

if (!whitelist.has(specifier.imported.name) == null) {
return;
}

来自 Material UI (还有:VS Code 1, 2, Webpack, Mozilla)

混淆 ??|| 优先级

当尝试定义默认值时,人们会对诸如 a === b ?? c 这样的表达式感到困惑,并认为它会被解析为 a === (b ?? c)。但实际上它会被解析为 (a === b) ?? c

shouldShowWelcome() {
return this.viewModel?.welcomeExperience === WelcomeExperience.ForWorkspace ?? true;
}

来自 VS Code

题外话:观察到开发人员经常被运算符优先级搞糊涂,这启发我尝试开发 一个 VS Code 扩展,以可视化地阐明优先级是如何被解释的。

期望对象按值比较

来自其他结构按值比较而非按引用比较的语言的开发人员,很容易陷入认为他们可以通过与新创建的空对象进行比较来测试对象是否为空的陷阱。当然,在 JavaScript 中,对象是按引用比较的,并且任何值都永远不可能等于新构造的对象字面量。

在这个例子中,hasData 将始终设置为 true,因为 data 永远不可能在引用上等于新创建的对象。

hasData = hasData || data !== {};

来自 Firefox (还有:Firefox)

期望空对象为 falsenull

另一个常见的 JavaScript 错误类别是期望空对象为 nullish 或 falsy。对于来自像 Python 这样的语言的人来说,这可能是一个容易犯的错误,在 Python 中,空列表和字典是 falsy 的。

const newConfigValue = { ...configProfiles } ?? {};

来自 VS Code (还有:VS Code 1, 2)

>= 还是 =>

我只见过一次这种特定的笔误,但我想把它包括进来,因为它很好地展示了这个规则可以捕获的意想不到的 bug 类型。

在这里,开发人员本意是测试一个值是否大于或等于零 (>= 0),但不小心颠倒了字符的顺序,创建了一个返回 0 && startWidth <= 1 的箭头函数!

assert(startWidth => 0 && startWidth <= 1);

来自 Mozilla

no-constant-binary-expression 规则捕获的其他错误

以上五类错误并非详尽无遗。当我最初在我们在 Meta 的(非常)大型的单体仓库上运行此规则的第一个版本时,它发现了 500 多个问题。虽然许多问题属于上述类别,但也有一长串其他有趣的 bug。一些亮点包括

  • 认为 || 允许集合运算:states.includes('VALID' || 'IN_PROGRESS')
  • 认为原始函数会传递 null 值:Number(x) == null
  • 不知道原始类型 构造函数 返回 boxed primitives:new Number(x) === 10

我从未想过要单独 lint 这些特定问题,但通过简单地尝试识别任何“无用”的东西,我们能够找到并纠正它们。

结论

正如你现在所看到的,no-constant-binary-expression 规则能够检测各种不同类型的 bug。该规则之所以能够做到这一点,并非因为它被编程为查找这些特定问题,而是因为所有这些 bug 都有一个共同点:它们都表现为无用的代码。由于开发人员通常不打算编写无用的代码,因此检测无用的代码通常会导致检测到 bug。

如果你觉得这些例子很有说服力,请考虑在你的 ESLint 配置中启用 no-constant-binary-expression 规则

// eslintrc
module.exports = {
rules: {
// Requires eslint >= v8.14.0
"no-constant-binary-expression": "error"
}
}

如果你这样做了,并且它发现了 bug,我很乐意 听到你的消息

感谢 Brad Zacher 的最初观察,这启发了这项工作,并建议将其作为新的核心规则提出。并感谢 Milos Djermanovic 在代码审查期间做出的重大贡献。

最新的 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 的次要版本升级。此版本添加了一些新功能,并修复了先前版本中发现的几个 bug。

ESLint v9.21.0 released
2 分钟阅读

ESLint v9.21.0 发布

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