news 2026/9/18 3:35:47

eslint-plugin-unicorn 规则深度解析:用 prefer-number-is-safe-integer 消除不安全的整数判断

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eslint-plugin-unicorn 规则深度解析:用 prefer-number-is-safe-integer 消除不安全的整数判断

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 === 0Math.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 === 0Math.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 双精度浮点无法区分相邻整数,90071992547409929007199254740993在内存中相同。此时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) === valueMath.floor(value) === value(含对称形式value === Math.trunc(value)
  • Lodash / Underscore 的isInteger()/isSafeInteger()调用

1. Number.isInteger() 调用

这是最直接的命中场景。源码在CallExpression监听器里通过isMethodCall精确匹配(rules/prefer-number-is-safe-integer.js):要求调用对象是裸的全局NumbersourceCode.isGlobalReference保证不是window.NumberglobalThis.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是流传已久的"手写整数检查",源码通过getModuloCheckArgumentgetModuloIntegerCheckArgument识别(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) === valueMath.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) === valuevalue === Math.floor(value)Math.trunc(value) === valuevalue === 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)匹配这些对象上的isIntegerisSafeInteger方法(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)的值将变为不通过,这可能让校验逻辑更严格、也可能意外拦截合法数据。

源码中的工程细节:注释保护与表达保持

规则的createIntegerCheckProblemhasCommentsOutsideNode([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 === 0Number.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)NumberisIntegerNumber"isInteger"
  • 大小写不匹配Number.isinteger(x)
  • 比较形式不合规value % 1 == 0(宽松)、value % 1 !== 00 !== value % 1Math.trunc(value) !== valueMath.floor(value) == value
  • 非等价比较Math.floor(value) === otherValue(两侧引用不同);
  • 刻意忽略的模式(value | 0) === value(位运算检查)、Number.parseInt(value, 10) === valueMath.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) === valueMath.trunc(value!) === valueMath.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 === 0Math.trunc()/Math.floor()引用比较、Lodash/UnderscoreisInteger()/isSafeInteger()),引导开发者采用Number.isSafeInteger()这一语义更严谨的 API,从而规避2^53 - 1之外的大整数精度陷阱。它刻意只提供建议而非自动修复,并精确豁免了位运算、parseIntMath.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),仅供参考

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

提示词工程实战指南:十个稳定输出技巧与模板库

1. 为什么你调了半年Prompt,结果还是忽好忽坏我早先犯过一个典型的错误:把生成式AI当成一个“会猜心的同事”,总以为只要把问题讲得足够清楚,它就能给出我想要的答案。后来做了几十个真实项目才发现,稳定输出从来不是“…

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

Adobe XD堆栈与状态:打造会呼吸的响应式UI设计稿

/* 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:33:40

研究情报库检索 RSI,TaoToken 放在向量化脚本

/* 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:33:39

3ds Max 2027工业级建模工作流实战指南

/* 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:31:53

闲置盒子变服务器:RK3568 刷 Armbian 的 4 步改造攻略

闲置盒子变服务器:RK3568 刷 Armbian 的 4 步改造攻略 【免费下载链接】amlogic-s9xxx-armbian Supports running Armbian on Amlogic, Allwinner, and Rockchip devices. Support a311d, s922x, s905x3, s905x2, s912, s905d, s905x, s905w, s905, s905l, rk3588, …

作者头像 李华