eslint-plugin-unicornno-useless-undefined规则快照解析:无用的undefined如何被识别与自动修复
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
本文以 test/snapshots/no-useless-undefined.js.md(ESLint 快照测试报告)为骨架,结合规则源码 rules/no-useless-undefined.js 与其完整测试文件 test/no-useless-undefined.js,系统梳理 eslint-plugin-unicorn 中no-useless-undefined规则的全部违规场景、修复行为与设计边界。读完本文,你将能理解该规则在哪些语法位置会报告 "Do not use uselessundefined"、每个场景对应的自动修复(含带注释、带类型断言的极端输入)是如何产生的,以及如何通过checkArguments、checkArrowFunctionBody两个选项控制检查范围。
快照测试是什么:文档的定位
该文档不是传统意义上的"使用手册",而是由 AVA 测试运行器自动生成的快照测试报告(Snapshot report)。文档头三行说明了它的定位:
- 报告的测试文件为
test/no-useless-undefined.js; - 实际快照内容保存在同目录的二进制/文本快照文件
no-useless-undefined.js.snap中; - 快照由 AVA 生成——即每次运行测试时,若规则输出与快照不一致,测试即失败,从而保证规则行为可回归验证。
文档内每个##节对应一条快照用例,结构统一为:输入代码(Input)→ 报错位置与消息(Message)→ 自动修复结果(Output)。其中invalid(1)~invalid(15)是默认 JS 快照组,之后又有 Vue(parsers.vue)与 TypeScript(parsers.typescript)两组独立快照。这三组恰好对应测试文件中三个test.snapshot({...})调用块(见 test/no-useless-undefined.js)。
规则的总体机制:基于 AST 的 Identifier 监听
no-useless-undefined的核心实现并不复杂:它在 rules/no-useless-undefined.js 中监听所有Identifier节点,凡是名称等于undefined的标识符(通过 rules/ast/is-undefined.js 判断node.name === 'undefined'),都会进入一系列父节点类型判断,命中以下场景之一即报告消息Do not use useless \undefined`.(messageId为no-useless-undefined`)。
规则元信息(见 rules/no-useless-undefined.js)显示:
- 规则类型:
suggestion; - 可自动修复:
fixable: 'code'; - 同时提供建议修复(
hasSuggestions: true,用于条件表达式场景); - 推荐级别:
recommended: 'unopinionated'; - 支持语言:
js/js(即 JavaScript,TypeScript 文件里对函数参数的处理另有分支); - 默认选项:
{checkArguments: true, checkArrowFunctionBody: true}。
场景一:函数调用参数中的尾部undefined(CallExpression)
识别逻辑
在 rules/no-useless-undefined.js 中,规则只检查最后一个参数(argumentNodes.at(-1)),且仅当它是undefined且调用者不在忽略名单内时才报告。原因很直观:非尾部的undefined无法删除(删除会改变参数位置语义),只有尾部多余的undefined才等价于"不传"。
从快照可以总结出该场景的几类形态:
| 快照用例 | 输入 | 修复输出 | 说明 |
|---|---|---|---|
| invalid(1) | foo(undefined, bar, undefined, undefined, undefined, undefined,) | 删除最后一个参数及其前的逗号,保留foo(undefined, bar, undefined, undefined, undefined,) | 多参数 + 尾逗号,只处理最后一个undefined |
| invalid(3) | foo(bar, undefined, undefined); | foo(bar, undefined); | 连续两个尾部undefined,每次修复一个,配合--fix可迭代清理 |
| invalid(14) | foo( ((a)), ((undefined)), ((undefined)), ) | foo( ((a)), ((undefined)), ) | 参数带冗余括号也不影响识别 |
| invalid(15) | foo( ((undefined)), ((undefined)), ) | foo( ((undefined)), ) | 同上,逐步收敛 |
修复实现:removeArgument
参数删除不是简单地移除 token,而是由通用修复工具 rules/fix/remove-argument.js 计算删除范围:
- 唯一参数:
fn(undefined)→fn(),同时处理尾逗号(fn(undefined,)→fn()); - 首参数(多参数):删除到其后逗号为止,
fn(a, b)→fn(b)而不是fn( b); - 其他位置:删除其前导逗号,保证
fn(a, b)→fn(a); - 首个参数且删除范围内有注释:通过
replaceTextRange保留注释,只移除逗号前后无注释的部分(见 remove-argument.js)。
快照 invalid(13) 展示了一个来自 webpack 源码的真实案例:
compilation.getDependencyReferencedExports( /** @type {Dependency} */ (connection.dependency), undefined );修复后第二个参数undefined被整体删除(含前面逗号与换行),中间的 JSDoc 类型断言注释完好保留。这验证了删除范围计算对注释安全性的处理。
值得注意的例外:Function#bind与忽略名单
bind特例:foo.bind(undefined)(仅一个参数时)被视为无用的this参数,修复为foo.bind()(快照 invalid(8));而foo.bind(bar, undefined)属于"传入了非undefined的 this 之后的多余参数",同样会报告(快照 invalid(9)~invalid(12) 展示了bind(foo, undefined)、可选链foo.bind?.(bar, undefined)、计算属性foobind等形态)。但源码中isFunctionBindCall明确规定:bind 调用且参数个数不为 1 时直接忽略(no-useless-undefined.js、L443-L446),因为bind的第二个参数开始绑定的是函数实参,语义上不能随意删除。测试中还覆盖了foo.bind(...bar, undefined)、foo?.bind(bar, undefined)等安全忽略的变体(见 test/no-useless-undefined.js)。- 比较/断言类函数忽略名单:
shouldIgnore(no-useless-undefined.js)把undefined作为合法断言值的场景全部排除:is/equal/notEqual/strictEqual/notStrictEqual、toBe/toEqual/toContain/toHaveBeenCalledWith等测试断言,以及array.push/unshift/includes、set.add/has/delete、map.set、React.createContext、useRef、ref(Vue)和/^set[A-Z]/形式的 setter(如setState)。这些调用中undefined是明确要写入/比较的值,删除会改变行为。
场景二:变量声明初始化let a = undefined
规则只针对let/var(VariableDeclarator且声明类型不是const,见 no-useless-undefined.js):
- 快照 invalid(4):
let a = undefined, b = 2;→let a, b = 2;; - 测试补充:
var a = undefined;→var a;,且let a = undefined, b = 2等多声明并列形式都能正确处理(test/no-useless-undefined.js)。
为什么不处理const a = undefined?因为const没有初始化器是语法错误(const a;不合法),所以const a = undefined反而是唯一可写的声明形式,属于有意保留的合法写法(测试的 valid 列表中明确包含'const foo = undefined;',见 test/no-useless-undefined.js)。
场景三:解构默认值{foo = undefined}/[bar = undefined]
AssignmentPattern的右侧是undefined时,修复会删除= undefined部分(no-useless-undefined.js)。快照 invalid(2) 展示:
function foo([bar = undefined] = []) {}→
function foo([bar] = []) {}注意这里有两层默认值:数组参数默认值= []被保留(它不是undefined),只有元素级bar = undefined被移除。测试中还覆盖了const {foo = undefined} = {};、function foo({bar = undefined}) {}、function foo({bar = undefined} = {}) {}等(test/no-useless-undefined.js)。
TypeScript 参数的特殊修复:当解构默认值出现在函数参数中、且是 TypeScript 文件或左侧带类型注解时,修复会在类型注解前插入?,把"带默认值"转为"可选参数"。快照 TS invalid(17) 展示了关键输出:
function a({foo} = undefined) {} // foo.ts→
function a({foo}?) {}对应源码 no-useless-undefined.js:left.typeAnnotation存在或isTypeScriptFile(context.physicalFilename)为真、且left尚未标记optional、且该模式确实是函数参数时,生成两个修复步骤——先删默认值,再插入?。
场景四:return undefined与yield undefined
return undefined:当ReturnStatement.argument是undefined时删除,function foo() {return undefined;}→function foo() {return;}(no-useless-undefined.js)。快照 invalid(5) 展示了最复杂的情形——return与undefined之间隔着多层括号和多处块注释:
return /* */ ( /* */ ( /* */ undefined /* */ ) /* */ ) /* */ ;修复输出把所有括号与undefined一起移除,仅保留注释与换行结构。这得益于修复工具 rules/fix/replace-node-or-token-and-spaces-before.js:它先收集节点外层括号逐层删除,再匹配undefined前的尾随空白(含换行)一起替换为空串,从而不破坏return与后续注释的相对换行关系。
yield undefined:同样删除(function* foo() {yield undefined;}→function* foo() {yield;},见 no-useless-undefined.js)。快照 invalid(6) 复现了带注释与多层括号的 yield 版本,修复逻辑与 return 一致。但yield* undefined是合法而yield*;非法,因此委托 yield 不检查(!parent.delegate条件,且测试 valid 列表中有'function* foo() {yield* undefined;}',见 test/no-useless-undefined.js)。
场景五:箭头函数简写体() => undefined
当箭头函数体直接是undefined表达式(无花括号)时,修复会把=> undefined改写为=> {}(空块),由选项checkArrowFunctionBody控制(no-useless-undefined.js)。快照 invalid(7) 展示带多层括号与注释的输入:
const foo = () => /* */ ( /* */ ( /* */ undefined /* */ ) /* */ );输出中所有括号与undefined被清空,替换为{}(花括号),注释保留在空块前后。这与return场景的"删光括号"策略不同:箭头函数必须有表达式体或块体,所以用空块{}替代,而不是删成() =>(那将是语法错误)。
与 TypeScript 返回类型的联动:修复前规则会检查外层函数的returnType——如果函数显式声明了非undefined/void的返回类型(如(): string),则不报告(checkFunctionReturnType参数,no-useless-undefined.js),因为此时undefined可能是唯一合法的返回表达式。
场景六:索引访问守卫条件表达式(含建议修复)
这是规则最精巧的部分,也是唯一不做自动修复、而提供 suggestion的场景(no-useless-undefined.js)。当三元表达式的某分支是undefined、另一分支是数组/对象索引访问array[index],且测试条件能证明索引安全时,规则建议直接用索引访问替换整个三元表达式:
const foo = index >= 0 ? array[index] : undefined; // 建议修复 → const foo = array[index];快照 invalid(1)~invalid(15) 中虽未直接展示此建议(快照组主要覆盖自动修复场景),但测试文件用大量用例验证了该能力(test/no-useless-undefined.js):
- 下界判断:
index >= 0、index > -1、-1 < index、0 <= index等形式,边界数值可以是常量或带as number等类型断言; - 上界判断:
index <= array.length - 1、index < array.length等基于.length的比较; - 带偏移索引:
index >= 5 ? array[index - 5] : undefined→array[index - 5]; - 安全性约束:索引访问的成员表达式必须是非可选链(
array?.[index]不处理)、无副作用(getArray()[index]不处理)、索引无副作用(array[index++]、array[getIndex()]不处理);测试条件中出现可选链(foo?.bar >= 0)也不处理; - 注释保护:只要三元表达式内部存在任何注释,就不提供建议(避免修复吞掉注释),见 no-useless-undefined.js;
- 类型断言保留:保留分支若带 TS 类型断言(
array[index] as string),建议修复会用分支原文替换,避免断言丢失(no-useless-undefined.js),并视情况补充分号(needsSemicolon处理a()\n...[array][index]这类换行场景)。
实现上通过getIndexedAccess/getIndexedAccessTestValidity结合lowerBoundComparison/upperBoundComparison两张算子表(no-useless-undefined.js)推导不等式方向与边界有效性,并用isSame验证比较操作数与索引表达式为同一 AST 节点,从而杜绝误报。
选项配置
规则 schema 定义于 no-useless-undefined.js,两个布尔选项默认均为true:
| 选项 | 默认值 | 作用 |
|---|---|---|
checkArguments | true | 是否检查函数调用参数中的尾部undefined。设为false时foo(undefined)、foo.bind(undefined)均不再报告 |
checkArrowFunctionBody | true | 是否检查箭头函数简写体() => undefined。设为false时const foo = () => undefined被放行 |
对应的测试用例如 test/no-useless-undefined.js:optionsIgnoreArguments = [{checkArguments: false}]、optionsIgnoreArrowFunctionBody = [{checkArrowFunctionBody: false}]。
配置示例(flat config):
import eslintPluginUnicorn from 'eslint-plugin-unicorn'; export default [ { plugins: {unicorn: eslintPluginUnicorn}, rules: { 'unicorn/no-useless-undefined': ['error', { checkArguments: true, checkArrowFunctionBody: true, }], }, }, ];TypeScript 与 Vue 专项快照
TypeScript 场景
快照 TS invalid(1)~invalid(17) 与测试文件 test/no-useless-undefined.js 对应,涵盖:
- 参数默认值
foo: Type = undefined:函数声明、函数表达式、箭头函数、对象方法、类方法中均会修复,TypeScript 文件中统一转为可选参数foo?: Type(快照 invalid(1)(6)(7)(8)(9)); foo?: Type = undefined:可选参数再加默认值纯属冗余,修复为foo?: Type(快照 invalid(2));- 返回类型为
undefined/void时的return undefined:会报告并修复(function shouldBeFlagged(): undefined {return undefined;}→{return;}),包括类方法、getter、静态方法、私有方法等(快照 TS 组的 invalid 系列与 test/no-useless-undefined.js); - 联合返回类型豁免(#880):当返回类型是
string | undefined、number | void、undefined | number等含真实值类型的联合类型时,return undefined有意保留(见 no-useless-undefined.js 的isUndefinedOrVoidReturnType:只匹配TSUndefinedKeyword/TSVoidKeyword纯类型,联合类型不匹配);any、unknown、never、Promise<void>同理不报告(test/no-useless-undefined.js); - TS 文件中的函数参数不检查:
.ts/.tsx/.mts/.cts文件中foo(undefined)、Promise.resolve(undefined)等调用全部放行(no-useless-undefined.js),因为删除参数可能影响泛型推断与重载解析。测试验证了不同扩展名(含foo.MTs大小写变体)下的行为一致(test/no-useless-undefined.js); - 默认值修复与返回类型无关:
function f(foo = undefined): string {}这种"冗余默认值 + 有值返回类型"的组合仍会报告(快照 invalid(3)),因为外层返回类型与参数默认值无关。
Vue 场景
快照 Vue invalid(1) 对应 test/no-useless-undefined.js:在<script>块中nextTick(undefined)会被修复为nextTick();而ref(undefined)、vue.ref(undefined)属于忽略名单中的 Vueref,不报告。
快照到行为的一致性验证
将快照文档与测试源码对照可以发现:每一条快照输出都严格对应测试文件中test.snapshot块的输入。例如快照 invalid(1) 的多行调用即 test/no-useless-undefined.js 的 outdent 模板;TS invalid(1)~invalid(17) 对应 test/no-useless-undefined.js。这说明该.md快照报告是规则行为最忠实的"可读快照"——想要快速了解规则在当前版本下会报什么、修成什么样,直接查阅这份文档即可;而想要理解每一步"为什么这样修",则需结合 rules/no-useless-undefined.js 与其依赖的 rules/fix/remove-argument.js、rules/fix/replace-node-or-token-and-spaces-before.js 两个修复工具。
总结:一条规则的两面
no-useless-undefined的自动修复覆盖五类高频冗余写法——尾部调用参数、let/var初始化、解构默认值、return/yield、箭头函数简写体;另有一类"条件索引访问"仅提供安全建议修复。它的设计哲学是宁可少报不可误报:const a = undefined、断言类函数参数、bind多参、TS 联合返回类型、TS 文件的函数参数、带注释的三元表达式等场景全部被刻意豁免,因为删除它们要么是语法错误,要么会改变运行语义。理解这份快照报告,等于拿到了该规则在纯 JS、TypeScript、Vue 三种环境下的完整行为矩阵,无论是评估是否启用规则、排查误报,还是为二次开发编写回归测试,都能从中直接受益。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考