news 2026/10/21 23:27:47

eslint-plugin-unicorn prefer-dispose 机制解析:如何安全地把 try/finally 资源释放转成 using 声明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eslint-plugin-unicorn prefer-dispose 机制解析:如何安全地把 try/finally 资源释放转成 using 声明

eslint-plugin-unicorn prefer-dispose 机制解析:如何安全地把 try/finally 资源释放转成 using 声明

【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn

eslint-plugin-unicorn 的prefer-dispose是面向 Explicit Resource Management(显式资源管理,using/await using声明)的 ESLint 规则:它识别"资源声明紧邻try、finally只为释放它"的写法,以编辑器建议的形式引导你改写成using声明。规则从不自动修复——因为不借助类型信息,无法确认对象真的实现了Symbol.dispose。

30 秒速览

  • 解决什么问题:手写try/finally释放资源容易漏写、多个资源释放顺序写错,且finally里释放器自己抛异常时会静默覆盖原始错误;using由运行时按逆序自动释放,并在释放器抛异常时用SuppressedError包装、保留原始错误。
  • 触发条件:资源声明紧邻且连续地位于try之前,finally的每条语句都是零参数close()/dispose()/destroy()/end()或[Symbol.dispose]()调用,且释放顺序符合 LIFO 逆序。
  • 输出形式:error级别报告(messageIdprefer-dispose/error)+ 编辑器建议(hasSuggestions: true),无自动修复(meta 里没有fixable)。
  • 默认启用:否。recommended与unopinionated配置均未启用,源码注释写明计划面向 Node.js 24 时再开。
  • 最小适用场景:运行时原生支持using(近期版本 Node.js / Chromium),或项目已用 TypeScript / Babel 转译该语法。
  • 一句话本质:报告先于确认——无类型信息时只给启发式建议;开启 type-aware lint 后,会先确认类型可释放才开口。

为什么需要它

传统写法你肯定见过:

const file = open(); try { read(file); } finally { file.close(); }

它有三个真实痛点:

  • 容易漏:改造try主体时忘了finally,资源直接泄漏;
  • 顺序错:多个资源要手动保证"后开的先关",声明顺序和释放顺序一错,依赖关系就崩了;
  • 错误被吞:file.close()自己抛异常时,try里的原始异常被释放异常直接覆盖,排障看不到根因。

Explicit Resource Management 一次性解决这三点:声明在块退出时自动释放;多资源按声明逆序(LIFO)释放;释放器抛异常时经SuppressedError包装,原始错误保留。该提案已达 TC39 Stage 4,近期版本 Node.js 与 Chromium 原生支持,更老的环境用 TypeScript 或 Babel 转译。

规则的职责因此很聚焦:找出所有能被using无行为变化替换的"纯释放"try/finally。难点全在"无行为变化"这几个字上,下面所有判定都围绕它。

判定边界:它认什么、不认什么

规则入口是TryStatement节点访问(rules/prefer-dispose.js),所有判定收敛为一个问题:这个try/finally能否被using等价替换?

会标记

✅ 单资源、finally仅一条零参数释放调用、声明紧邻其上:

const foo = open(); try { use(foo); } finally { foo.close(); }

✅ 多资源,finally按声明逆序释放(与using的 LIFO 一致),依据 rules/prefer-dispose.js:

const foo = openFoo(); const bar = openBar(); try { use(foo, bar); } finally { bar.destroy(); foo.close(); }

✅ async 函数内await释放,会生成await using,依据 rules/prefer-dispose.js:

async function main() { const foo = open(); try { await use(foo); } finally { await foo.close(); } }

✅ 同步与异步释放混用,同时生成using和await using(测试用例 test/prefer-dispose.js):

try { use(foo, bar); } finally { await bar[Symbol.asyncDispose](); foo.close(); }

✅let声明、catch并存、[Symbol.dispose]()写法、可选链foo?.close()均被识别,依据 rules/prefer-dispose.js:

let foo = open(); try { use(foo); } catch (error) { handle(error); } finally { foo?.close(); }

不会标记

❌var声明:函数作用域会改变using的块级作用域语义,依据 rules/prefer-dispose.js:

var foo = open(); try { use(foo); } finally { foo.close(); }

❌ 解构声明、无初始化器、export声明,依据 rules/prefer-dispose.js:

