[译] 由 no-constant-binary-expression 捕捉到的几个有趣的代码缺陷
原文地址:https://eslint.org/blog/2022/07/interesting-bugs-caught-by-no-constant-binary-expression/
原文作者:Jordan Eldredge
原文发布于:2022 年 07 月 06 日
这个新规则能捕捉大量惊人惊讶的逻辑错误。
我在 ESLint v8.14.0 提交了一个新的核心规则 no-constant-binary-expression。我通过这个规则检测到大量的微妙、有趣的代码缺陷。
在这篇文章中,我将解释该规则的作用,并分享它在 Material UI、Webpack、VS Code 和 Firefox 等流行开源项目中检测到的一些真实错误示例,以及它在 Meta 内部发现的一些有趣错误。我希望这些示例能说服您尝试在自己的项目中启用该规则!
no-constant-binary-expression 有什么作用?
该规则检查运行时结果不会改变的比较(==
、!==
等),以及总是或从不短路的逻辑表达式(&&
、??
、||
)。
举个例子:
+x == null
永远都是 fasle 的,因为+
会把x
强制变成一个数字,而数字永远不会是 nullish。{ ...foo } || DEFAULT
永远不会返回DEFAULT
,因为对象总是 truthy 的。
使用 no-constant-binary-expressions
在真实世界中发现到的错误
在本节中,我将与大家分享该规则可以捕捉到的一些错误类型。每种类型都至少包含一个在流行的开源项目中发现的具体实例。我选择在这里列举真实的例子,并不是要羞辱任何人或任何项目,而是要让大家明白,这些错误是任何团队都很容易犯的。
混淆运算符优先级
该规则发现的最常见错误是开发人员误解了运算符的优先级,尤其是一元运算符,如 !
、+
和 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 !== {};
期望空对象为 false
或 null
另一种常见的 JavaScript 错误是将空对象视为 nullish 或 falsy。对于来自 Python 等语言的用户来说,这很可能是一个容易犯的错误,因为在 Python 中,空列表和字典都是 falsy 的。
const newConfigValue = { ...configProfiles } ?? {};
是 >=
还是 =>
?
我只见过一次这样的错误情景,但我还是想把它写出来,因为它是一个很好的例子,说明这条规则可以捕捉到意想不到的错误类型。
在这里,开发人员的本意是测试一个值是否大于或等于零(>= 0
),但却不小心颠倒了字符的顺序,创建了一个返回 0 && startWidth <= 1
的箭头函数!
assert(startWidth => 0 && startWidth <= 1);
_来自 _Mozilla
no-constant-binary-expression 捕捉到的其他错误
以上五类错误并不详尽。我最初在 Meta 的大型 monorepo 上运行这一规则的第一个版本时,发现了 500 多个问题。虽然许多问题属于上述类别,但也有一些其他有趣的长尾错误。其中包括:
- 以为
||
支持集合操作:states.includes('VALID' || 'IN_PROGRESS')
- 以为原生函数的返回结果是 nulls:
Number(x) == null
- 不知道原生对象的构造函数返回的是包装对象:
new Number(x) === 10
我从来没有打算逐一找出这些具体问题,但通过努力找出任何“无用”的东西,我们得以发现并纠正这些问题。
结论
正如你现在看到的,no-constant-binary-expression
能够检测出各种不同类型的错误。该规则之所以能做到这一点,并不是因为它的程序设计就是为了查找这些特定的问题,而是因为所有这些错误都有一个共同点:它们表现为无用代码。由于开发人员通常不会编写无用代码,因此检测无用代码通常就能检测出错误。
如果您觉得这些示例很有说服力,请考虑在 ESLint 配置中启用 no-constant-binary-expression
:
// eslintrc
module.exports = {
rules: {
// Requires eslint >= v8.14.0
"no-constant-binary-expression": "error"
}
}
如果你这样做了,并且发现了错误,我很乐意听听你的意见!
感谢 Brad Zacher 最初的观察,他的观察启发了这项工作,并建议将其作为新的核心规则。感谢 Milos Djermanovic 在代码审查过程中做出的重要贡献。