ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ESLint no-unsafe-negation 规则完全指南:杜绝 `!key in object` 这类关系运算符取反陷阱

ESLint no-unsafe-negation 规则完全指南:杜绝 `!key in object` 这类关系运算符取反陷阱 ESLint no-unsafe-negation 规则完全指南杜绝!key in object这类关系运算符取反陷阱【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint导读no-unsafe-negation是 ESLint 内置的一条 problem 类规则专门拦截“对关系运算符左操作数进行逻辑取反”的低级错误例如把!(key in object)误写成!key in object。本文将以 docs/src/rules/no-unsafe-negation.md 为骨架结合 lib/rules/no-unsafe-negation.js 的源码实现与 tests/lib/rules/no-unsafe-negation.js 的测试用例讲清该规则的触发原理、例外情形、enforceForOrderingRelations选项以及两条内置修复建议帮助你既能在配置中正确启用它也能理解它为什么值得加入 recommended 配置。问题背景运算符优先级带来的经典误写在 JavaScript 中一元逻辑非运算符!的优先级高于in与instanceof这两个二元关系运算符。这意味着!key in object会被解析为(!key) in object而不是开发者心中想要的!(key in object)。正如我们可能把-(a b)误写为-a b一样!key in object的写法几乎总是出于失误。更糟糕的是由于in运算符右侧会做类型转换把左操作数转成字符串作为属性名!key in object的实际语义变成了(key ? false : true) in object与预期的“判断 key 是否不在 object 中”南辕北辙。!obj instanceof Ctor同样危险它等价于(!obj) instanceof Ctor而布尔值不是对象instanceof的结果恒为false代码会静默地永远走错分支。该规则正是针对in和instanceof默认以及可选的、、、开启选项后这两类关系运算符的左操作数取反行为进行拦截。在其文档元信息中规则的 front matter 声明了rule_type: problem表明它属于“代码确实有 bug”级别的检查项而非风格类检查同时handled_by_typescript: true说明这类问题的根因运算符优先级理解错误也被 TypeScript 的类型系统关注。Rule Details规则的默认行为规则默认禁止对以下两个关系运算符的左操作数进行取反in运算符instanceof运算符。从源码 lib/rules/no-unsafe-negation.js 可以看到默认匹配的运算符集合由辅助函数isInOrInstanceOfOperator定义仅包含in与instanceoffunction isInOrInstanceOfOperator(op) { return op in || op instanceof; }判定“取反”的依据是 AST 节点形态。源码中的isNegation辅助函数lib/rules/no-unsafe-negation.js要求左操作数是一个UnaryExpression且运算符为!function isNegation(node) { return node.type UnaryExpression node.operator !; }不正确的代码示例以下写法都会被规则报告为unexpected错误/*eslint no-unsafe-negation: error*/ if (!key in object) { // operator precedence makes it equivalent to (!key) in object // and type conversion makes it equivalent to (key ? false : true) in object } if (!obj instanceof Ctor) { // operator precedence makes it equivalent to (!obj) instanceof Ctor // and it equivalent to always false since boolean values are not objects. }注意!key in object这类表达式在语法上完全合法、不会报语法错误因此很难被开发者肉眼发现必须依靠静态分析规则来兜底。正确的代码示例规则鼓励把取反作用在整个关系表达式上而非左操作数/*eslint no-unsafe-negation: error*/ if (!(key in object)) { // key is not in object } if (!(obj instanceof Ctor)) { // obj is not an instance of Ctor }Exception显式用括号包裹时放行规则为“确实想对左操作数取反”的罕见场景保留了一个例外当取反表达式被整段显式包裹在括号中时规则不再报告。允许的例外写法/*eslint no-unsafe-negation: error*/ if ((!foo) in object) { // allowed, because the negation is explicitly wrapped in parentheses // it is equivalent to (foo ? false : true) in object // this is allowed as an exception for rare situations when that is the intended meaning } if (( !foo) in object) { // you can also make the intention more explicit, with type conversion }这里的判断在源码中有明确对应。规则的 create 逻辑lib/rules/no-unsafe-negation.js中有一个关键条件if ( (isInOrInstanceOfOperator(operator) || orderingRelationRuleApplies) isNegation(node.left) !astUtils.isParenthesised(sourceCode, node.left) ) { // report ... }只有当左操作数没有被括号包裹!astUtils.isParenthesised(sourceCode, node.left)时才报告。isParenthesised实现在 lib/rules/utils/ast-utils.js 中它通过检查节点前后相邻 token 是否分别是(与)来判断节点是否被括号括起function isParenthesised(sourceCode, node) { const previousToken sourceCode.getTokenBefore(node), nextToken sourceCode.getTokenAfter(node); return ( Boolean(previousToken nextToken) previousToken.value ( previousToken.range[1] node.range[0] nextToken.value ) nextToken.range[0] node.range[1] ); }不构成例外的写法单纯给foo加括号并不算“显式包裹整个取反”因此以下代码仍然会被报告/*eslint no-unsafe-negation: error*/ if (!(foo) in object) { // this is not an allowed exception }这一行为在测试中得到了精确覆盖tests/lib/rules/no-unsafe-negation.js的 valid 用例包含(!a) in b而 invalid 用例包含!(a) in btests/lib/rules/no-unsafe-negation.js证明括号必须包裹住!与整个左操作数才能触发例外。OptionsenforceForOrderingRelations规则支持一个对象选项用于扩展检查范围选项值默认作用enforceForOrderingRelations: false默认允许对排序关系运算符、、、的左操作数取反enforceForOrderingRelations: true—同样禁止对排序关系运算符的左操作数取反选项的默认值与 schema 定义在规则源码的 meta 中lib/rules/no-unsafe-negation.jsdefaultOptions: [ { enforceForOrderingRelations: false, }, ], schema: [ { type: object, properties: { enforceForOrderingRelations: { type: boolean, }, }, additionalProperties: false, }, ],在 create 逻辑中orderingRelationRuleApplies同时依赖该选项是否为true以及当前运算符是否为排序关系运算符lib/rules/no-unsafe-negation.jsfunction isOrderingRelationalOperator(op) { return op || op || op || op ; }开启该选项的目的是避免写出! a b这类表达式——它等价于(a ? 0 : 1) b而开发者真正想要的多半是!(a b)。开启选项后新增的不正确代码/*eslint no-unsafe-negation: [error, { enforceForOrderingRelations: true }]*/ if (! a b) {} while (! a b) {} foo ! a b; foo ! a b;对应的测试用例见 tests/lib/rules/no-unsafe-negation.js四个排序运算符、、、各有独立的 invalid 用例且每个用例都验证了两条 suggestion 的输出。需要注意的是该选项默认关闭因为对! a b这类写法的意图判断不如in/instanceof那么绝对——有些人可能是刻意用! a b表达“取反后的比较值”。测试的 valid 用例tests/lib/rules/no-unsafe-negation.js也确认不配置选项或显式配置{ enforceForOrderingRelations: false }时! a b、foo ! a b;都是合法的。源码级解析报告、修复建议与 recommended 状态报告信息与定位规则触发时context.report会把loc指向左操作数的位置node.left.loc并附带当前运算符名作为消息数据lib/rules/no-unsafe-negation.js。预设的三条消息为messages: { unexpected: Unexpected negating the left operand of {{operator}} operator., suggestNegatedExpression: Negate {{operator}} expression instead of its left operand. This changes the current behavior., suggestParenthesisedNegation: Wrap negation in () to make the intention explicit. This preserves the current behavior., },两条内置修复建议suggestions规则声明了hasSuggestions: true虽然自身不可自动修复fixable: null但会给出两条语义明确的 suggestion开发者可手动一键应用suggestNegatedExpression改变行为把整个关系表达式用括号括起来并对整体取反即把!a in b改成!(a in b)。其 fix 实现lib/rules/no-unsafe-negation.js将!之后到表达式结尾的文本整体包进括号。suggestParenthesisedNegation保持行为仅把取反的左操作数用括号包裹即把!a in b改成(!a) in b从而命中规则例外、保留当前语义。测试用例对这两条建议的输出做了精确断言例如对!a in btests/lib/rules/no-unsafe-negation.js第一条建议输出!(a in b)第二条建议输出(!a) in b对排序运算符场景也一样如if (! a b) {}的两条建议分别输出if (!( a b)) {}与if ((! a) b) {}tests/lib/rules/no-unsafe-negation.js。两条建议的取舍本质上是一次行为选择前者修正语义、后者承认意图规则的 message 文案已经明确提示了这一点。已进入 recommended 配置在规则源码的meta.docs中声明了recommended: truelib/rules/no-unsafe-negation.js即该规则默认包含在 ESLint 的 recommended 配置中凡是使用eslint:recommended的项目无需额外配置即可获得此类检查。规则的注册入口在 lib/rules/index.js采用惰性加载方式导出。底层调用链小结从规则触发到报告的完整链路为遍历器遇到BinaryExpression节点时调用规则的create中注册的处理器判断node.operator是否命中in/instanceof或开启选项后的排序关系运算符判断node.left是否为!一元表达式且未被括号包裹astUtils.isParenthesised命中后context.report报告错误同时挂载两条 suggestion。何时不使用该规则如果你不希望提示这类“不安全的关系运算符左操作数取反”可以安全地关闭此规则/*eslint no-unsafe-negation: off*/或在使用 flat config 时从 rules 中显式关闭export default [ { rules: { no-unsafe-negation: off, }, }, ];不过考虑到该问题根植于 JavaScript 运算符优先级的固有陷阱、且错误形态高度隐蔽一般建议保持默认开启。如果项目大量依赖(!a) in b这类刻意写法也可以保留规则并让团队统一采用“显式括号包裹”的例外写法从而兼顾意图表达与代码检查。关联资源规则文档docs/src/rules/no-unsafe-negation.md规则实现lib/rules/no-unsafe-negation.js测试用例tests/lib/rules/no-unsafe-negation.js括号判断工具函数lib/rules/utils/ast-utils.js规则注册表lib/rules/index.js【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进