const {foo} = open(); export const bar = open(); let baz;

❌ 释放调用带参数、计算字符串键,依据 rules/prefer-dispose.js 与 rules/prefer-dispose.js:

} finally { foo.close(true); foo['close'](); }

❌finally混入额外语句、同一资源重复释放,依据 rules/prefer-dispose.js:

} finally { foo.close(); cleanup(); foo.close(); }

❌ 声明与try之间夹了其他语句(不连续),依据 rules/prefer-dispose.js:

const foo = open(); doSomethingElse(); try { use(foo); }

❌ 多资源按声明顺序(而非逆序)释放,建议会改变可观察的清理顺序,依据 rules/prefer-dispose.js:

} finally { foo.close(); bar.destroy(); }

❌ 非 async 上下文(含类静态块)中的异步释放,依据 rules/prefer-dispose.js:

function nonAsync() { try { use(foo); } finally { foo[Symbol.asyncDispose](); } }

❌ 资源在try之后被重新赋值或继续使用,依据 rules/prefer-dispose.js:

try { use(foo); } finally { foo.close(); } log(foo);

内部机制拆解

规则按执行时序分六步,每一步要么缩小候选集,要么直接产出结论。

1. 入口闸门:TryStatement 与 finally 是否存在

输入:AST 访问到的try语句节点。处理:create注册context.on('TryStatement', …)(rules/prefer-dispose.js),findResources先检查finalizer,没有finally立即返回undefined(rules/prefer-dispose.js)。输出:不通过则该节点完全沉默。

为什么放第一步:规则只关心"释放可以被替换",没有finally的try/catch没有可迁移的释放语义,不值得再走后面的解析。

2. 把 finally 解析成"纯释放语句流"

输入:finalizer.body的全部语句。处理:逐条过parseDisposalStatement(rules/prefer-dispose.js),一条语句必须依次满足:是表达式语句;可被await(AwaitExpression)和/或可选链(ChainExpression,用于foo?.close())包裹;是零参数调用(argumentsLength: 0);调用对象是标识符;方法名命中白名单,或是Symbol.dispose/Symbol.asyncDispose计算写法:

const DISPOSAL_METHOD_NAMES = new Set(['close', 'dispose', 'destroy', 'end']);

同层还有两条硬校验:finally出现任何一条非释放语句(含空finally)就整体放弃(rules/prefer-dispose.js);同一资源名出现两次也放弃(rules/prefer-dispose.js)。输出:按finally出现顺序排列的disposals列表。

为什么用白名单而不是"任意方法调用":建议的目标是把这些调用换成Symbol.dispose,方法名只是"这句是释放意图"的线索。若接受任意调用,foo.save()这类清理副作用会被误认成释放,生成错误建议。

一个细节:foo[Symbol.asyncDispose]()即使没写await也会被标记isAwaited = true(rules/prefer-dispose.js),因为它返回 Promise,using语义要求等待其完成。

3. 资源名解析回声明

输入:disposals。处理:resolveResource对每个释放名做作用域查找并校验(rules/prefer-dispose.js):

  • 作用域内恰好一处定义(variable.defs.length === 1,L130);
  • 定义是带初始化器的VariableDeclarator,且声明名为标识符(排除解构)(L135-L142);
  • 关键字是const或let,排除var(L148-L150);
  • 声明语句只有一个声明符,排除const a = 1, b = 2;(L150);
  • 不是export声明(L151);
  • 声明与try位于同一块级作用域(L152)。

输出:含declaration、identifier、variable的resources列表。

这里有个坑:definition.kind由 espree 填充,但在@typescript-eslint/parser下是undefined,源码回退到definition.parent.kind(rules/prefer-dispose.js)。没有这个回退,规则在 TS parser 下会把所有声明误判掉。

4. 可转换性校验:连续性、LIFO、try 之后

输入:resources。处理:areResourcesConvertible顺序执行四项检查(rules/prefer-dispose.js):

  1. 连续性:资源声明必须占据紧邻try之前的一整段连续位置,中间不允许任何其他语句(L171-L177);
  2. LIFO:finally的释放顺序必须等于声明顺序的逆序,否则建议会改变可观察的清理顺序,依赖资源可能被提前破坏(L166-L177);
  3. try 之后:资源的任何写引用(初始化除外)或任何位于try结束位置之后的引用 → 拒绝(L184-L189)。using是块级作用域,try一结束资源即释放;
  4. async 上下文:只要有一个资源是异步释放,try必须位于 async 函数内或模块顶层;isInAsyncContext沿父链上溯,遇StaticBlock直接返回false(静态块不允许await),遇Program则看sourceType === 'module'(rules/prefer-dispose.js)。

