eslint-plugin-unicorn 规则深度解析:用 prefer-number-is-safe-integer 消除不安全的整数判断
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
本文以 eslint-plugin-unicorn 项目中的prefer-number-is-safe-integer规则文档(docs/rules/prefer-number-is-safe-integer.md)为核心,结合规则源码(rules/prefer-number-is-safe-integer.js)与测试用例(test/prefer-number-is-safe-integer.js),系统讲解 JavaScript 中"整数判断"的精度陷阱,以及该规则如何在Number.isInteger()、value % 1 === 0、Math.trunc()/Math.floor()比较、Lodash/UnderscoreisInteger()等常见写法中识别隐患,并提示改为Number.isSafeInteger()。读完本文,你将理解安全整数范围的含义、规则覆盖与刻意忽略的模式边界、为什么它只提供建议而非自动修复,以及如何结合源码判断哪些写法会被报告、哪些不会。
规则速览:它到底检查什么
prefer-number-is-safe-integer是一条suggestion(建议)类型的规则,核心目的是推广使用Number.isSafeInteger()替代各种"只判断整数性、不判断可精确表示性"的写法。从规则元数据(rules/prefer-number-is-safe-integer.js)可以看到:
type: 'suggestion':属于建议类规则,不直接改变运行时行为;recommended: true:该规则在recommended配置中默认开启(readme.md 的规则总表中用 ✅ 标注),而在unopinionated配置中保持关闭;hasSuggestions: true:通过editor suggestions(编辑器建议)提供手动可用的修复,而非自动修复;languages: ['js/js']:仅针对 JavaScript,TypeScript 需要借助解析器支持(测试中可见 TS 断言场景)。
从源码可见,规则实际定义了四条消息 ID,覆盖两类问题(rules/prefer-number-is-safe-integer.js):
prefer-number-is-safe-integer/error:针对Number.isInteger()调用的主报告;prefer-number-is-safe-integer/suggestion:对应的替换建议;prefer-number-is-safe-integer/integer-check-error:针对% 1 === 0、Math.trunc()/Math.floor()比较、Lodash/Underscore 调用等通用整数检查的报告;prefer-number-is-safe-integer/integer-check-suggestion:对应的替换建议。
为什么要用 Number.isSafeInteger():精度问题的本质
Number.isSafeInteger()检查一个值既是整数,又能被精确表示,即落在安全整数范围[-(2 ** 53 - 1), 2 ** 53 - 1]内。而Number.isInteger()只判断是否为整数,对超出该范围、已无法精确保存的"大整数"也会返回true——这几乎从来不是开发者想要的语义。
原文档给出的示例很直观地说明了差异:
// ❌ // This is problematic because Numbers larger than 2^53 - 1 lose precision const largeNumber = 9007199254740992; // 2^53 Number.isInteger(largeNumber); // true (misleading!) largeNumber === 9007199254740993; // true (precision lost!) // ✅ Number.isSafeInteger(largeNumber); // false (correctly identifies the issue)当数值超过2^53 - 1时,IEEE 754 双精度浮点无法区分相邻整数,9007199254740992与9007199254740993在内存中相同。此时Number.isInteger()给出的true具有误导性——它只证明"类型上是整数",无法证明"数值本身可靠"。
典型的业务场景是 ID、时间戳、序列号等数据:
// ❌ function processId(id) { if (!Number.isInteger(id)) { throw new Error('Invalid ID'); } // id could still be too large to represent exactly } // ✅ function processId(id) { if (!Number.isSafeInteger(id)) { throw new Error('Invalid ID'); } // id is guaranteed to be safely representable }规则覆盖的四类写法
原文档明确指出,该规则不仅报告Number.isInteger(),还报告常见的整数检查写法:
value % 1 === 0(及对称形式0 === value % 1)Math.trunc(value) === value与Math.floor(value) === value(含对称形式value === Math.trunc(value))- Lodash / Underscore 的
isInteger()/isSafeInteger()调用
1. Number.isInteger() 调用
这是最直接的命中场景。源码在CallExpression监听器里通过isMethodCall精确匹配(rules/prefer-number-is-safe-integer.js):要求调用对象是裸的全局Number(sourceCode.isGlobalReference保证不是window.Number、globalThis.Number或用户自定义的Number变量)、方法名为isInteger、非可选调用、非可选成员、非计算属性。命中后仅将isInteger标识符替换为isSafeInteger,其余参数原样保留。
// ❌ if (!Number.isInteger(index)) { throw new Error('Expected an integer.'); } // ✅ if (!Number.isSafeInteger(index)) { throw new Error('Expected a safe integer.'); }快照测试(test/snapshots/prefer-number-is-safe-integer.js.md)展示了真实输出:错误定位在isInteger标识符上,提示语为 "PreferNumber.isSafeInteger()overNumber.isInteger().",并附建议 "ReplaceNumber.isInteger()withNumber.isSafeInteger().",替换结果如!Number.isInteger(x)→!Number.isSafeInteger(x)。
2. 取模检查:value % 1 === 0
value % 1 === 0是流传已久的"手写整数检查",源码通过getModuloCheckArgument与getModuloIntegerCheckArgument识别(rules/prefer-number-is-safe-integer.js):要求是%二元表达式、右侧为字面量1、并与字面量0做严格相等比较,左右两侧均可。object.value % 1 === 0、(foo, bar) % 1 === 0这类成员表达式和序列表达式同样会命中。
// ❌ if (value % 1 === 0) { console.log('Integer'); } // ✅ if (Number.isSafeInteger(value)) { console.log('Safe integer'); }注意测试用例中明确排除了value % 1 == 0(宽松相等)、value % 1 !== 0(否定比较)和(value | 0) === value(位运算检查),这些都不会被报告。
3. Math 方法比较:Math.trunc() / Math.floor()
Math.trunc(value) === value与Math.floor(value) === value同样会被识别。源码getMathIntegerCheckArgument(rules/prefer-number-is-safe-integer.js)限定了严格条件:
- 必须是
Math.trunc/Math.floor方法调用(mathIntegerCheckMethods数组只含这两个); - 参数个数必须恰好为 1;
Math必须是全局引用;- 不可选调用、不可选成员、不可计算属性。
getMathComparisonIntegerCheckArgument(rules/prefer-number-is-safe-integer.js)进一步要求比较两侧通过isSameReference判定为同一引用(如Math.trunc(object.value) === object.value),否则不报告。以下形式均会被报告:Math.floor(value) === value、value === Math.floor(value)、Math.trunc(value) === value、value === Math.trunc(value)。
有意忽略的类似模式包括Math.round(value) === value(取整方向不同,语义不等价),以及Number.parseInt(value, 10) === value这类字符串解析比较。
4. Lodash / Underscore 的 isInteger() / isSafeInteger()
规则同样覆盖工具库调用。源码中的lodashObjects常量定义了['_', 'lodash', 'underscore']三种对象名(rules/prefer-number-is-safe-integer.js),getLodashIntegerCheckArgument(rules/prefer-number-is-safe-integer.js)匹配这些对象上的isInteger与isSafeInteger方法(1 个参数、非可选、非计算属性)。
// ❌ if (_.isInteger(value)) { console.log('Integer'); } // ✅ if (Number.isSafeInteger(value)) { console.log('Safe integer'); }_.isSafeInteger(value)、lodash.isSafeInteger(value)、underscore.isSafeInteger(value)也会被报告——因为既然已经显式表达了"安全整数"语义,直接使用原生Number.isSafeInteger()更为简洁统一。一个细节是:报告 Lodash 写法前,源码会调用isGlobalNumberAvailable(rules/prefer-number-is-safe-integer.js)确认当前作用域内的Number未被局部变量遮蔽,确保替换后的代码语义安全。
为什么只提供建议而不自动修复
原文档强调:该规则仅提供建议(suggestion),不做自动修复(automatic fix),因为各类检查并不完全等价。两个关键反例:
Number.isInteger(2 ** 53)返回true,而Number.isSafeInteger(2 ** 53)返回false——直接替换会改变程序行为;[['1']] % 1 === 0返回true(隐式强制转换后取模),而Number.isSafeInteger([['1']])返回false——% 1检查会做类型强制转换,替换后行为不同。
因此在应用建议前,必须逐个审查具体场景。尤其要注意否定检查:!Number.isInteger(x)换成!Number.isSafeInteger(x)后,"非整数"的判定集合会扩大(更多值被判为不合法),例如原本通过!Number.isInteger(2 ** 53)的值将变为不通过,这可能让校验逻辑更严格、也可能意外拦截合法数据。
源码中的工程细节:注释保护与表达保持
规则的createIntegerCheckProblem与hasCommentsOutsideNode([rules/prefer-number-is-safe-integer.js](https://link.gitcode.com/i/5f1edce8d963264272b8874e5f397501#L24-L31, L119-L135))处理了一个容易被忽略的边界:当被检查的表达式中带有注释时(如value /* comment */ % 1 === 0、_.isInteger(/* comment */ value)、Math.trunc(/* comment */ value) === value),替换整段表达式会导致注释丢失,因此规则仍然报告错误,但不提供建议——测试用例中明确标注了 "Reported without suggestion to avoid dropping comments"。
对于Number.isInteger(/* comment */ x),由于替换只作用于isInteger属性名,注释安全保留,所以照常给出建议。此外,getExpressionText(rules/prefer-number-is-safe-integer.js)会把序列表达式用括号包裹(如(foo, bar) % 1 === 0→Number.isSafeInteger((foo, bar))),保证替换后运算顺序不变。
精确识别:不会误报的边界情况
综合测试文件(test/prefer-number-is-safe-integer.js)中的 valid 用例,可以梳理出规则刻意放过的完整清单:
- 非调用形态:
Number.isInteger(仅引用)、Number.isInteger.bind(Number)、new Number.isInteger(x); - 对象被遮蔽或非全局:
const Number = {...}、import Number from "number"、函数参数function foo(Number)、window.Number.isInteger(x)、globalThis.Number.isInteger(x)、NotNumber.isInteger(x); - 可选调用 / 可选成员 / 计算属性:
Number.isInteger?.(x)、Number?.isInteger(x)、NumberisInteger、Number"isInteger"; - 大小写不匹配:
Number.isinteger(x); - 比较形式不合规:
value % 1 == 0(宽松)、value % 1 !== 0、0 !== value % 1、Math.trunc(value) !== value、Math.floor(value) == value; - 非等价比较:
Math.floor(value) === otherValue(两侧引用不同); - 刻意忽略的模式:
(value | 0) === value(位运算检查)、Number.parseInt(value, 10) === value、Math.round(value) === value; - Lodash 变体:
_.isInteger?.(value)、_?.isInteger(value)、_isInteger、_.isInteger(...value)、_.isInteger(value, extra)。
在 TypeScript 场景下,测试覆盖了Number.isInteger(x as number)、Number.isInteger(x!)(非空断言必须在建议中保留)、Math.trunc(value as number) === value、Math.trunc(value!) === value、Math.trunc(value) === value as number等断言写法,证明建议生成时会原样保留类型断言与运算符。
如何在项目中启用与使用
该规则已在recommended配置中默认开启(见 readme.md 规则总表中prefer-number-is-safe-integer一行的 ✅ 标注),使用 ESLint flat config 时直接继承 recommended 配置即可生效,无需额外配置:
import unicorn from 'eslint-plugin-unicorn'; export default [ unicorn.configs.recommended, // ...其他配置 ];若使用旧版 eslintrc 风格,则对应plugin:unicorn/recommended扩展。该规则没有任何可配置选项,行为完全由源码内置的匹配逻辑决定。
启用后,当编辑器(如 VS Code)集成了 ESLint 建议功能时,报告的代码行下方会显示建议提示(💡 标记),开发者可逐个审阅并手动应用替换,这正是该规则设计为 "suggestion" 而非 "fix" 的价值所在——让Number.isSafeInteger()的替换决策始终保留在人的判断中。
小结
prefer-number-is-safe-integer通过识别四类整数检查写法(Number.isInteger()、% 1 === 0、Math.trunc()/Math.floor()引用比较、Lodash/UnderscoreisInteger()/isSafeInteger()),引导开发者采用Number.isSafeInteger()这一语义更严谨的 API,从而规避2^53 - 1之外的大整数精度陷阱。它刻意只提供建议而非自动修复,并精确豁免了位运算、parseInt、Math.round等不强制转换的模式,同时在带注释表达式上放弃建议以保证注释不丢失——这些细节都值得在审计自己代码库中的整数校验逻辑时参照(完整测试矩阵见 test/prefer-number-is-safe-integer.js 与快照 test/snapshots/prefer-number-is-safe-integer.js.md)。对于处理 ID、时间戳、金额等数值型数据的项目,这是 recommended 配置中值得优先理解并落地的规则之一。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考