ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

eslint-plugin-unicorn 规则快照测试解析:从 `no-useless-recursion` 快照报告读懂递归检测与回归验证

2026/9/19 6:41:39 拓冰建站 浏览量
eslint-plugin-unicorn 规则快照测试解析:从 `no-useless-recursion` 快照报告读懂递归检测与回归验证 eslint-plugin-unicorn 规则快照测试解析从no-useless-recursion快照报告读懂递归检测与回归验证【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicornno-useless-recursion是 eslint-plugin-unicorn 中一条recommended级别的建议型规则用于发现可以被循环替代的简单尾递归函数。本篇文章以仓库内的快照报告 test/snapshots/no-useless-recursion.js.md 为骨架逐条还原其测试输入与期望错误输出并结合 规则源码、测试用例 与 规则文档讲清该规则“判定什么、放过什么、为什么”以及快照测试在 ESLint 规则开发中的作用。读完你将能复现该规则的测试链路并理解如何在自定义 ESLint 规则中写出可回归、可审查的快照测试。快照报告是什么AVA 快照测试的产物test/snapshots/no-useless-recursion.js.md是由 AVA 测试框架自动生成的快照报告对应二进制快照文件为同目录下的no-useless-recursion.js.snap无法用 UTF-8 直接读取报告即其人类可读版本。它记录了test/no-useless-recursion.js中test.snapshot(...)每次运行的输入与输出用于在后续迭代中做回归比对报告头部标注了快照来源test/no-useless-recursion.js、实际快照文件位置以及生成工具 AVAGenerated by AVA每个用例块以invalid(n)编号包含 Input被 lint 的源码和 Error 1/1期望的报错信息含错误行、错误列与 Message。快照测试的价值在于一旦规则行为发生非预期变化npm test会因快照不匹配而失败开发者可据此审查改动是否有意为之。这正是规则回归测试的“合同”。六条 invalid 用例规则到底抓什么快照报告完整保留了 6 条无效invalid用例的输入与期望输出它们共同刻画了规则的触发边界。以下逐一还原并补充规则上下文。用例 1条件尾调用最典型场景输入对应 test/no-useless-recursion.js 中第 168–176 行的invalid用例function foo(bar) { if (bar.baz) { return foo(bar.baz); } return bar; }期望输出仅在第 3 行foo(bar.baz)的foo标识符处报错^^^指向第 3 行第 8 列Message 为Use a loop instead of this recursive function call.这是典型的“沿链表/树向下遍历”写法函数只在return位置直接调用自身完全可以用while改写。规则文档 docs/rules/no-useless-recursion.md 给出了对应的正确写法// ✅ function foo(bar) { while (bar.baz) { bar bar.baz; } return bar; }用例 2命名函数表达式输入const bar function foo(value) { return foo(value.next); };function foo(...)是命名函数表达式Named Function Expression其函数名foo仅在函数体内可见。规则依然能识别该自引用说明其作用域解析findVariable对函数表达式同样成立。用例 3多参数尾调用输入function foo(first, second) { return foo(first.next, second.next); }即使递归发生在多个参数的变换上只要return处直接调用自身仍被判定为可循环化如while中同时推进两个变量。用例 4无参数无限递归输入function foo() { return foo(); }这是“死循环式”自调用同样命中。注意规则并不检查递归是否真的能终止只关心“是否可直接用循环表达”。用例 5async 函数中的尾调用输入async function foo(value) { if (value) { return foo(value.next); } return value; }async function的尾调用同样会被报告说明规则对 async 函数没有豁免源码中只排除了 generator见下文。用例 6TypeScript 语法下的尾调用快照报告的第二段编号重新从invalid(1)开始对应 test/no-useless-recursion.js 第 204–218 行使用 TypeScript parser 的独立测试组function foo(bar: Bar) { return foo(bar.baz as Bar); }期望同样在第 2 行报错。这证明规则通过meta.languages: [js/js]与测试中的languageOptions.parser配置可在 TypeScript 代码上正常工作。规则源码六步判定“无用递归”快照中的每条期望输出都不是凭空而来而是由 rules/no-useless-recursion.js 中isUselessRecursion(returnStatement, sourceCode)的判定结果决定。该函数对每个ReturnStatement依次检查必须是直接函数调用argument.type CallExpression且非可选调用argument.optional为假排除return foo?.(value)且callee是Identifier排除return object.foo(value)必须处于具名函数内向上遍历祖先节点找到最近的函数节点且functionNode.id存在——因此const foo () foo(value)匿名箭头函数和const foo function (value) {...}匿名函数表达式都被放过排除 generator!functionNode.generator所以function * foo(value) { return foo(value); }不报告generator 有惰性语义不能用普通循环替换排除 try 语句包裹!isInsideTryStatement(...)因为 try/finally 中的return涉及finally清理语义改循环会改变行为排除using声明场景!hasUsingDeclarationBefore(...)若函数体内或所在循环头部存在using/await using声明ES 显式资源管理递归时资源会被正确释放改写为循环可能破坏该语义确认是真正的自引用调用名与函数名一致且通过eslint-community/eslint-utils的findVariable做作用域解析确认该标识符确实绑定到当前函数而非被遮蔽的同名变量——源码中isSelfReference正是variable?.identifiers.includes(functionNode.id)。只有全部通过才会返回{node: returnStatement.argument.callee, messageId: MESSAGE_ID}即在调用处的callee节点上报告快照中见到的 MessageUse a loop instead of this recursive function call.规则元信息源码第 107–120 行标明type: suggestion、docs.recommended: true与 readme.md 规则清单中“推荐配置 ✅”的标注一致。valid 用例规则的边界与豁免快照报告只收录 invalid 输出但配套测试 test/no-useless-recursion.js 的valid数组第 7–166 行完整记录了规则“不抓”的情形与源码判定一一对应场景示例豁免原因已用循环实现while (bar.baz) { bar bar.baz; }本就不存在递归回调中递归return bar.map(baz foo(baz));非return处直接自调用非尾调用return 1 foo(value);调用不是return的整个参数条件式递归return value ? foo(value.next) : value;调用在条件表达式中非直接返回嵌套函数function bar() { return foo(value); }自调用不发生在当前函数返回位同名遮蔽const foo () value; return foo();findVariable判定非本函数绑定可选调用return foo?.(value);argument.optional为真属性调用return object.foo(value);callee非Identifier匿名函数const foo value foo(value);/const foo function (value) {...}函数无idgeneratorfunction * foo(value) { return foo(value); }generator为真对象/类方法object.foo() {...}/class Foo { foo() {...} }方法无函数名绑定且object.foo(value)非 Identifier 调用try/finally、try/catchtry { return foo(value.next); } finally { cleanup(value); }try 包裹豁免using/await using函数体或for头部声明资源资源释放语义这也印证了规则文档中的设计说明“This rule intentionally only reports direct returned self-calls in named functions. More complex recursion can be useful and is left alone.”——它只针对“简单到可以直接改写为循环”的尾调用刻意放过多分支、非尾位置等更复杂的递归。如何复现快照测试快照本身是 AVA 自动生成的无需手写。在仓库根目录执行相关测试即可# 运行单条规则测试含快照比对与更新交互 npx ava test/no-useless-recursion.js # 快照不匹配时使用 AVA 的 -u 更新快照需人工确认改动是有意的 npx ava test/no-useless-recursion.js -u注意仓库为只读环境建议仅在本地克隆后运行修改快照前务必核对报告中的输入与期望输出是否与规则意图一致。小结通过这份快照报告可以完整还原no-useless-recursion规则的“可测行为”检测对象具名函数含命名函数表达式、async 函数中return位置直接调用自身的简单尾递归判定核心CallExpression Identifier callee 名字匹配 findVariable 自引用确认六大豁免非尾位置调用、可选/属性调用、匿名函数、generator、try 包裹、using资源声明修复建议改写为while/for循环规则为 suggestion 型不提供自动 fix。快照测试将上述行为固化为可回归的“合同”任何源码改动都必须与之对齐。对于想要为自定义 ESLint 规则添加测试的开发者test/no-useless-recursion.js test/snapshots/no-useless-recursion.js.md这套组合本身就是一份高质量范本输入输出分离、快照可审查、边界用例完备值得直接借鉴。【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考