输出:布尔值。任一失败 →findResources返回undefined→ 完全不报告。

5. 类型感知确认:资源真的可释放吗

输入:resources+parserServices。处理:有类型信息(parserServices.program为真)时,areAllResourcesDisposable对每个资源调用getTypeAtLocation(identifier),检查属性列表里是否含Symbol.dispose(异步资源检查Symbol.asyncDispose)(rules/prefer-dispose.js)。源码直接匹配 TypeScript 内部转义名,而不调用typeChecker.symbolToString()——后者是已知会引发崩溃的调用之一:

const name = String(symbol.escapedName); return name.startsWith('__@dispose') || (isAwaited && name.startsWith('__@asyncDispose'));

TypeScript 把 well-known symbol 成员存成__@dispose@9这类名字,字符串前缀匹配就够。getProperties()对联合类型解析为公共属性,所以Disposable | {close(): void}这种"有一个成员不可释放"的联合会被正确拦截。

输出:类型检查抛异常时(TypeScript 6 深处对跨模块 symbol 计算Cannot read properties of undefined (reading 'includes')的崩溃),catch兜底返回false,即"视为不可释放"(rules/prefer-dispose.js)。效果:type-aware 模式下,无法确认可释放的资源完全不报告(L196)。

为什么这一步必须存在:没有类型信息时,.close()只是一个名字,而using只认Symbol.dispose;仅有.close()的对象在运行时直接TypeError。宁可漏报,不可误导。

6. 产出报告、跳过建议检查与重写

输入:通过全部校验的resources。处理:消息里的keyword取await using(任一资源isAwaited)或using(L346);报告节点是TryStatement本身(L347)。附suggest之前还有两道检查(rules/prefer-dispose.js):

  • 注释检查:try块与catch处理器会被逐字保留,但finally块及}与finally之间的区域会被删除。存在这些位置的注释 → 只报告、不给建议;声明内部的注释因修复只替换关键字 token 而得以保留,建议照给;
  • 遮蔽检查:没有catch时修复会解包try块合并作用域,若try块内声明了与资源同名的绑定,会与using foo冲突 → 只报告、不给建议(hasShadowingConflict,rules/prefer-dispose.js)。

重写(fixTryStatement,rules/prefer-dispose.js):

  1. 把每个声明的关键字 token 替换为using/await using,并在第一个声明前打开一个新的{块(L269-L276);
  2. 有catch:保留try { … } catch (…) { … },仅删finally,再闭合新块(L294-L306);
  3. 无catch:把try块"解包"进新using块(L308-L330);
  4. 保留文本整体右移一级缩进,但遇到多行模板字面量时放弃重缩进,避免改变其内容(L288-L292);换行符跟随文件原样(CRLF 场景由getLinebreak处理)。

输出:带或不带suggest的problem。

边界情况与保守策略

源码里所有提前退出路径分三类,行为各不相同。

A. 完全不报告(findResources返回undefined,create静默 early-return,rules/prefer-dispose.js):

#路径(行号)静默理由
1无finally(L202)没有可替换的释放语义
2finally含非释放语句 / 空finally(L206-L219)using只能表达纯释放
3同一资源名出现两次(L221-L224)using无法表达重复释放,语义不等价
4try不在BlockStatement/Program直接子级(L226-L229)新块打开位置会改变语句结构
5变量不存在 / 作用域内多处定义(L130)无法确定改写哪条声明
6无初始化器 / 解构 / 非标识符名(L135-L142)无法直接改写为using声明
7var/ 多声明符 /export/ 不在同块(L148-L155)作用域或模块语义会变
8声明不连续 / 释放顺序非 LIFO(L171-L177)会改变可观察的清理顺序
9资源在try后被重赋值或使用(L184-L189)using的块级释放会早于原语义
10异步释放但不在 async 上下文(L191-L193)await using在函数式非 async 上下文是语法错误
11type-aware 下类型不含Symbol.dispose(L196)转换后运行时会抛TypeError

B. 报告但不给建议(返回不带suggest的problem,rules/prefer-dispose.js):

#路径不给建议的理由
1finally内或}与finally之间有注释(L353-L358)修复会删除该区域,注释丢失;声明内的注释不受影响,建议照给
2无catch且try块内声明同名绑定(L359,hasShadowingConflictL250-L259)解包合并作用域后与using声明冲突

