重构遗留的x += 0没人敢删?no-useless-compound-assignment 帮你定位并安全清理
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
一次例行重构之后,代码库里多出一行total += 0;。它不影响运行,但没人敢删——万一背后有玄机呢?其实这类「恒等复合赋值」绝大多数是纯粹的残留物。eslint-plugin-unicorn 提供的no-useless-compound-assignment规则就是为此而生:自动找出这类不改变数值的赋值语句,并告诉你安全清理的正确姿势。
能力全景:一张只有五张「罚单」的质检单
这条规则管的范围非常窄:只有当复合赋值的右操作数恰好是「该运算的恒等值」时,才判定为无意义操作。规则源码 里用一张noOpAssignments映射表锁定了全部命中组合:
| 运算符 | 触发值 | 命中示例 |
|---|---|---|
+= | 0 | total += 0 |
-= | 0 | count -= 0 |
*= | 1 | price *= 1 |
/= | 1 | ratio /= 1 |
**= | 1 | base **= 1 |
除此之外,下列写法全部豁免,一眼就能建立全局印象:
- 所有位运算符(
|=、^=、&=、<<=、>>=、>>>=) - 取模恒等值
x %= 1 - 逻辑赋值(
||=、&&=、??=)与普通赋值(x = 0) - 右值不是纯数字字面量的所有情况(变量、函数调用、BigInt、带符号写法、TS 断言)
换个角度看,它就像一个只查五种证件的安检员:不在名单上的,一律放行。
判定边界拆解:四类被刻意放过的写法及原因
为什么豁免项这么讲究?每一条背后都有明确理由。
① 位运算强转惯用法。x |= 0、x >>>= 0、x &= -1这类写法在性能敏感代码里是刻意为之的 int32/uint32 强转手段,是「技巧」而不是「失误」。源码注释直接写明了排除动机。
②%= 1看起来像恒等,实际不是。x % 1得到的是 x 的小数部分,而不是 x 本身——除以 1 不变,取余 1 会「截」掉整数位,两者语义完全不同。
③ 非算术复合赋值不在射程内。x ||= 0、x &&= 1、x ??= 0以及普通赋值x = 0,无论值是否「看起来没意义」,规则一概不碰。
④ 右值必须能被静态确认为「纯数字字面量」。这是边界的核心,源码借助 isNumericLiteral(判断node.type === 'Literal' && typeof node.value === 'number')做硬校验,因此以下写法全部放行:
- BigInt:
x += 0n——类型不同,语义也不同; - 一元符号包裹:
x += -0、x *= +1——在 AST 中是UnaryExpression而非Literal,且-0的行为并不总是恒等; - 变量与调用:
x += y、x += foo()——运行时的值无从判断; - TypeScript 断言:
x += 0 as number——节点是TSAsExpression,不是纯字面量。
值得注意的是,字面量的「等价形态」逃不掉:0.0、1.0、0x1、0e3一样会被标记。因为 AST 保存的是规范化后的数字0或1,只是报错信息里会原样展示你写的原始文本(比如+= 0.0)。
那为什么x *= 0、x /= 0不报?因为它们根本不是恒等操作——乘 0 会把值清零,除以 0 得到Infinity,值确实被改变了。
核心设计取舍:为什么只给建议、不直接帮你删
「既然恒等,删掉不就完了?」——不能。因为复合赋值的实际效果取决于左值在运行时的真实状态,存在一批看似无操作、实则改状态的极端场景:
'a' += 0; // 变成字符串拼接,产出 'a0' '5' *= 1; // 字符串被隐式转成数字 5 5n *= 1; // BigInt 与 Number 混算,直接抛 TypeError // x 为 -0 时,x += 0 会把 -0 翻成 +0(IEEE 754 有符号零)💡 也就是说,规则只能确认「这大概率是死代码」,无法确认「删掉一定等价」。于是 规则元数据 只声明了hasSuggestions: true,不提供fix:错误照常上报,但清理动作以编辑器建议的形式摆出来,等你亲手点确认。这一步把「自动改写」降级为「人工确认」,是对运行时语义差异的保守让步。
修复形态:两种建议,各有一套触发条件
确认报错后,规则会看赋值表达式在 AST 中的上下文,二选一:
删除整句。当赋值是独立语句(ExpressionStatement),且直接挂在Program、BlockStatement、StaticBlock、SwitchCase之下时,建议「Remove the useless assignment」。底层调用 removeStatement 助手:连带吸收本行缩进与换行符,不留空行,且语句之外的尾随注释会保留——x += 0; // keep清理后注释还在。
替换为左值。当赋值结果被外部使用时,整行删除会破坏语义,此时建议把整个赋值表达式换成左值文本:
foo(x += 0)→foo(x)return count -= 0→return countconst y = (x *= 1)→const y = xfor (let i = 0; i < n; i += 0) {}→ 更新式变为i
左值即使是成员表达式也没问题:this.count -= 0、foo.bar.baz *= 1都能正确取文本。
还有一个隐蔽的边界:注释卡在赋值内部时,规则只报错、不出建议。比如x += /* keep */ 0;,无论删句还是替换都会吞掉这段注释,于是规则宁可保守,把清理工作留给人。
实战对照:错 → 对
// ① 独立语句 → 采纳「删除整句」建议 let total = getTotal(); total += 0; // ✅ 清理后只剩上一行 // ② 结果被 return 使用 → 采纳「替换为左值」建议 function report() { return count -= 0; // ✅ 清理后为 return count; } // ③ 注释位于赋值内部 → 只报错、无建议,手动清理 x += /* keep */ 0;对照 官方文档 的示例与 快照文件,可以逐条验证上面每个判定分支的真实输出。
如何启用:默认配置即生效
接入成本几乎为零:
- 该规则同时启用在
recommended与unopinionated两套预设中(文档头部以 ✅ / ☑️ 标注),使用插件预设配置时开箱即得; - 元数据为
type: 'suggestion',定位是「不掺杂强烈风格偏好」的规则; - 不接收任何自定义选项——五种恒等组合是硬编码的,也不开放配置开关。
在 readme.md 的规则总表中,它以 💡 标记归类于「提供编辑器建议」的规则一族。
小结:一套可移植的「三步走」实现模式
这条规则的参考价值不在规则本身,而在它的工程范式,拆开来正好三步:
- 恒等判定窄化:把「什么是无操作」固化为一张小小的映射表,且右值只认纯字面量——判定面越窄,误报率越低;
- 上下文分诊:动手前先看父节点,决定「能删」还是「必须保留」,再选删除或替换策略,让建议天然适配所在语句;
- 保守落地:运行时语义可能不同的场景,一律降级为建议而非自动修复;注释无法保全时,宁可只报错不出修复。
「判定恒等 → 区分上下文 → 保守修复」这条流水线,对任何想做冗余代码安全清理的静态分析工具,都是可以直接抄作业的骨架。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考