十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESLint require-atomic-updates 规则深度解析:检测 async/generator 中的竞态条件赋值

ESLint require-atomic-updates 规则深度解析:检测 async/generator 中的竞态条件赋值 ESLint require-atomic-updates 规则深度解析检测 async/generator 中的竞态条件赋值【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslintrequire-atomic-updates是 ESLint 内置的一条 problem 级规则用于检测在async函数与generator函数中因await/yield暂停执行而导致的基于过期值进行赋值non-atomic update这类竞态条件race conditionBug。本文以官方文档为骨架结合本仓库中 规则源码 与 完整测试用例从问题原理、规则行为、配置选项到底层代码路径分析实现为你完整讲透这条规则帮助你在项目中正确启用并规避异步赋值陷阱。问题背景异步代码中的竞态条件编写异步代码时很容易产生微妙的竞态条件 Bug。官方文档给出的开篇示例非常典型let totalLength 0; async function addLengthOfSinglePage(pageNum) { totalLength await getPageLength(pageNum); } Promise.all([addLengthOfSinglePage(1), addLengthOfSinglePage(2)]).then(() { console.log(The combined length of both pages is, totalLength); });这段代码看起来会把getPageLength(1)和getPageLength(2)的结果累加起来但实际上最终totalLength的值只会是其中某一页的长度。Bug 出在totalLength await getPageLength(pageNum);这一句上该语句首先读取totalLength的初始值然后调用getPageLength(pageNum)并等待这个 Promise 兑现此时函数被挂起最后把await getPageLength(pageNum)的结果与初始读取的totalLength相加并写回。如果在getPageLength(pageNum)的 Promise 挂起期间totalLength被另一个函数调用更新了那么那次更新就会丢失——因为新值被覆盖时根本没有把最新的值读进来。两个并发调用各自都基于同一个旧值0做累加第二次写回覆盖了第一次的结果。两种修复思路思路一保证读取与更新同时发生——先拿到异步结果再在同步代码里完成读写使读写之间没有等待点async function addLengthOfSinglePage(pageNum) { const lengthOfThisPage await getPageLength(pageNum); totalLength lengthOfThisPage; }思路二干脆不用可变的共享变量改用纯函数式的方式聚合结果Promise.all([getPageLength(1), getPageLength(2)]).then(pageLengths { const totalLength pageLengths.reduce((accumulator, length) accumulator length, 0); console.log(The combined length of both pages is, totalLength); });这两种修复方式的共同点正是require-atomic-updates规则所鼓励的不要让基于旧值的写回跨过await/yield暂停点。所谓 atomic updates原子更新就是指变量的读取与写回之间不存在可能让外部并发修改介入的间隙。Rule Details规则检测目标该规则的目标是报告那些可能基于过期值outdated values进行的对变量或属性的赋值。规则的meta.type为problem见 规则元数据意味着它被设计用来标记会导致实际 Bug 的代码模式。它不对recommended配置默认启用recommended: false也不提供自动修复fixable: null因为它揭示的是语义层面的并发隐患无法机械改写。启用方式扁平配置 flat configexport default [ { rules: { require-atomic-updates: error } } ];也可以直接用 eslintrc 风格的数组形式传入选项见下文 Options 一节/* eslint require-atomic-updates: [error, { allowProperties: true }] */变量的检测逻辑三步执行流针对变量规则在检测到以下执行流时报告赋值变量被读取一个yield或await挂起函数函数恢复执行后向第 1 步中的那个变量赋值。第 3 步的赋值会被报告因为它可能被错误地解析第 1 步读取的变量值在第 2 步到第 3 步之间可能已经改变。尤其是当该变量可从其他执行上下文访问时例如它不是局部变量其他函数可以修改它变量值完全可能在函数被挂起的期间被别处改掉。规则不会报告第 3 步赋值的情况如果在第 2 步和第 3 步之间变量又被读取了一次重新读取会刷新过期标记详见下文源码解析如果变量在函数挂起期间不可能被外部访问例如它是局部变量。不正确的代码示例/* eslint require-atomic-updates: error */ let result; async function foo() { result await something; } async function bar() { result result await something; } async function baz() { result result doSomething(await somethingElse); } async function qux() { if (!result) { result await initialize(); } } function* generator() { result yield; }注意qux这个例子if (!result)读取了result随后await initialize()挂起函数恢复后直接给result赋值——赋值前没有重新读取因此同样被标记。测试用例中与之对应的一条是 tests/lib/rules/require-atomic-updates.js 里的let foo; async function x() { foo (a ? b : foo) await bar; if (baz); }——只要旧值参与运算且中间跨越了await无论嵌套多深都会被报告。正确的代码示例/* eslint require-atomic-updates: error */ let result; async function foobar() { result await something result; } async function baz() { const tmp doSomething(await somethingElse); result tmp; } async function qux() { if (!result) { const tmp await initialize(); if (!result) { result tmp; } } } async function quux() { let localVariable 0; localVariable await something; } function* generator() { result (yield) result; }逐个解读这些正确写法foobarresult await something result的求值顺序是先等await something完成再读取result并相加读与写之间没有暂停点是原子更新baz先用局部变量tmp接住异步结果随后在同步代码中result tmp读写之间无暂停quxawait initialize()之后重新读取result做二次检查确认条件仍然成立才赋值quuxlocalVariable是函数内局部变量外部无法在挂起期间修改它规则判定其值不会过期generator(yield) result在yield恢复后才读取result同样是原子更新。属性的检测逻辑更严格的规则针对属性规则通过变量检测到以下执行流时报告赋值变量或对象属性被读取一个yield或await挂起函数函数恢复执行后向某个属性赋值。该逻辑与变量的检测逻辑类似但更加严格第 3 步被赋值的属性不必与第 1 步读取的属性是同一个——规则假设整个流程依赖的是对象整体状态只要对象曾被读取过、期间又发生过挂起之后再写任何属性都可能基于过期状态。不正确的代码示例/* eslint require-atomic-updates: error */ async function foo(obj) { if (!obj.done) { obj.something await getSomething(); } }这里obj.done被读取await挂起恢复后直接写obj.something。即使something与done不是同一个属性规则依然认为对象的整体状态可能在挂起期间发生了变化从而报告。测试用例中的foo.bar await baz、foo[bar].baz await result以及私有属性foo.#bar await baz都对应这一分支见 测试文件 中STATIC_PROPERTY_ERROR、COMPUTED_PROPERTY_ERROR、PRIVATE_PROPERTY_ERROR三个断言常量说明静态属性、计算属性和私有字段都在检测范围内。正确的代码示例/* eslint require-atomic-updates: error */ async function foo(obj) { if (!obj.done) { const tmp await getSomething(); if (!obj.done) { obj.something tmp; } } }先取得异步结果存入局部变量然后在同步代码中重新读取obj.done确认状态未变再执行赋值——读与写之间没有暂停点赋值基于最新状态。OptionsallowProperties 选项该规则接受一个对象选项allowProperties设为true时规则不再报告对属性的赋值。默认值为false。选项的默认值与 schema 定义都写在 规则源码 中defaultOptions: [{ allowProperties: false }]且 schema 明确allowProperties为boolean类型、不允许其他附加属性。在{ allowProperties: true }下下面的代码会被视为正确/* eslint require-atomic-updates: [error, { allowProperties: true }] */ async function foo(obj) { if (!obj.done) { obj.something await getSomething(); } }从测试用例可以看出该选项的语义边界开启后if (foo.bar) { foo.bar await something; }不再报错测试 valid 分支但对变量的非原子赋值仍然会报错——allowProperties只放行属性赋值不影响变量分支测试let foo; async function a() { if (foo) { foo await something; } }配合{ allowProperties: true }依然报nonAtomicUpdate传options: [{}]或{ allowProperties: false }时属性赋值照常报告说明默认值与显式 false 行为一致。源码实现原理基于代码路径分析的过期读追踪规则并不做运行时分析而是完全基于 ESLint 的**代码路径分析Code Path Analysis与作用域引用Scope Reference**机制在静态层面推演竞态。理解它有助于把握规则的精确行为边界。核心实现位于 lib/rules/require-atomic-updates.js整体分四层1. 引用收集createReferenceMapcreateReferenceMap源码 L14-L29从当前函数作用域出发递归遍历所有子作用域跳过嵌套函数因为嵌套函数内部的引用属于别的执行上下文把已解析变量的标识符节点映射到对应的Reference对象形成referenceMap。只有reference.resolved ! null的引用才纳入映射——未解析的全局变量如浏览器环境里隐式创建的无法参与跨上下文判定会被跳过。该规则只对async或generator函数作用域建立referenceMapscope.block.async || scope.block.generator判断见 L227-L240因为只有这两种函数才可能被await/yield挂起后恢复。2. 写入表达式识别getWriteExprgetWriteExprL37-L58负责从引用反推写入表达式。对a.b await a这类写法它沿 MemberExpression 链向上寻找AssignmentExpression的 RHS——这是为了把foo.bar await foo中的foo读取与写入关联起来从而正确处理成员访问场景。3. 局部性判定isLocalVariableWithoutEscapeisLocalVariableWithoutEscapeL66-L81判断一个变量是否局部且不逃逸只有当变量的所有引用都来自同一个函数作用域reference.from.variableScope functionScope时才判定为局部一旦有任何闭包或其他函数引用了它就认为外部可能修改需要警惕。对属性访问场景若变量是函数参数直接视为非局部外部持有对象引用即可改属性。这条判定直接对应文档中的例外条款如果变量在函数挂起期间不可能被外部访问例如它是局部变量则不报告——测试用例async function x() { let foo; foo await bar; }位于 valid 列表就是证明。4. 代码路径上的过期追踪SegmentInfoSegmentInfo类L86-L172是整个规则最核心的数据结构。ESLint 代码路径分析把函数体拆成若干代码段segments每个 segment 记录两组变量集合freshReadVariables在本段刚刚被读取仍然新鲜的变量outdatedReadVariables已被判定为过期读的变量。沿控制流传播时initialize从所有前驱 segment 合并两组集合markAsRead变量被重新读取时加入freshReadVariables同时从outdatedReadVariables中删除——这正是两次读取之间重新读取则不报的机制makeOutdated遇到AwaitExpression/YieldExpression的:expression:exit时把所有freshReadVariables整体转移为outdatedReadVariables——挂起点就是新鲜变过期的转折点isOutdated检查当前 segment 中变量是否处于过期集合是则报告。最终在:expression:exitL310-L362中统一执行验证与报告按目标区分两条消息nonAtomicUpdatePossible race condition:{{value}}might be reassigned based on an outdated value of{{value}}.变量场景nonAtomicObjectUpdatePossible race condition:{{value}}might be assigned based on an outdated state of{{object}}.属性场景。测试用例 tests/lib/rules/require-atomic-updates.js 中共 472 行覆盖了普通赋值、复合赋值、三元表达式嵌套、循环while、闭包引用、generator、async generator、私有字段、计算属性、try/catch恢复路径以及process.exitCode这类全局对象属性等场景。其中还有两条针对性能的回归测试对应 issue #10893大量连续if分支与大量嵌套三元表达式不得导致指数级耗时——这得益于 SegmentInfo 的集合合并设计避免了重复遍历。规则的演进背景与项目中的位置从仓库数据可以确认该规则的版本沿革与地位规则于 ESLintv5.3.0引入见 docs/src/_data/rule_versions.json 中require-atomic-updates对应的版本号在 升级到 6.0.0 的迁移指南 中明确说明该规则用于报告 async 函数中重新赋值变量可能产生的竞态条件 Bug并在 6.0 版本中默认保持off——因为它在某些场景如process.exitCode赋值会产生误报需要用户结合代码实际语义决定是否启用规则被收录进eslint:all配置require-atomic-updates: error见 packages/js/src/configs/eslint-all.js但不在eslint:recommended中——这一点从源码recommended: false与tests/conf/eslint-recommended.js中无此规则均可印证规则类型声明位于 lib/types/rules.d.ts提供Linter.RuleEntry级别的类型提示。When Not To Use It何时不需要启用官方文档的结论很直接如果你不使用 async 函数或 generator 函数就不需要启用这条规则。同步代码中不存在挂起后恢复的执行模型也就不存在基于过期值的写回问题。即便使用了 async/generator也建议先结合上面的allowProperties选项评估如果项目中大量存在读取对象状态 → await → 写该对象属性的惯用法且经过人工审查确认安全可以通过allowProperties: true关闭属性分支仅保留对变量赋值的检测从而在降低误报的同时保住对最危险模式共享可变变量的非原子累加的监控。配置速查配置形态写法默认启用严格模式require-atomic-updates: error仅关闭属性检测require-atomic-updates: [error, { allowProperties: true }]完全关闭require-atomic-updates: off在扁平配置flat config中直接写入rules对象即可在旧式.eslintrc中写入rules字段效果一致。该规则无需额外依赖属于 ESLint 内置核心规则规则注册入口见 lib/rules/index.js。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表