C. fallback-catch 兜底:areAllResourcesDisposable整个类型检查包在try/catch里(L109、L122-L124)。TypeScript 6 内部崩溃时规则把资源当作"不可释放"处理,type-aware 模式下即不报告——宁可漏报,不让整次 lint 运行崩溃。源码注释明确:已测试的 TypeScript 版本没有已知输入会触发此崩溃,该分支是纯防御(L120-L121)。

最后一条全局保守策略:规则 meta 里没有任何fixable,只有hasSuggestions: true(rules/prefer-dispose.js)——即使所有条件都确认通过,最终决定也交给你在编辑器里"采纳建议"这一动作。误报的代价是运行时TypeError,这个成本规则不替你承担。

配置与启用

包的 peer dependency 要求 ESLint>=10.4。最小 flat config:

import unicorn from 'eslint-plugin-unicorn'; export default [ { plugins: {unicorn}, rules: {'unicorn/prefer-dispose': 'error'}, }, ];

TypeScript 项目强烈建议同时开 type-aware,让规则验证Symbol.dispose:

import unicorn from 'eslint-plugin-unicorn'; import tsParser from '@typescript-eslint/parser'; export default [ { files: ['**/*.ts'], languageOptions: { parser: tsParser, parserOptions: {projectService: true}, }, plugins: {unicorn}, rules: {'unicorn/prefer-dispose': 'error'}, }, ];

当前默认状态:recommended: false(rules/prefer-dispose.js),recommended与unopinionated均未启用。源码注释给出了未来启用条件:

Enable in therecommendedconfig once we target Node.js 24, whereusing/await usingis supported natively.(rules/prefer-dispose.js)

也就是说,等项目目标运行时升到 Node.js 24,规则会自动进入recommended;在那之前,只能显式开启。⚠️ 没开 type-aware 就显式启用时,规则退化为纯方法名启发式,务必自己核对类型。

协作关系

  • no-using-resource-escape(recommended+unopinionated默认启用):一推一防。prefer-dispose负责把try/finally推向using;no-using-resource-escape负责拦住using声明的资源经return/export/ 函数捕获逃逸出作用域——资源在所属作用域退出时即释放,逃逸出去的引用拿到的是已释放对象。文档见 docs/rules/no-using-resource-escape.md。
  • prefer-top-level-await:本规则在 TS parser 下的kind回退处理(rules/prefer-dispose.js)注释中明确注明参考了该规则,两者共享"不假设definition.kind一定有值"的解析姿势。

实践清单

  • □ 运行时是 Node.js 24+,或项目已用 TS/Babel 转译using→ 显式开启'unicorn/prefer-dispose': 'error';否则别开,建议目标语法你的运行时会直接报错
  • □ TypeScript 项目 → 必须搭配 type-aware lint(projectService/project);无类型信息时规则只认方法名,仅有.close()的对象会被误建议,采纳后运行时TypeError
  • □ 资源需要条件释放,或finally还承担日志、指标等职责 → 不采纳建议,保留手写try/finally,using表达不了这些
  • □ 原finally按声明顺序释放且顺序不可调换 → 保持原样;规则对这种 LIFO 不符的写法本来也不报告
  • □ 建议输出拿不准 → 先核对资源类型是否声明了Disposable/AsyncDisposable(或联合类型里每个成员都声明),存疑就拒绝该建议

速查表

规则名类型默认配置是否自动修复关键源码位置测试文件位置
unicorn/prefer-disposesuggestionrecommended/unopinionated均关闭;计划面向 Node.js 24 时启用(L385-L386)否,仅编辑器建议(L388)解析 L22-L76、校验 L163-L197、重写 L261-L331test/prefer-dispose.js(约 668 行,快照落 test/snapshots/prefer-dispose.js.snapshot);type-aware 用例夹具 test/fixtures/prefer-dispose/tsconfig.json

【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 12:15:49

双电池单元均衡方案:基于MP2642与STM32L4R5的BMS设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华