eslint-plugin-unicorn 规则详解:require-array-sort-compare——强制为 Array#sort()/toSorted() 提供比较函数
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
本文以 eslint-plugin-unicorn 仓库中的 require-array-sort-compare 规则文档 为骨架,结合 规则实现源码、共享工具 与 测试用例,系统讲解该规则的设计动机、报告范围、自动修复机制与配置启用方式。读完本文,你将理解"默认字符串排序"的陷阱为何值得用一条 ESLint 规则拦截,并掌握如何在自己的项目中开启与使用该规则及其编辑器建议修复。
规则背景:为什么必须显式传入比较函数
JavaScript 的Array#sort()与Array#toSorted()在没有显式比较函数(compare function)时,会先把每个元素转换为字符串,再按字符串的字典序(UTF-16 码元顺序)排序。这种默认行为对数字、日期等非字符串类型几乎总是产生意外结果。
require-array-sort-compare规则的核心诉求非常明确:凡是调用Array#sort()或Array#toSorted()且未传入比较函数(或显式传入了undefined),都应当被报告,因为"按字符串排序"几乎不可能是开发者的真实意图。
// ❌ - Sorts as strings: [1, 10, 2, 20, 3] const numbers = [3, 1, 10, 2, 20]; numbers.toSorted(); // ✅ - Properly numeric sort: [1, 2, 3, 10, 20] const numbers = [3, 1, 10, 2, 20]; numbers.toSorted((a, b) => a - b);// ❌ - String sorting on numbers is wrong [5, 10, 15, 2, 25].sort(); // → [10, 15, 2, 25, 5] (unexpected!) // ✅ [5, 10, 15, 2, 25].sort((a, b) => a - b); // → [2, 5, 10, 15, 25]// ❌ - Sorting strings without explicit compare const names = ['Alice', 'bob', 'Charlie']; names.sort(); // → ['Alice', 'Charlie', 'bob'] (case-sensitive) // ✅ - Case-insensitive sorting const names = ['Alice', 'bob', 'Charlie']; names.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'})); // → ['Alice', 'bob', 'Charlie']// ✅ - Descending order const numbers = [3, 1, 4, 1, 5, 9]; numbers.sort((a, b) => b - a); // [9, 5, 4, 3, 1, 1]从上面的例子可以看出,字符串排序对数字数组([5, 10, 15, 2, 25].sort()得到[10, 15, 2, 25, 5])和大小写混合的字符串数组(['Alice', 'Charlie', 'bob'])都会产生反直觉的结果;而显式传入比较函数后,行为完全可控。
报告范围:哪些调用会被该规则拦截
规则通过isMethodCall辅助函数精确匹配调用形态(见 规则源码):
context.on('CallExpression', callExpression => { if (!isMethodCall(callExpression, { methods: ['sort', 'toSorted'], maximumArguments: 1, optionalCall: false, })) { return; } // ... });匹配条件包括:
- 方法名:仅
sort与toSorted两个方法; - 参数上限:
maximumArguments: 1,即最多只允许一个参数(比较函数); - 禁止可选调用:
optionalCall: false,array.sort?.()这类形式不会命中。
随后规则检查参数:
if ( callExpression.arguments.length === 1 && !isUndefined(callExpression.arguments[0]) ) { return; }也就是说,只有一个参数且该参数不是undefined时直接放行;无参数或参数为undefined才会进入报告逻辑。
会被报告的典型场景
根据 测试用例,以下调用均会被报告:
array.sort(); array.toSorted(); array.sort(undefined); array.toSorted(undefined); array?.sort(); array?.toSorted(); [].sort(); [].toSorted(); [3, 2, 1].sort(); Array.from(iterable).sort(); Array.of(3, 2, 1).sort(); new Array(3).sort(); const array = []; array.sort(); array.sort(/* comment */); // 仅注释,无实际比较函数TypeScript 场景下同样生效:string[]、Array<string>、as string[]断言、<string[]>value泛型断言等写法都会被正确识别并报告。
不会报告的合法场景
- 已传入任意比较函数:
array.sort((a, b) => a - b)、array.toSorted((a, b) => a.localeCompare(b)); - 传入展开参数:
array.sort(...[])、array.toSorted(...[])(参数形态无法静态确认,予以放行); - 可选调用:
array.sort?.()、array?.sort?.(); - 属性访问而非方法调用:
array["sort"]()、array[sort]()(见测试用例中的字符串索引与计算索引形式); - 借用原型:
Array.prototype.sort.call(array)、Array.prototype.sort.apply(array)等; - 已知非数组接收者:
({sort() {}}).sort()、(() => {}).sort()、new Set().sort()、const object = {}; object.sort()等,因为调用者本身不是数组,默认字符串排序的语义假设不成立。
关键设计:类型化数组(TypedArray)绝不报告
规则文档特别强调了一处与其他数组规则不同的设计:类型化数组永远不会被报告。
原因在于TypedArray#sort()本身已经按数值排序,与Array#sort()的字符串排序行为完全不同,因此无需比较函数。而大多数针对数组方法的 unicorn 规则会报告类型化数组接收者,因为类型化数组共享了Array的大部分方法表面(sort()、forEach()、join()、reduce()等)。
这一差异在源码中体现得非常直白(规则源码注释):
// Deliberately not `isKnownNonIndexedCollection`: `TypedArray#sort()` already sorts numerically, so it needs no comparator if (isKnownNonArray(callExpression.callee.object, context)) { return; }与之对应的共享工具 should-skip-known-non-array-receiver.js 也解释了这个取舍:类型化数组缺少的数组方法(push()、splice()、flat()等)根本不在其原型上,调用必然报错,因此报告它们没有额外成本;但require-array-sort-compare属于"类型化数组必须豁免"的例外,因为TypedArray#sort()的语义已经正确,所以它直接调用isKnownNonArray而非把类型化数组视为数组的isKnownNonIndexedCollection。
isKnownNonArray的判定逻辑定义在 is-array.js,其knownNonArrayTypeNames集合包含:
- 全部类型化数组(来自共享的 typed-array 列表),如
Int8Array、Uint8Array、BigInt64Array等; - 非索引集合类型:
Map、ReadonlyMap、WeakMap、Set、ReadonlySet、WeakSet以及CanvasRenderingContext2D等; - 语法层面即可判定的非数组表达式:
ObjectExpression、FunctionExpression、ArrowFunctionExpression、ClassExpression、TemplateLiteral。
测试用例对这一点覆盖得很充分(测试文件):
// valid(不被报告) function f(foo: Int8Array) { foo.sort(); } const foo = new Int8Array(); foo.sort(); declare function getBytes(): Int8Array; getBytes().sort(); // 通过类型信息解析其中最后一个用例说明:不仅类型注解形式能豁免,通过 TS 类型信息(type-aware linting)解析出的Int8Array返回值同样豁免。
自动修复与建议修复:两种开箱即用的比较函数
该规则不提供"一键修复"(fix),而是通过editor suggestions(编辑器建议)提供修复,属于"手动可修复"级别。报告位置为callExpression.callee.property,即sort/toSorted属性本身(规则源码)。
两个内置建议
当调用内没有注释时,规则会生成两个修复建议(getSuggestions,见 规则源码):
| messageId | 建议文本 | 注入的比较函数 |
|---|---|---|
require-array-sort-compare/numeric | Sort numerically.(按数值排序) | (a, b) => a - b |
require-array-sort-compare/string | Sort strings with String#localeCompare().(用 localeCompare 排序字符串) | (a, b) => a.localeCompare(b) |
如果调用内部存在注释(如array.sort(/* comment */)),则不生成任何建议,避免修复时吞掉注释。
对应错误消息为:
- 主消息:
Pass a compare function to avoid sorting elements as strings.(传入比较函数以避免按字符串排序元素)
修复的底层实现
建议的fix由getCompareFunctionFix生成,它会根据调用是否已有参数,选择两种修复路径(规则源码):
const getCompareFunctionFix = (callExpression, compareFunction, context) => fixer => { const [firstArgument] = callExpression.arguments; return firstArgument ? replaceArgument(fixer, firstArgument, compareFunction, context) : appendArgument(fixer, callExpression, compareFunction, context); };- 有参数时(即
sort(undefined)):调用 replace-argument.js 中的replaceArgument,用getParenthesizedRange获取参数(含必要括号)的完整范围并整体替换为比较函数; - 无参数时:调用 append-argument.js 中的
appendArgument,在(与)之间插入比较函数。该实现会检查倒数第二个 token 是否为逗号,以决定插入格式是, ${text}还是${text},,从而正确处理多参数场景下的空白。
实际效果示例:
// 应用 numeric 建议后 numbers.toSorted(); // → numbers.toSorted((a, b) => a - b); [3, 2, 1].sort(); // → [3, 2, 1].sort((a, b) => a - b); // 应用 string 建议后 names.sort(); // → names.sort((a, b) => a.localeCompare(b));规则元数据与配置启用
规则注册于 rules/index.js,元数据定义在 规则源码:
const config = { create, meta: { type: 'problem', // 属于"问题"类规则,提示潜在 bug docs: { description: 'Require a compare function when calling `Array#sort()` or `Array#toSorted()`.', recommended: 'unopinionated', }, hasSuggestions: true, // 提供编辑器建议 messages, languages: ['js/js'], // 仅作用于 JavaScript }, };根据 规则文档 顶部的自动生成头部,该规则在以下配置中默认启用:
- ✅
recommended(推荐配置) - ☑️
unopinionated(无观点配置)
因此,只要你的 ESLint 配置继承了 eslint-plugin-unicorn 的recommended或unopinionatedflat config(参考 flat-config-base.js),该规则就会自动生效,无需手动声明。若需要单独开启或调整,可在eslint.config.js中显式配置:
import unicorn from 'eslint-plugin-unicorn'; export default [ unicorn.configs['recommended'], { rules: { 'unicorn/require-array-sort-compare': 'error', }, }, ];测试验证:从快照看规则行为
规则的 20 余个 valid / invalid 用例集中在 test/require-array-sort-compare.js,对应的快照文件为 test/snapshots/require-array-sort-compare.js.md。其中值得注意的边界用例:
- 类型化数组豁免(valid):
Int8Array类型注解、new Int8Array()构造、以及通过projectService类型解析的返回类型,均不被报告; undefined也报告(invalid):array.sort(undefined)与array.toSorted(undefined)被列入 invalid,说明显式传入undefined并不比省略参数更安全;maximumArguments: 1的体现:array.sort(compareFunction)(两个参数如(a, b, c)形态)超出参数上限时不被匹配;- 接收者静态类型推断:
const array: string[] = [];、(value as string[]).sort()、(<string[]>value).toSorted()等 TS 写法都能被识别为数组接收者并触发报告;而const array: string = ""; array.sort()(明确是string)则豁免。
与 @typescript-eslint/require-array-sort-compare 的协作
规则文档末尾给出了一条重要提示:如果项目启用了typed linting(基于类型信息的 lint,如@typescript-eslint的类型感知解析器),那么类型感知的@typescript-eslint/require-array-sort-compare规则可以做到更精确——它能够根据真实的类型信息判断接收者是否确实是数组,从而减少误报。
而 unicorn 的该规则在无类型信息的情况下,主要依赖语法形态(数组字面量、Array.from()/Array.of()/new Array()调用、TS 类型注解)与保守的"已知非数组"判定来平衡精度与召回。对于追求更高精度的类型化项目,可以两者配合使用;对于普通 JavaScript 项目,unicorn 的这条规则是零配置、零额外依赖的轻量替代方案。
总结
require-array-sort-compare是 eslint-plugin-unicorn 中一条小而实用的"问题检测"规则:它基于Array#sort()/Array#toSorted()默认字符串排序这一语言陷阱,强制开发者显式传入比较函数,并提供"数值排序"与"字符串 localeCompare 排序"两个编辑器建议一键修复。其实现巧妙地通过isKnownNonArray与isKnownNonIndexedCollection的区分,为语义本已正确的TypedArray#sort()保留了豁免通道,值得作为阅读 ESLint 规则实现与类型辅助工具设计的上佳范例。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考