eslint-plugin-unicorn 规则实战:从快照测试看 prefer-simple-sort-comparator 如何把啰嗦的比较器简化为减法
2026/9/19 6:49:26 网站建设 项目流程

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; });

当比较器的两个操作数互为“镜像”(如aba.foob.foo)时,整个逻辑等价于一次减法运算:

array.sort((a, b) => a - b);

prefer-simple-sort-comparator规则的作用正是发现这类可简化的比较器,并给出替换为减法的建议。按官方文档 docs/rules/prefer-simple-sort-comparator.md 的说明,该规则:

  • recommendedunopinionated两种配置中默认启用(见 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 : -1a > 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 : -1a - b(invalid 8);
  • a <= b ? -1 : 1a - b(invalid 9);
  • a >= b ? -1 : 1b - a(invalid 10,降序)。

由于a >= b ? 1 : -1a > 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 : -1a.foo - b.foo(invalid 12);
  • a[0] > b[0] ? 1 : -1a[0] - b[0](invalid 13,下标字面量);
  • a[i] > b[i] ? 1 : -1a[i] - b[i](invalid 14,下标变量);
  • a.foo.bar < b.foo.bar ? -1 : 1a.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限定方法名为sorttoSorted且恰好一个参数;随后校验比较器必须是普通(非 async、非 generator)函数且恰好两个Identifier类型的形参。任何不符合条件的调用直接放行,这正是快照之外那些“valid”用例(如array.sort()array.sort(compare)array.sort((a, b, c) => ...))不被报告的原因,详见 test/prefer-simple-sort-comparator.js。

3.2 语法树归约:analyzeExpressionanalyzeStatements

analyzeExpression把比较器函数体递归归约为一棵“分支树”:

  • 叶子节点(leaf):带符号的数值字面量(1-10+1等);
  • 分支节点(branch):形如{test, consequent, alternate}ConditionalExpression

analyzeStatements处理块体,支持ReturnStatementIfStatement、以及if/else链。随后:

  • collectTests收集树中所有关系测试;
  • collectLeaves收集所有数值叶子。

关键约束一:镜像匹配。isParameterSwap递归验证每个测试的左右操作数是否为“参数互换镜像”(aba.foob.fooa[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 符号验证:leafSigntrueBranchSignbranchSignsMatchSubtraction

leafSignMath.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:

  1. 函数体内部没有注释(避免丢失注释);
  2. 接收者不是 BigInt 类型化数组(避免生成运行时报错的代码);
  3. 接收者不是“已知非数组”类型——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 - blocaleCompare、非双向比较器(a > b ? 0 : -1)、分支方向与测试不符的写法、多键比较器、Math.random()乱序、非镜像操作数(a > ca[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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询