资讯动态

ESLint 规则深入解析:no-unsafe-optional-chaining 与可选链的安全边界

发布时间:2026/9/12 22:23:36 来源:尧图企业网站定制
ESLint 规则深入解析no-unsafe-optional-chaining 与可选链的安全边界【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint可选链?.是 ECMAScript 2020 引入的高频语法糖它能优雅地避免深层属性访问抛错但也存在一个被普遍忽视的陷阱短路后返回的undefined一旦被当作函数、对象、数字等继续参与运算反而会立刻抛出TypeError或产生NaN。本篇以 ESLint 内置规则no-unsafe-optional-chaining为主线结合本仓库中该规则的完整文档docs/src/rules/no-unsafe-optional-chaining.md、源码实现lib/rules/no-unsafe-optional-chaining.js与测试用例tests/lib/rules/no-unsafe-optional-chaining.js讲清该规则检测的每一类危险写法、disallowArithmeticOperators选项的用法以及其底层 AST 检测原理。读完本文你将能在自己的 ESLint 配置中精确启用并调优该规则彻底杜绝可选链短路引发的运行时错误。一、问题背景可选链短路之后会发生什么可选链表达式?.的语义是当链式访问路径上的某个引用为null或undefined时整个表达式短路并以undefined作为返回值而不再继续向后求值。这在多数场景下是安全的但若把求值结果继续当作可调用的函数、可索引的对象、可数值参与运算的数字undefined显然不满足这些角色的要求于是运行时立刻抛错。原始文档给出了最直观的演示当obj为undefined时下列写法全部触发TypeErrorconst obj undefined; 1 in obj?.foo; // TypeError with (obj?.foo); // TypeError for (bar of obj?.foo); // TypeError bar instanceof obj?.foo;// TypeError const { bar } obj?.foo;// TypeError细看这些写法共同点是可选链表达式都处在一个必须拿到真实值的位置上in/instanceof的右侧操作数要求是对象with、for...of要求是对象解构赋值const { bar } ...要求可迭代或可解构的容器。括号会缩小短路范围另一个关键认知是括号会限制短路链的作用域。obj?.foo.bar中短路保护覆盖整条链但写成(obj?.foo).bar后括号把obj?.foo圈成独立表达式——短路一旦发生括号外对.bar的访问依然会对undefined执行照样抛TypeErrorconst obj undefined; (obj?.foo)(); // TypeErrorundefined 不是函数 (obj?.foo).bar; // TypeErrorundefined 没有 .bar这正是本规则名称中 unsafe不安全的由来可选链用得看似安全实际却挡不住紧跟其后的强制解引用。二、规则目标Rule Details该规则的官方定位是problem类型规则见文档 frontmatter 的rule_type: problem以及 lib/rules/no-unsafe-optional-chaining.js 中meta.type: problem其目标是检测可选链未能阻止运行时错误的场景具体而言就是那些短路为undefined之后会立刻抛出TypeError的可选链使用位置。值得注意的是该规则在源码的meta.docs中标记为recommended: true并确实出现在eslint:recommended内置配置中packages/js/src/configs/eslint-recommended.js 将其配置为error同时也被收录进eslint:allpackages/js/src/configs/eslint-all.js。这意味着使用eslint:recommended的工程无需额外配置即可获得这项保护。规则的 incorrect 示例完整继承以下全部写法都会在短路时抛出TypeError因此被判定为错误代码/*eslint no-unsafe-optional-chaining: error*/ (obj?.foo)(); // 把可能为 undefined 的结果当函数调用 (obj?.foo).bar; // 括号截断短路链后继续取属性 (foo?.()).bar; // 可选调用结果被继续取属性 (foo?.()).bar(); // 可选调用结果被继续取属性再调用 (obj?.foo ?? obj?.bar)(); // ?? 右侧短路结果被当作函数 (foo || obj?.foo)(); // || 右侧短路结果被当作函数 (obj?.foo foo)(); // 左侧短路结果被当作函数 (foo ? obj?.foo : bar)(); // 三目运算符的分支短路结果被当作函数 (foo, obj?.bar).baz; // 逗号表达式末尾短路结果被继续访问属性 (obj?.foo)template; // 标签模板的标签不能是 undefined new (obj?.foo)(); // 构造函数不能是 undefined [...obj?.foo]; // 展开要求可迭代对象 bar(...obj?.foo); // 参数展开要求可迭代对象 1 in obj?.foo; // in 右侧要求对象 bar instanceof obj?.foo; // instanceof 右侧要求构造器对象 for (bar of obj?.foo); // for...of 要求可迭代对象 const { bar } obj?.foo; // 解构要求可解构对象 [{ bar } obj?.foo] []; // 嵌套解构中的默认值来源不能是 undefined with (obj?.foo); // with 要求对象需在 script 模式下 class A extends obj?.foo {} // extends 要求构造器 const a class A extends obj?.foo {}; // 类表达式同样受限 async function foo () { const { bar } await obj?.foo; // await 结果不能用于解构 (await obj?.foo)(); // await 结果不能直接调用 (await obj?.foo).bar; // await 结果不能继续取属性 }规则的 correct 示例完整继承对应地以下写法是安全的要么在短路发生后没有继续强制解引用要么通过?.()、??兜底、外层逻辑运算等方式为undefined提供了保护/*eslint no-unsafe-optional-chaining: error*/ (obj?.foo)?.(); // 调用本身也带可选链短路即停 obj?.foo(); // 链内调用短路覆盖整条链 (obj?.foo ?? bar)(); // ?? 兜底后再调用 obj?.foo.bar; // 短路保护覆盖整条链 obj.foo?.bar; // 可选链在链条末端安全 foo?.()?.bar; // 可选调用 可选属性 (obj?.foo ?? bar)template; // ?? 兜底后再作标签 new (obj?.foo ?? bar)(); // ?? 兜底后再 new const baz {...obj?.foo}; // 对象展开允许 undefined 参与 const { bar } obj?.foo || baz; // || 兜底后再解构 async function foo () { const { bar } await obj?.foo || baz; // await 结果有 || 兜底 (await obj?.foo)?.(); // await 结果再套可选调用 (await obj?.foo)?.bar; // await 结果再套可选属性 }对比两组示例可以提炼出安全判据短路结果必须要么停留在表达式内部被?.继续保护要么被??/||等逻辑运算符兜底要么所处的位置本就允许undefined如对象展开。三、选项OptionsdisallowArithmeticOperators该规则只接受一个对象型选项disallowArithmeticOperators默认值为false源码中通过defaultOptions声明见 lib/rules/no-unsafe-optional-chaining.js。它解决的是另一类不太显眼的隐患当可选链短路为undefined后若继续参与算术运算结果不是抛错而是静默地变成NaN例如undefined 1→NaN。这类问题同样值得告警。当disallowArithmeticOperators设为true时规则会额外覆盖三类运算符文档原文列表一元运算符-、算术运算符、-、/、*、%、**赋值运算符、-、/、*、%、**开启该选项后新增的 incorrect 示例完整继承/*eslint no-unsafe-optional-chaining: [error, { disallowArithmeticOperators: true }]*/ obj?.foo; // 一元正号undefined → NaN -obj?.foo; // 一元负号undefined → NaN obj?.foo bar; // 算术运算undefined → NaN obj?.foo - bar; obj?.foo / bar; obj?.foo * bar; obj?.foo % bar; obj?.foo ** bar; baz obj?.foo; // 复合赋值同样会得到 NaN baz - obj?.foo; baz / obj?.foo; baz * obj?.foo; baz % obj?.foo; baz ** obj?.foo; async function foo () { await obj?.foo; // await 结果参与一元运算 await obj?.foo bar; // await 结果参与算术 baz await obj?.foo; // await 结果参与复合赋值 }需要说明两个边界行为位运算与逻辑赋值不受影响|、、、、、|、、^以及||、等运算符不在该选项的覆盖范围内——这些运算对undefined有明确的数值转换规则如undefined | 0结果为0不会产生NaN。这一点在 tests/lib/rules/no-unsafe-optional-chaining.js 的 valid 用例中有系统验证。typeof、void、!、~等不告警typeof obj?.foo本身是安全的惯用写法测试中也将其列为通过用例。选项的默认行为当选项为默认值false时obj?.foo bar、obj?.foo、baz obj?.foo这类算术写法不会被报告——规则只聚焦会抛TypeError的硬错误。测试文件中专门用options: [{}]与options: [{ disallowArithmeticOperators: false }]两组用例验证了默认不告警的行为。团队如果希望连静默 NaN也拦截可以显式开启该选项。四、源码剖析规则如何在 AST 上工作理解了规则行为后再进入 lib/rules/no-unsafe-optional-chaining.js 的源码看它是如何在 AST抽象语法树层面实现检测的。整个实现可以拆成三块。1. 危险运算符白名单源码开头用三个Set定义了需要关注的运算符lib/rules/no-unsafe-optional-chaining.jsconst UNSAFE_ARITHMETIC_OPERATORS new Set([, -, /, *, %, **]); const UNSAFE_ASSIGNMENT_OPERATORS new Set([, -, /, *, %, **]); const UNSAFE_RELATIONAL_OPERATORS new Set([in, instanceof]);可以看到关系运算符只包含in与instanceof这两个右侧操作数要求是对象的运算符而、等大小比较对undefined是安全的行为因此不在名单内。2. 核心算法checkUndefinedShortCircuit整条规则的心脏是checkUndefinedShortCircuit(node, reportFunc)函数lib/rules/no-unsafe-optional-chaining.js。它沿 AST 递归穿透若干不会改变短路语义的包装节点直到找到ChainExpression可选链的 AST 节点类型后报告LogicalExpression运算符为||或??时只检查右操作数因为短路时返回的恰是右操作数运算符为时左右两侧都检查SequenceExpression逗号表达式只检查最后一个表达式node.expressions.at(-1)因为逗号表达式的值就是最后一项ConditionalExpression三目检查consequent与alternate两个分支AwaitExpression穿透检查argumentChainExpression命中可选链调用传入的reportFunc报告。例如(obj?.foo ?? obj?.bar)()中callee是LogicalExpression(??)函数只检查右侧的obj?.bar并报告一次——右侧一旦短路为undefined调用必然抛错左侧是否安全已无关紧要。3. 访问者覆盖所有强制解引用位置规则的create(context)返回了一组访问者lib/rules/no-unsafe-optional-chaining.js每一个都对应一种拿到undefined就会出问题的语法位置访问者检测内容说明AssignmentExpression/AssignmentPattern左操作数为解构模式时检查右侧值解构来源不能为undefinedVariableDeclarator声明标识符为解构模式时检查初始化器如const { bar } obj?.fooClassDeclaration/ClassExpression检查superClassextends需要构造器CallExpression仅当调用本身不是可选调用!node.optional时检查calleeobj?.foo()安全(obj?.foo)()危险NewExpression检查calleenew的构造器不能为undefinedMemberExpression仅当属性访问不是可选访问时检查object与括号短路语义对应TaggedTemplateExpression检查tag标签模板的标签必须可调用ForOfStatement检查right可迭代对象要求SpreadElement父节点不是ObjectExpression时检查argument数组/参数展开要求可迭代对象展开允许undefinedBinaryExpressionin/instanceof时检查右操作数开启算术选项时检查两侧见前文运算符表WithStatement检查object需在 script 模式下解析UnaryExpression开启算术选项时-/检查操作数AssignmentExpression开启算术选项时复合赋值检查右操作数两个细节值得注意CallExpression与MemberExpression都通过node.optional判断这一层访问本身是否可选。若调用或访问本身就是?.说明短路保护延续到了最终用途上无需报告——这正是(obj?.foo)?.()能通过检测的底层原因。所有报告都通过meta.messages中定义的unsafeOptionalChain与unsafeArithmetic两条消息发出lib/rules/no-unsafe-optional-chaining.js分别为Unsafe usage of optional chaining. If it short-circuits with undefined the evaluation will throw TypeError.Unsafe arithmetic operation on optional chaining. It can result in NaN.该规则fixable: null不提供自动修复——因为短路可能发生也可能不发生自动改写会改变程序语义只能由开发者手工调整代码。4. 规则的注册方式该规则通过懒加载方式注册在 lib/rules/index.jsno-unsafe-optional-chaining: () require(./no-unsafe-optional-chaining),这也解释了为什么它同时存在于eslint:recommended与eslint:all中作为recommended: true的problem规则它被视为大多数代码都应避免的明确 bug级别。五、测试验证边界行为一览lib/rules/no-unsafe-optional-chaining.js 的 380 余行测试tests/lib/rules/no-unsafe-optional-chaining.js通过RuleTester系统性地固化了规则的边界值得挑选几组说明危险组合的判定逻辑链(obj?.foo obj?.baz).bar会同时报告两处测试断言了第 1 行第 2 列与第 14 列两条unsafeOptionalChain错误印证了两侧都检查的逻辑三目组合(foo ? obj?.foo : obj?.bar).bar同样报告两处错误with语句用例特意设置了languageOptions: { sourceType: script }说明检测with需要非模块环境解构的兜底安全const { foo } obj?.bar || obj?.foo中数组解构版本const [foo] obj?.bar || obj?.foo因||兜底为 valid而未兜底的const [foo] obj?.bar为 invalid。算术选项的边界关闭选项时obj?.foo bar、obj?.foo、baz obj?.foo均为 valid默认不告警开启选项后上述写法全部变为 invalid 且消息为unsafeArithmetic但位运算|、、、逻辑赋值||、、typeof、void、!、~即使开启选项也保持 valid用??/||兜底过的算术表达式如(obj?.foo ?? baz) bar在开启选项后依然 valid——因为checkUndefinedShortCircuit穿透逻辑表达式时只会命中未兜底的链。这些用例一方面验证了源码行为另一方面也是理解哪些写法安全的最佳参考样例。六、在项目中启用与调优该规则随eslint:recommended默认开启为error因此大多数现代 ESLint 项目无需任何额外配置。若需手动声明或调优可参考下面的配置写法// eslint.config.jsflat config export default [ { rules: { // 保持默认仅拦截会抛 TypeError 的硬错误 no-unsafe-optional-chaining: error, // 或同时拦截可能产生 NaN 的算术运算 no-unsafe-optional-chaining: [error, { disallowArithmeticOperators: true }], }, }, ];配置决策建议追求兼容旧行为、改动最小使用默认选项即可eslint:recommended已包含希望根除NaN隐患例如财务计算、数值密集代码开启disallowArithmeticOperators: true代价是obj?.foo bar这类写法需要显式兜底为(obj?.foo ?? 0) bar配合编码规范与no-unused-expressions等规则联动可在团队内形成可选链结果必须兜底或保持链式可选的统一约束。七、总结可选链安全使用的三条准则结合文档示例与源码实现可以把no-unsafe-optional-chaining的判定逻辑浓缩为三条可操作的准则别让括号截断短路链(obj?.foo).bar与obj?.foo.bar语义截然不同前者在短路时抛错若确需括号请在括号外用?.延续保护。强制解引用前必须兜底凡是要调用、取属性、new、解构、展开、for...of、in/instanceof、extends、作为标签模板的可选链结果一律用??/||提供默认值或让用途本身变成可选的。留意静默的NaN默认配置不拦截算术场景若代码中对数值精度敏感应开启disallowArithmeticOperators将隐患显式暴露在静态检查阶段。作为problem级规则no-unsafe-optional-chaining与no-unreachable、no-constant-condition等规则一样捕捉的是代码可运行但必然出错的确定性缺陷。理解其背后的短路语义与 AST 检测模型不仅有助于正确配置也能加深对可选链这一语言特性的整体把握——这正是它在 docs/src/rules/no-unsafe-optional-chaining.md 中被收录为核心文档的价值所在。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价