eslint-plugin-unicorn 规则实战:从快照测试看 prefer-simple-sort-comparator 如何把啰嗦的比较器简化为减法
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
eslint-plugin-unicorn 是内置 300+ 条 ESLint 规则的 JavaScript 风格检查插件,其中prefer-simple-sort-comparator专门解决Array#sort()中冗长的比较器写法问题。本文以规则仓库中的 AVA 快照文件 test/snapshots/prefer-simple-sort-comparator.js.md 为主线,逐条拆解 31 个非法用例的报错信息与修复建议,并结合 规则源码 和 官方规则文档 讲解其底层判定逻辑。读完本文,你将理解该规则能识别哪些比较器形态、为何只给建议(suggestion)而不做自动修复,以及它在 BigInt 类型数组、多键比较器等边界场景下的处理策略。
一、规则背景与快照文件的结构
1.1 规则要解决的问题
Array#sort()(以及 ES2023 新增的Array#toSorted())允许传入比较器函数,决定元素的排列顺序。很多开发者习惯用一连串if或三元表达式手写比较逻辑:
array.sort((a, b) => { if (a > b) { return 1; } if (a < b) { return -1; } return 0; });当比较器的两个操作数互为“镜像”(如a与b、a.foo与b.foo)时,整个逻辑等价于一次减法运算:
array.sort((a, b) => a - b);prefer-simple-sort-comparator规则的作用正是发现这类可简化的比较器,并给出替换为减法的建议。按官方文档 docs/rules/prefer-simple-sort-comparator.md 的说明,该规则:
- 在
recommended与unopinionated两种配置中默认启用(见 docs/rules/prefer-simple-sort-comparator.md 顶部标注); - 只提供编辑器可手动应用的“建议”(suggestion),从不自动修复;
- 刻意不报告多键比较器、基于
Math.random()的乱序函数以及使用了可选链操作数的比较器。
规则在 rules/index.js 中注册导出,其meta声明了type: 'suggestion'、hasSuggestions: true,消息文本为 “Prefer a simple comparison function forArray#sort().”。
1.2 快照文件的格式解读
test/snapshots/prefer-simple-sort-comparator.js.md 是由 AVA 测试框架(avajs.dev)自动生成的快照报告,标题注明其对应的测试文件为test/prefer-simple-sort-comparator.js,实际二进制快照保存在同目录的prefer-simple-sort-comparator.js.snap中。
每个invalid(n)小节包含三部分信息:
- Input:被测试的原始代码(带行号);
- Error 1/1:规则报告的出错位置(
^波浪线标出)与消息文本; - Suggestion 1/1:规则建议的替换结果,例如 “Replace with
(a, b) => a - b.”
31 个用例完整覆盖了规则能识别的所有比较器语法形态,下面按类别逐一解析。
二、31 个快照用例全解析
2.1 单层三元表达式(invalid 1~5)
最基础的形态是单层三元表达式,操作数顺序与比较方向各有变化:
| 用例 | 原始代码 | 建议替换 |
|---|---|---|
| invalid(1) | (a, b) => a > b ? 1 : -1 | (a, b) => a - b |
| invalid(2) | (a, b) => a < b ? -1 : 1 | (a, b) => a - b |
| invalid(3) | (a, b) => b > a ? 1 : -1 | (a, b) => b - a |
| invalid(4) | (a, b) => a > b ? -1 : 1 | (a, b) => b - a |
| invalid(5) | (a, b) => a < b ? 1 : -1 | (a, b) => b - a |
对照快照可见,规则并不死板——它同时识别:
- 参数交换:
b > a ? 1 : -1与a > b ? 1 : -1语义相同,但替换时保留原始操作数顺序(b - a); - 降序方向:
a > b ? -1 : 1是降序,规则会翻转减法方向生成b - a; - 不等号方向:
a < b ? -1 : 1的符号约定被正确换算为a - b。
2.2 嵌套三元与相等分支(invalid 6~7)
当比较器需要区分相等情况时,会出现嵌套三元:
array.sort((a, b) => a > b ? 1 : a < b ? -1 : 0);快照 invalid(6) 显示其建议为(a, b) => a - b。同理,a < b ? -1 : a > b ? 1 : 0(invalid(7))也被简化为a - b。嵌套的三元树只要整体符号分布与减法一致,就被判定为可简化。
2.3>=与<=(invalid 8~10)
规则同样覆盖非严格不等号:
a >= b ? 1 : -1→a - b(invalid 8);a <= b ? -1 : 1→a - b(invalid 9);a >= b ? -1 : 1→b - a(invalid 10,降序)。
由于a >= b ? 1 : -1与a > b ? 1 : -1对数字排序结果等价,规则对>、>=、<、<=四种关系运算符一视同仁——这一点可以从源码中relationalOperators = new Set(['>', '<', '>=', '<='])得到印证(见 rules/prefer-simple-sort-comparator.js)。
2.4 块体if语句(invalid 11、17~19)
比较器同样可以写成块体 +if的形式:
两分支if(invalid 11):
array.sort((a, b) => { if (a > b) { return 1; } return -1; });快照给出的建议是(a, b) => a - b。
if链 + 兜底return 0(invalid 17):
array.sort((a, b) => { if (a > b) { return 1; } if (a < b) { return -1; } return 0; });→(a, b) => a - b;invalid(18) 的降序版本(if (a > b) return -1; if (a < b) return 1; return 0;)→(a, b) => b - a。
if/else if/else(invalid 19):
array.sort((a, b) => { if (a > b) { return 1; } else if (a < b) { return -1; } else { return 0; } });同样被识别为等价于(a, b) => a - b。
2.5 成员表达式操作数(invalid 12~15)
比较器的操作数不限于参数本身,只要两侧互为镜像即可:
a.foo > b.foo ? 1 : -1→a.foo - b.foo(invalid 12);a[0] > b[0] ? 1 : -1→a[0] - b[0](invalid 13,下标字面量);a[i] > b[i] ? 1 : -1→a[i] - b[i](invalid 14,下标变量);a.foo.bar < b.foo.bar ? -1 : 1→a.foo.bar - b.foo.bar(invalid 15,多层链式访问)。
快照对这些用例的替换结果完整保留了成员访问路径,体现了源码中isParameterSwap的递归判定能力(见 3.2 节)。
2.6 带一元正号的字面量(invalid 16)
array.sort((a, b) => a > b ? +1 : -1);+1这种带显式一元正号的数值字面量也被识别(invalid 16),建议为(a, b) => a - b。源码中的isSignedNumericLiteral工具函数专门处理这种情况:它既接受纯数值字面量,也接受带+/-一元符号的数值字面量。
2.7function表达式(invalid 20)
比较器不一定是箭头函数:
array.sort(function (a, b) { return a > b ? 1 : -1; });invalid(20) 显示规则同样报告,并且建议总是改写为箭头函数:(a, b) => a - b。
2.8toSorted()(invalid 21)
规则的检测范围覆盖toSorted:
array.toSorted((a, b) => a > b ? 1 : -1);invalid(21) 的替换结果为(a, b) => a - b。在源码中,isMethodCall(callExpression, {methods: ['sort', 'toSorted'], argumentsLength: 1})同时匹配这两个方法(见 rules/prefer-simple-sort-comparator.js)。
2.9 括号包裹的操作数(invalid 22)
array.sort((a, b) => (a) > (b) ? 1 : -1);多余的括号不会阻碍识别(invalid 22),建议为(a, b) => a - b。
2.10 函数体内含注释(invalid 23)
array.sort((a, b) => { // Compare return a > b ? 1 : -1; });invalid(23) 是一个特例:规则照常报告错误,但快照中没有 Suggestion 部分。原因在源码中有明确注释:“Replacing the whole function would drop any comments inside it, so only suggest when there are none.”(替换整个函数会丢弃其中的注释,因此仅在无注释时才给出建议)。判断依据是sourceCode.getCommentsInside(comparator).length === 0。
2.11 TypeScript 场景(invalid 24~26)
规则支持带类型注解的比较器:
(a: number, b: number): number => a > b ? 1 : -1→(a, b) => a - b(invalid 24,建议会剥离类型注解);function f(foo: number[]) { foo.sort(...) }(invalid 25)——通过类型信息确认foo是数组后,依然报告;function f(foo: Int8Array) { foo.sort(...) }(invalid 26)——类型化数组(typed array)复用Array#sort()的语义,同样报告。
这些用例需要启用 TypeScript 解析器(测试代码中的parsers.typescript,见 test/prefer-simple-sort-comparator.js)。
2.12 BigInt 类型化数组(invalid 27~31)
快照的最后五个用例全部针对 BigInt 类型化数组:
new BigInt64Array().sort(...)(invalid 27);new BigUint64Array().sort(...)(invalid 28);BigInt64Array.from([]).sort(...)(invalid 29);BigUint64Array.of(1n).sort(...)(invalid 30);function f(values: BigInt64Array | Int8Array) { values.sort(...) }(invalid 31,联合类型)。
这组用例只报告错误、不给建议(快照中均无 Suggestion 部分),原因非常关键:BigInt 之间执行减法会抛出TypeError,因此不能机械地把比较器改写为a - b。源码通过isKnownBigIntTypedArray(callExpression.callee.object, context)判断接收者是否为 BigInt 类型化数组,命中时抑制建议(见 rules/prefer-simple-sort-comparator.js)。该判定实现在 rules/utils/is-array.js 中,它同时覆盖类型注解(BigInt64Array)与构造调用(new BigInt64Array())两种拼写方式。
三、源码级实现原理
快照展示的是“输入 → 输出”的观测结果,其背后的判定逻辑全部集中在 rules/prefer-simple-sort-comparator.js 中,大致分四步。
3.1 前置筛选:只关心合法的 sort/toSorted 调用
create函数监听CallExpression,通过isMethodCall限定方法名为sort或toSorted且恰好一个参数;随后校验比较器必须是普通(非 async、非 generator)函数且恰好两个Identifier类型的形参。任何不符合条件的调用直接放行,这正是快照之外那些“valid”用例(如array.sort()、array.sort(compare)、array.sort((a, b, c) => ...))不被报告的原因,详见 test/prefer-simple-sort-comparator.js。
3.2 语法树归约:analyzeExpression与analyzeStatements
analyzeExpression把比较器函数体递归归约为一棵“分支树”:
- 叶子节点(
leaf):带符号的数值字面量(1、-1、0、+1等); - 分支节点(
branch):形如{test, consequent, alternate}的ConditionalExpression。
analyzeStatements处理块体,支持ReturnStatement、IfStatement、以及if/else链。随后:
collectTests收集树中所有关系测试;collectLeaves收集所有数值叶子。
关键约束一:镜像匹配。isParameterSwap递归验证每个测试的左右操作数是否为“参数互换镜像”(a↔b、a.foo↔b.foo、a[0]↔b[0]),并且所有测试必须比较同一对操作数——否则就是多键比较器,直接跳过(源码注释:“All tests must compare the same operand pair, otherwise it is a multi-key comparator”)。这就是a.foo > b.foo ? 1 : a.bar > b.bar ? -1 : 0不被报告的原因。同时isParameterSwap只接受标识符和成员表达式,可选链操作数(a?.foo > b?.foo)不满足条件,也不会被误报。
关键约束二:真正的双向比较器。规则要求比较器必须同时返回正数和负数(signs中不能全为非正或全为非负),且最外层分支为真时的符号不能为 0——即a > b ? 0 : -1这类“单边”写法不在报告范围内。
3.3 符号验证:leafSign、trueBranchSign与branchSignsMatchSubtraction
leafSign用Math.sign计算数值叶子的符号;trueBranchSign沿“最外层测试为真”的路径下行,得到外层分支对应的返回符号;expectedSignForTest根据测试的运算符方向(>/>=视为正序)推算出该测试在减法语义下应当返回的符号;最后branchSignsMatchSubtraction递归核对整棵树每一层的符号都与减法行为一致。
方向取向在create中完成:根据外层测试与符号计算减数与被减数——const [minuend, subtrahend] = isLeftIsMinuend ? [test.left, test.right] : [test.right, test.left],从而保证a > b ? -1 : 1(降序)能正确生成b - a而非a - b。
3.4 报告与建议的生成条件
最后生成替换文本(a, b) => <minuend文本> - <subtrahend文本>,并满足以下条件才附带 suggestion:
- 函数体内部没有注释(避免丢失注释);
- 接收者不是 BigInt 类型化数组(避免生成运行时报错的代码);
- 接收者不是“已知非数组”类型——
shouldSkipKnownNonArrayReceiver会跳过Set或声明了同名sort方法的自定义类型,但数组字面量、对象字面量、函数、字符串字面量等直接可见的接收者仍会报告(实现见 rules/utils/should-skip-known-non-array-receiver.js,规则层面的行为在 test/unit/array-receiver-policy.js 有统一验证)。
快照 invalid 23(含注释)与 invalid 27~31(BigInt 类型化数组)恰好对应前两条抑制条件,是理解“何时不给建议”的最佳样例。
四、为什么不自动修复:数字与字符串的语义差异
官方文档特别强调:该规则永远只提供建议,不做自动修复,因为冗长的比较器同样适用于字符串排序,而减法只对数字成立:
// ✅ 字符串排序应使用 localeCompare array.sort((a, b) => a.localeCompare(b)); // ❌ 若自动改写为减法,对字符串会得到 NaN,排序失效快照中所有用例的修复结果都由开发者通过编辑器的“应用建议”(Apply suggestion)操作手动采纳,这与meta.hasSuggestions: true的声明一致。对于字符串比较器(如a > b ? 1 : -1作用于字符串数组),规则仍会报告——因为从语法层面无法可靠判断元素类型,但建议本身是可选的,用户可以忽略。
五、在本地复现与验证
快照文件本身是 AVA 测试的产物。要在本地复现这些结果,可执行仓库的测试命令:
# 运行全部单元测试(包含快照断言) npm test # 只运行该规则的测试 npx ava test/prefer-simple-sort-comparator.js测试用例的“合法代码”(valid)与“非法代码”(invalid)清单维护在 test/prefer-simple-sort-comparator.js 中,快照则是运行后自动生成的观测结果。valid 用例覆盖了大量反例:已经是减法形态的a - b、localeCompare、非双向比较器(a > b ? 0 : -1)、分支方向与测试不符的写法、多键比较器、Math.random()乱序、非镜像操作数(a > c、a[i] > b[j])、非数值返回(a > b ? "a" : "b")、函数体内夹带副作用语句(doSomething())、a === b ? 0 : 1等——这些与 31 个 invalid 快照互为补充,共同勾勒出规则的完整边界。
六、实战建议小结
- 数字排序请直接写
(a, b) => a - b(升序)或(a, b) => b - a(降序),对象数组用(a, b) => a.foo - b.foo; - 字符串排序用
(a, b) => a.localeCompare(b),规则不会误报; - 多键比较(
a.foo - b.foo || a.bar - b.bar)与洗牌逻辑(Math.random() - 0.5)应保持原样,规则不会误报; - 当规则报告但未给出建议时,通常是函数体内含注释或接收者是 BigInt 类型化数组,需要人工确认后再改写;
- 接入插件后,可用编辑器的 Quick Fix 面板逐条应用该规则的替换建议。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考