eslint-plugin-unicorn 规则详解:require-array-sort-compare——强制为 Arraysort()/toSorted() 提供比较函数
2026/9/18 14:53:21 网站建设 项目流程

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

匹配条件包括:

  • 方法名:仅sorttoSorted两个方法;
  • 参数上限maximumArguments: 1,即最多只允许一个参数(比较函数);
  • 禁止可选调用optionalCall: falsearray.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 列表),如Int8ArrayUint8ArrayBigInt64Array等;
  • 非索引集合类型:MapReadonlyMapWeakMapSetReadonlySetWeakSet以及CanvasRenderingContext2D等;
  • 语法层面即可判定的非数组表达式:ObjectExpressionFunctionExpressionArrowFunctionExpressionClassExpressionTemplateLiteral

测试用例对这一点覆盖得很充分(测试文件):

// 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/numericSort numerically.(按数值排序)(a, b) => a - b
require-array-sort-compare/stringSort strings with String#localeCompare().(用 localeCompare 排序字符串)(a, b) => a.localeCompare(b)

如果调用内部存在注释(如array.sort(/* comment */)),则不生成任何建议,避免修复时吞掉注释。

对应错误消息为:

  • 主消息:Pass a compare function to avoid sorting elements as strings.(传入比较函数以避免按字符串排序元素)

修复的底层实现

建议的fixgetCompareFunctionFix生成,它会根据调用是否已有参数,选择两种修复路径(规则源码):

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 的recommendedunopinionatedflat 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 排序"两个编辑器建议一键修复。其实现巧妙地通过isKnownNonArrayisKnownNonIndexedCollection的区分,为语义本已正确的TypedArray#sort()保留了豁免通道,值得作为阅读 ESLint 规则实现与类型辅助工具设计的上佳范例。

【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询