news 2026/9/18 3:50:34

eslint-plugin-unicorn no-subtraction-comparison 规则详解:36 个快照测试用例、自动修复边界与源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eslint-plugin-unicorn no-subtraction-comparison 规则详解:36 个快照测试用例、自动修复边界与源码实现

eslint-plugin-unicorn no-subtraction-comparison 规则详解:36 个快照测试用例、自动修复边界与源码实现

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

本文以no-subtraction-comparison规则的 AVA 快照测试报告为主体,完整解读其 36 个无效代码用例的输入、报错与修复结果,并逐条对照规则源码(rules/no-subtraction-comparison.js)分析「模式匹配 → 操作符翻转 → autofix/suggestion 三级判定」的实现机制。读完你能准确掌握这条规则的触发条件、自动修复与仅建议的边界划分,以及isNumber静态数字判定在其中的关键作用。

快照报告的来龙去脉

测试快照报告 是 AVA 测试框架针对 test/no-subtraction-comparison.js 中test.snapshot调用自动生成的行为基线文档,报告头部明确说明「实际快照保存在no-subtraction-comparison.js.snap中,由 AVA 生成」。它与快照文件 test/snapshots/no-subtraction-comparison.js.snap 一一对应:每当规则对某个输入产生的报错消息(Message)、自动修复输出(Output)或建议(Suggestion)发生变化时,快照测试就会失败,从而把这条规则的行为锁定为可回归的契约。

这份快照报告本身就是规则的「行为规格说明书」:它完整记录了 36 个被报告为无效的表达式,每一个都包含三要素——

  • Input:触发的原始代码;
  • Message:统一为Prefer comparing the values directly over comparing the difference with0.
  • 修复方式:要么是Output--fix自动改写结果),要么是Suggestion 1/1: Replace with …(需人工确认的建议),个别情况则只报错、不给任何修复。

规则本身在 rules/index.js 中以no-subtraction-comparison名称导出,其meta声明(rules/no-subtraction-comparison.js)为:type: 'suggestion'fixable: 'code'hasSuggestions: truerecommended: 'unopinionated'、仅适用于js/js语言。官方规则文档(docs/rules/no-subtraction-comparison.md)进一步说明:该规则同时启用于recommendedunopinionated两套配置,并且「可通过--fix自动修复,也可通过编辑器建议手动修复」。

36 个无效用例全景:输入、消息与修复结果

以下将快照报告中的 36 个用例按行为模式分组还原。所有消息文案相同,故下表聚焦「输入 → 输出/建议」。

分组一:右侧与零比较,两侧为裸标识符(仅建议)

这是规则最典型的触发形态。由于两侧只是标识符,无法证明为数字,规则给出 suggestion(建议)而非 autofix:

#输入建议替换为
1if (a - b > 0) {}a > b
2if (a - b >= 0) {}a >= b
3if (a - b < 0) {}a < b
4if (a - b <= 0) {}a <= b
5a - b === 0a === b
6a - b !== 0a !== b
7a - b == 0a == b
8a - b != 0a != b

分组二:零在左侧,关系操作符被镜像翻转

0 op (a - b)等价于(a - b) flip(op)。等号族是对称的(映射到自身),大小于族则翻转。同样只给建议:

#输入建议替换为
90 < a - ba > b
100 <= a - ba >= b
110 > a - ba < b
120 >= a - ba <= b
130 === a - ba === b
140 !== a - ba !== b

分组三:操作数可证明为数字 → 自动修复

#输入自动修复输出
151 - 2 > 01 > 2
16foo.length - bar.length > 0foo.length > bar.length
17Number(a) - Number(b) < 0Number(a) < Number(b)
18Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY > 0Number.POSITIVE_INFINITY > Number.POSITIVE_INFINITY
191 - 2 >= 01 >= 2
201 - 1 === 01 === 1
210 < foo.length - bar.lengthfoo.length > bar.length
26Math.round(a) - Math.round(b) > 0Math.round(a) > Math.round(b)
28(foo.length) - (bar.length) > 0(foo.length) > (bar.length)(保留原有括号)

