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: true、recommended: 'unopinionated'、仅适用于js/js语言。官方规则文档(docs/rules/no-subtraction-comparison.md)进一步说明:该规则同时启用于recommended与unopinionated两套配置,并且「可通过--fix自动修复,也可通过编辑器建议手动修复」。
36 个无效用例全景:输入、消息与修复结果
以下将快照报告中的 36 个用例按行为模式分组还原。所有消息文案相同,故下表聚焦「输入 → 输出/建议」。
分组一:右侧与零比较,两侧为裸标识符(仅建议)
这是规则最典型的触发形态。由于两侧只是标识符,无法证明为数字,规则给出 suggestion(建议)而非 autofix:
| # | 输入 | 建议替换为 |
|---|---|---|
| 1 | if (a - b > 0) {} | a > b |
| 2 | if (a - b >= 0) {} | a >= b |
| 3 | if (a - b < 0) {} | a < b |
| 4 | if (a - b <= 0) {} | a <= b |
| 5 | a - b === 0 | a === b |
| 6 | a - b !== 0 | a !== b |
| 7 | a - b == 0 | a == b |
| 8 | a - b != 0 | a != b |
分组二:零在左侧,关系操作符被镜像翻转
0 op (a - b)等价于(a - b) flip(op)。等号族是对称的(映射到自身),大小于族则翻转。同样只给建议:
| # | 输入 | 建议替换为 |
|---|---|---|
| 9 | 0 < a - b | a > b |
| 10 | 0 <= a - b | a >= b |
| 11 | 0 > a - b | a < b |
| 12 | 0 >= a - b | a <= b |
| 13 | 0 === a - b | a === b |
| 14 | 0 !== a - b | a !== b |
分组三:操作数可证明为数字 → 自动修复
| # | 输入 | 自动修复输出 |
|---|---|---|
| 15 | 1 - 2 > 0 | 1 > 2 |
| 16 | foo.length - bar.length > 0 | foo.length > bar.length |
| 17 | Number(a) - Number(b) < 0 | Number(a) < Number(b) |
| 18 | Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY > 0 | Number.POSITIVE_INFINITY > Number.POSITIVE_INFINITY |
| 19 | 1 - 2 >= 0 | 1 >= 2 |
| 20 | 1 - 1 === 0 | 1 === 1 |
| 21 | 0 < foo.length - bar.length | foo.length > bar.length |
| 26 | Math.round(a) - Math.round(b) > 0 | Math.round(a) > Math.round(b) |
| 28 | (foo.length) - (bar.length) > 0 | (foo.length) > (bar.length)(保留原有括号) |
这里有两个值得注意的细节:
- 用例 18 中两侧是
Number.POSITIVE_INFINITY。由于是严格比较(>),x - y > 0与x > y即使在无穷值或NaN参与时也同真同假(Infinity - Infinity为NaN,两者均为false),所以严格比较允许放宽到「可证明是数字」即可 autofix。 - 用例 28 展示了
getParenthesizedText的作用:操作数原本的括号被完整保留在修复结果中。
分组四:降格为建议的边界场景
以下用例同样被报告,但只给 suggestion,不给 autofix——它们正是理解规则保守策略的关键:
| # | 输入 | 建议替换为 | 降格原因 |
|---|---|---|---|
| 22 | Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY >= 0 | Number.POSITIVE_INFINITY >= Number.POSITIVE_INFINITY | 非严格比较遇到无穷值:Infinity - Infinity >= 0为false,而Infinity >= Infinity为true,改写会改变行为 |
| 23 | Number.POSITIVE_INFINITY - Number.POSITIVE_INFINITY === 0 | Number.POSITIVE_INFINITY === Number.POSITIVE_INFINITY | 同上(NaN === 0为false,Infinity === Infinity为true) |
| 24 | foo.length - bar.length >= 0 | foo.length >= bar.length | 非严格比较要求两侧是静态可知的有限数字,length不是 |
| 25 | foo.length - bar > 0 | foo.length > bar | 严格比较要求两侧都可证明是数字,bar无法证明 |
| 27 | a.length - b.length - c > 0 | a.length - b.length > c | 嵌套减法:右侧0实际对应的是c,改写保留左侧减法 |
| 29 | const 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'),无法静态定值 |
| 30 | const modes = new Set(['foo']); modes.clear(); ((modes.size && 1) || value) - 1 === 0 | ((modes.size && 1) || value) === 1 | 逻辑表达式静态值不可推断 |
| 31 | const object = {value: true}; Object.defineProperty(object, 'value', {get() { return false; }}); (object.value ? 1 : value) - 1 === 0 | (object.value ? 1 : value) === 1 | getter 定义无法被静态值分析穿透 |
| 32 | const modes = new Set(['foo']); modes.clear(); ((modes.size ? 1 : value) as number) - 1 === 0 | ((modes.size ? 1 : value) as number) === 1 | TypeScriptas number断言提供的是类型信息而非静态值,非严格比较不采信 |
| 33 | (a - b) > 0 | a > b | 整个减法被括号包裹,两侧仍是标识符 |
| 34 | a?.b - c?.d > 0 | a?.b > c?.d | 可选链结果可能为undefined,非数字 |
| 36 | (a as number) - (b as number) > 0 | (a as number) > (b as number) | (自动修复)严格比较中TSAsExpression断言为number被isNumber采信 |
分组五:表达式内含注释 → 只报错、不修复
| # | 输入 | 结果 |
|---|---|---|
| 35 | a - /* 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。 - 其余一律给 suggestion:
problem.suggest携带Replace with {{replacement}}消息和同一个fix函数,由用户确认后应用。覆盖分组一、二、四的大部分用例。
之所以对非严格比较加「静态有限数字」的更严限制,官方文档(docs/rules/no-subtraction-comparison.md)的 Caveats 一节给出了反例:Infinity - Infinity是NaN,导致Infinity - Infinity >= 0为false而Infinity >= Infinity为true;同样,"10" - "5" > 0为true而字符串按字典序"10" > "5"为false——重写只对数字才保证行为等价。
第四步:isNumber——「可证明是数字」的判定清单
autofix 与 suggestion 的分野很大程度取决于 rules/utils/is-number.js 中的isNumber。从源码结构看,它按 AST 形式白名单式地认可以下「数字证据」:数字字面量、Math静态属性(PI、E等)与静态方法调用(Math.round(...)等)、Number(...)调用、Number.EPSILON等静态属性、Number.parseInt/parseFloat调用、字符串的数字返回方法(charCodeAt、indexOf、localeCompare等,且接收方需可证明是字符串)、.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 > 0n | 0n是 BigInt 字面量,isLiteral(node, 0)不成立 |
a - b > -0 | -0是一元表达式而非0字面量 |
a - b instanceof c | instanceof不在 8 个受支持的操作符集合内 |
使用与验证方式
在 ESLint 中启用该规则(flat config 示例,规则前缀为unicorn):
// eslint.config.js export default [ { rules: { 'unicorn/no-subtraction-comparison': 'error', }, }, ];如果直接采用插件提供的recommended或unopinionated配置集,该规则已默认开启(见 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),仅供参考