这里有两个值得注意的细节:

  • 用例 18 中两侧是Number.POSITIVE_INFINITY。由于是严格比较(>),x - y > 0x > y即使在无穷值或NaN参与时也同真同假(Infinity - InfinityNaN,两者均为false),所以严格比较允许放宽到「可证明是数字」即可 autofix。
  • 用例 28 展示了getParenthesizedText的作用:操作数原本的括号被完整保留在修复结果中。

分组四:降格为建议的边界场景

以下用例同样被报告,但只给 suggestion,不给 autofix——它们正是理解规则保守策略的关键:

#输入建议替换为降格原因
22Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY >= 0Number.POSITIVE_INFINITY >= Number.POSITIVE_INFINITY非严格比较遇到无穷值:Infinity - Infinity >= 0false,而Infinity >= Infinitytrue,改写会改变行为
23Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY === 0Number.POSITIVE_INFINITY === Number.POSITIVE_INFINITY同上(NaN === 0falseInfinity === Infinitytrue
24foo.length - bar.length >= 0foo.length >= bar.length非严格比较要求两侧是静态可知的有限数字length不是
25foo.length - bar > 0foo.length > bar严格比较要求两侧都可证明是数字bar无法证明
27a.length - b.length - c > 0a.length - b.length > c嵌套减法:右侧0实际对应的是c,改写保留左侧减法
29const modes = new Set(['foo']); modes.clear(); (modes.size ? 1 : 'x') - (modes.size ? 1 : 'x') === 0(modes.size ? 1 : 'x') === (modes.size ? 1 : 'x')条件表达式两分支类型不一致(1/'x'),无法静态定值
30const modes = new Set(['foo']); modes.clear(); ((modes.size && 1) || value) - 1 === 0((modes.size && 1) || value) === 1逻辑表达式静态值不可推断
31const object = {value: true}; Object.defineProperty(object, 'value', {get() { return false; }}); (object.value ? 1 : value) - 1 === 0(object.value ? 1 : value) === 1getter 定义无法被静态值分析穿透
32const modes = new Set(['foo']); modes.clear(); ((modes.size ? 1 : value) as number) - 1 === 0((modes.size ? 1 : value) as number) === 1TypeScriptas number断言提供的是类型信息而非静态值,非严格比较不采信
33(a - b) > 0a > b整个减法被括号包裹,两侧仍是标识符
34a?.b - c?.d > 0a?.b > c?.d可选链结果可能为undefined,非数字
36(a as number) - (b as number) > 0(a as number) > (b as number)(自动修复)严格比较中TSAsExpression断言为numberisNumber采信

分组五:表达式内含注释 → 只报错、不修复

#输入结果
35a - /* comment */ b > 0仅报告错误,既无 autofix 也无 suggestion

规则源码中有一句注释说明了原因(rules/no-subtraction-comparison.js):「为避免丢失注释,表达式内部存在任何注释时不提供修复」。因为fixer.replaceText(node, replacement)会整段替换节点文本,会连带删除夹在操作符之间的注释,所以干脆放弃修复。

规则源码实现:三级判定链

理解了用例分布后,再回看 rules/no-subtraction-comparison.js 的实现,逻辑非常紧凑,可以拆成四步。

第一步:模式匹配「比较表达式的一侧是减法、另一侧是 0」

规则监听BinaryExpression(第 40 行起),要求节点操作符属于 8 个比较操作符之一,且「一侧是减法(BinaryExpression且操作符为-)、另一侧是字面量0」:

let subtraction; let operator; if (isZero(node.right) && isSubtraction(node.left)) { subtraction = node.left; operator = node.operator; } else if (isZero(node.left) && isSubtraction(node.right)) { subtraction = node.right; operator = invertedOperator[node.operator]; } else { return; }

两个判定点的细节都体现在用例中:

  • isZero = node => isLiteral(node, 0)只认数字字面量0。因此测试的 valid 用例里a - b > 0n(BigInt 零)和a - b > -0(一元负号表达式,不是0字面量)都不会被报告。
  • 零在左侧时通过invertedOperator表翻转操作符(第 11-22 行):><>=<=,而===!====!=对称映射到自身——这正是分组二六个用例的来源。

第二步:注释保护

if (sourceCode.getCommentsInside(node).length > 0 { return problem; // 只报错 }

对应快照用例 35。

第三步:构造替换文本并决定修复级别

替换文本由两侧操作数加翻转后的操作符拼接而成(第 67-68 行):

const replacement = `${getParenthesizedText(left, context)} ${operator} ${getParenthesizedText(right, context)}`;

其中 getParenthesizedText 会连同包裹操作数的括号一起切片源码文本,保证了用例 28 中(foo.length) > (bar.length)这样的输出。随后是核心的三级判定(第 75-89 行):

const canAutofix = strictOrderingOperators.has(operator) ? isNumber(left, context) && isNumber(right, context) : isFiniteStaticNumber(left, context) && isFiniteStaticNumber(right, context);
  • 严格比较(>/<,翻转后同理):要求两侧都通过isNumber判定(类型层面可证明是数字)→ autofix。覆盖用例 15-18、21、26、28、36。
  • 非严格比较(>=<====!====!=:要求两侧都能通过 getStaticValueForControlFlow 求出有限数字静态值(typeof value === 'number' && Number.isFinite(value))→ autofix。覆盖用例 19、20。
  • 其余一律给 suggestionproblem.suggest携带Replace with {{replacement}}消息和同一个fix函数,由用户确认后应用。覆盖分组一、二、四的大部分用例。

之所以对非严格比较加「静态有限数字」的更严限制,官方文档(docs/rules/no-subtraction-comparison.md)的 Caveats 一节给出了反例:Infinity - InfinityNaN,导致Infinity - Infinity >= 0falseInfinity >= Infinitytrue;同样,"10" - "5" > 0true而字符串按字典序"10" > "5"false——重写只对数字才保证行为等价。

第四步:isNumber——「可证明是数字」的判定清单

autofix 与 suggestion 的分野很大程度取决于 rules/utils/is-number.js 中的isNumber。从源码结构看,它按 AST 形式白名单式地认可以下「数字证据」:数字字面量、Math静态属性(PIE等)与静态方法调用(Math.round(...)等)、Number(...)调用、Number.EPSILON等静态属性、Number.parseInt/parseFloat调用、字符串的数字返回方法(charCodeAtindexOflocaleCompare等,且接收方需可证明是字符串)、.length属性、算术运算(-*/%**等至少一侧为数字,>>>与一元+恒为数字)、条件/逻辑/序列表达式的传播、带: number注解的标识符,以及 TypeScript 的as number/satisfies number/ 非空断言。兜底再走getStaticValueForControlFlow静态求值。

把这份清单与快照用例对读,每一条降格都能找到出处:用例 25 的裸标识符bar无类型注解、无可静态值;用例 34 的a?.b因可选链可能短路为undefined不在白名单内;用例 26 的Math.round(a)恰好在mathMethods白名单里,因此得以 autofix。

值得再引用一条非快照用例佐证控制流敏感性的边界(test/no-subtraction-comparison.js):

{ code: 'const alias = condition; var condition = true; (alias ? 1 : value) - 1 === 0', errors: [{messageId: 'no-subtraction-comparison/error', suggestions: 1}], },

断言suggestions: 1(且没有output)说明:尽管condition后面被赋值为true,静态分析仍无法证明alias的真值,条件表达式(alias ? 1 : value)得不到静态数字,于是只能给建议。这与用例 29、30、31 的降格原因一脉相承。

合法用例:哪些写法不会被报告

test/no-subtraction-comparison.js 的valid列表(对应快照报告中不存在的报告记录)同样重要,它划定了规则的负边界:

输入不报告原因
a > b/a < b/a === b已经是直接比较
a - b减法本身不是比较表达式
a - b > 1/a - b > c/a - b === c比较对象不是0
a + b > 0/a * b > 0/a % b > 0一侧不是减法
a > 0/0 > a/0 > 0两侧都不是减法
a - b > 0n0n是 BigInt 字面量,isLiteral(node, 0)不成立
a - b > -0-0是一元表达式而非0字面量
a - b instanceof cinstanceof不在 8 个受支持的操作符集合内

使用与验证方式

在 ESLint 中启用该规则(flat config 示例,规则前缀为unicorn):

// eslint.config.js export default [ { rules: { 'unicorn/no-subtraction-comparison': 'error', }, }, ];

如果直接采用插件提供的recommendedunopinionated配置集,该规则已默认开启(见 docs/rules/no-subtraction-comparison.md 头部的配置说明)。对严格比较场景,运行--fix即可自动改写;其余场景在编辑器中确认建议即可。

行为回归方面,本规则的全部 36 个无效用例与若干合法用例由 test/no-subtraction-comparison.js 中的test.snapshot定义,基线即本文分析的 test/snapshots/no-subtraction-comparison.js.md 与配套的 test/snapshots/no-subtraction-comparison.js.snap:只要规则的报错文案、修复级别或改写结果发生任何变化,快照测试都会暴露出来。

小结

no-subtraction-comparison是 eslint-plugin-unicorn 中一条「小规则、深功夫」的典型:模式匹配只区分两种形态(零在左/在右)和八种比较操作符,但修复策略却精细到三级——严格比较要求isNumber双证、非严格比较要求静态有限数字双证、其余一律降级为 suggestion,并对表达式内注释彻底弃修。快照报告以 36 个用例把这些边界全部固化成了可检索、可回归的文档,这也是研究该仓库其他建议类规则 autofix 设计思路时值得参照的样本。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 3:48:54

Colibri 实战:CPU 与系统内存部署调优 MoE 大模型

在折腾大模型的圈子里&#xff0c;最近被反复提起的一个名字是 Colibri。它做的事情说起来很朴素&#xff1a;让那些"看起来根本跑不动"的混合专家&#xff08;MoE&#xff09;大模型&#xff0c;在没有独立显卡的普通机器上也能以可用的速度吐字。我第一次听到这个方…

作者头像 李华
网站建设 2026/9/18 3:48:38

VoiceStudio 音频工作流实战:录音降噪、语音合成与批量导出

1. VoiceStudio 想解决的其实是"音频工作流割裂"这件事第一次看到 VoiceStudio 这个名字&#xff0c;我脑子里蹦出来的不是某个具体软件&#xff0c;而是一类很典型的痛点&#xff1a;做内容的人手里往往同时开着四五个工具&#xff0c;录音用一个、降噪用一个、配音…

作者头像 李华
网站建设 2026/9/18 3:44:30

AI写论文避坑指南:从可验证文献到真实数据全解析

先讲个我亲眼见过的翻车案例&#xff0c;再聊今天想说的正事。去年有个师弟找我参谋&#xff0c;说想用AI写论文&#xff0c;看到某软件宣传“输入标题&#xff0c;三分钟出初稿”&#xff0c;他真信了。结果交上去没两天&#xff0c;导师把他叫去办公室&#xff0c;指着参考文…

作者头像 李华
网站建设 2026/9/18 3:42:42

STM32F411启动全链路:从向量表、启动文件到main

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:42:39

煤矿物料编码规则解析与数据库校验实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:41:41

oh-my-hermes:智能体本地部署与任务编排实战指南

"oh-my-hermes"这个名字起得挺有迷惑性&#xff0c;第一次听还以为是个终端美化主题&#xff0c;跟oh-my-zsh是一路货色。实际上它是一套围绕Hermes智能体的安装、配置、工作流管理的实战方案。我最初接触Hermes是在一个技术交流群里&#xff0c;看到有人把DeepSeek的…

作者头像 李华