深入解读 eslint-plugin-unicorn 的 no-confusing-array-with 规则快照测试驱动的Array#with()误用检测【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn本篇文章以 eslint-plugin-unicorn 仓库中的 AVA 快照报告 test/snapshots/no-confusing-array-with.js.md 为骨架结合规则实现 rules/no-confusing-array-with.js、官方文档 docs/rules/no-confusing-array-with.md 与完整测试用例 test/no-confusing-array-with.js剖析该规则如何识别Array#with()的两种高危索引写法并解释快照中每条用例背后的判定原理。读者读完后将能准确理解规则的触发条件、边界行为与源码级实现也能读懂 AVA 快照报告的结构与信息。规则背景为什么Array#with()的索引需要被审查Array.prototype.with()是 ES2023 新增的不可变数组方法它返回一个替换了指定索引元素的新数组而不会修改原数组const array [1, 2, 3]; const next array.with(1, 9); // [1, 9, 3]与at()不同with()不支持负索引语义。传入负数或超出边界index array.length的索引会直接抛出RangeError。这意味着以下两种写法是典型的运行时雷区负数索引array.with(-1, value)在数组为空或长度不足时必然抛错即使数组非空也几乎总是与预期不符-1并不表示最后一个元素.length作为索引array.with(array.length, value)的索引恒等于数组长度永远超出有效范围[0, array.length - 1]必定抛错。正如 docs/rules/no-confusing-array-with.md 所述该规则故意不去识别 length 守卫length guards而是要求开发者始终传入一个明确有效clearly valid的索引。因此快照中的错误消息只有两种固定文案对应两个 messageIdmessageId错误消息触发场景negative-indexAvoid using a negative index with \Array#with().静态可求值的负数索引length-indexAvoid using \.length as the index in Array#with().| 与接收者同一对象的.length 访问两个 messageId 在 rules/no-confusing-array-with.js 中定义。该规则属于suggestion类型在 ✅recommended配置中默认开启在 ☑️unopinionated配置中关闭且schema: []表示不接受任何选项——启用即用无参数可配。读懂快照报告文件结构与错误定位方式快照文件 test/snapshots/no-confusing-array-with.js.md 由 AVA 测试框架自动生成对应二进制快照文件 test/snapshots/no-confusing-array-with.js.snap其每个用例块都包含三个关键部分用例标题invalid(n): 代码n 为该test.snapshot()分组内的序号Input被检测的原始源码带行号Error报告的消息文本、行号以及用^^^^标注的报告节点在源码中的精确区间。以第一条用例为例 1 | array.with(-1, value) | ^^^^ Avoid using a negative index with Array#with().^^^^恰好落在with标识符上。这是因为规则在命中后返回node: callExpression.callee.property见 rules/no-confusing-array-with.js即把错误定位在方法名本身而不是整个调用表达式或索引参数。这个细节说明规则认为问题不在于索引写错了而在于对with()的使用方式是危险的因此直接高亮方法调用处。用例一负数字面量与一元表达式的静态求值快照中 JS 分组的 12 条 invalid 用例invalid(1)~invalid(12)覆盖了负数索引与.length索引两类其中负数索引的 8 条展示了规则对静态可求值的严格定义用例代码判定结果invalid(1)array.with(-1, value)负数字面量invalid(2)array.with(-2, value)负数字面量invalid(3)array.with(-1.5, value)负数小数invalid(4)array.with(- 1, value)带空格的一元负号invalid(5)array.with(- - -1, value)三重一元负号嵌套invalid(6)array.with(-(-(-1)), value)括号化的一元负号嵌套invalid(7)array.with(-1)缺省第二个参数仍报告invalid(8)array.with(-1, value, extra)多余参数仍报告这些用例的执行逻辑来自isNegativeStaticIndexconst isNegativeStaticIndex node Math.trunc(getStaticNumberValue(node)) 0;rules/no-confusing-array-with.js其中getStaticNumberValue实现在 rules/utils/numeric.js它先解包 TypeScript 表达式然后对数值字面量直接返回node.value对/-一元表达式则递归求值其操作数并翻转符号。这正是- - -1、-(-(-1))这类绕圈写法仍被识别为负数的原因——无论嵌套多少层一元负号静态求值结果始终是-1。而Math.trunc()则负责处理小数-1.5截断后为-1同样落入 0分支。作为对照测试文件中test/no-confusing-array-with.js以下写法被认为是安全的array.with(0, value)/array.with(1, value)/array.with(1, value)非负索引array.with(-0, value)Math.trunc(-0) 0为false-0实际等于0array.with(-0.5, value)截断后为-0不报array.with(- -1, value)与array.with(-(-(1)), value)嵌套负号求值为1array.with(array.length - 1, value)length - 1是最后一个元素的合法索引规则刻意放行——这与官方文档不检测 length 守卫请使用明确有效的索引的立场完全一致array.with(otherArray.length, value)非同一对象的.length不触发length-index消息。用例二.length索引与对象身份比较快照中invalid(9)~invalid(12)展示了length-index分支的判定if (isLengthOf(indexNode, object)) { return MESSAGE_ID_LENGTH_INDEX; }isLengthOf来自 rules/utils/comparison.js其判定条件有两层结构匹配索引节点必须是非可选、非计算属性的object.length成员表达式optional: false、computed: false因此array?.length、array[length]这类写法不会命中对象身份匹配isSame(node.object, object)要求索引所属对象与with()的接收者是同一个表达式节点。这正是快照中的关键区分点array.with(array.length, value)同一对象array的.length→ 报错object.items.with(object.items.length, value)接收者object.items与索引所属对象object.items是同一个成员表达式 → 报错而array.with(otherArray.length, value)接收者array与索引对象otherArray不同 → 不报。同时getMessageId对索引节点先做unwrapExpression解包在isLengthOf内部再走isLengthOf判断保证了对 TypeScript 包装语法的一致处理见下文 TS 分组。用例三TypeScript 语法包装下的穿透检测快照的第二分组invalid(1)~invalid(17)共 17 条全部运行在 TypeScript parser 下languageOptions.parser parsers.typescript验证规则能穿透常见的 TS 类型语法包装依然命中。核心机制是getStaticNumberValue开头调用的unwrapTypeScriptExpression以及isLengthOf内的unwrapExpression两者会剥离TSAsExpression、TSTypeAssertion、TSNonNullExpression、TSSatisfiesExpression、TSParenthesizedExpression等包装节点。快照中可归为三类负数索引被 TS 包装invalid(1)~invalid(4)array.with(-1 as const, value) // TSAsExpression array.with(number-1, value) // TSTypeAssertion array.with(-1!, value) // TSNonNullExpression array.with(-1 satisfies number, value) // TSSatisfiesExpression四条全部报告negative-index错误定位仍然指向with属性。.length索引被 TS 包装invalid(5)~invalid(9)array.with(array.length as number, value) array.with(numberarray.length, value) array.with(array.length!, value) array.with(array.length satisfies number, value) array.with((array satisfies number[]).length, value)注意invalid(9)的包装位置在对象侧array satisfies number[]后再取.lengthisLengthOf的对象身份比较在解包后依然成立。同时测试文件中对应的 valid 用例array.with(0 as const, value)、array.with(number0, value)、array.with(0!, value)、array.with(0 satisfies number, value)证明TS 包装不会改变底层数值的符号判断——包装后仍为正数就不报。接收者被 TS 包装invalid(10)~invalid(12)(array satisfies number[]).with(array.length, value) object.items.with((object satisfies {items: unknown[]}).items.length, value) (object satisfies {items: unknown[]}).items.with(object.items.length, value)这三条验证接收者或索引对象即使被satisfies包装对象身份比较isSame依然能够匹配规则不会因包装而漏报。用例四类型信息参与——已知非数组接收者过滤快照 TS 分组的invalid(13)~invalid(17)是最能体现该规则深度的一组它们把TypeScript 类型信息引入判定function f(foo: number[]) { foo.with(-1, value); } // 数组 → 报 function f(foo: Uint8Array) { foo.with(-1, value); } // 类型化数组 → 报 const foo new Uint8Array(); foo.with(-1, value); // 类型化数组 → 报 function f(foo: Uint8Array | number[]) { foo.with(-1, value); } // 联合含数组 → 报 function f(foo: number[] | Setnumber) { foo.with(-1, value); } // 联合含数组 → 报对照测试文件中的 valid 用例test/no-confusing-array-with.js可以反推出跳过条件function f(foo: Setnumber) { foo.with(-1, value); }→不报Set是已知的非数组类型function f(foo: {with(index: number, value: unknown): void}) { foo.with(-1, value); }→不报自定义类型恰好声明了同名with方法属于另一套 APIconst foo new Foo(); foo.with(-1, value);→不报任何new Foo()构造的接收者除new Array()外都被视为非数组——即便是继承了Array的类也会被跳过这是刻意设计而非 bug。这套过滤逻辑实现在 rules/utils/should-skip-known-non-array-receiver.js规则内调用如下if (!messageId || shouldSkipKnownNonArrayReceiver(object, context)) { return; }rules/no-confusing-array-with.js其要点可归纳为字面量/表达式直接报告接收者是ArrayExpression数组字面量、FunctionExpression、Literal、ObjectExpression、TemplateLiteral时directlyReportableReceiverTypes直接判定可报告——因为调用点即可见类型不匹配类型化数组仍报告Uint8Array等类型化数组共享了Array的绝大多数方法面包括with()及相同的负索引行为因此shouldSkipKnownNonArrayReceiver明确不跳过它们联合类型全成员为数组才报Uint8Array | number[]、number[] | Setnumber这种联合类型只要有一个成员是数组就报告反之Setnumber单独出现或类型全是非数组成员时才跳过。安全边界可选链与方括号调用快照与测试文件共同勾勒出规则的另一组安全边界。以下写法不会被报告见 test/no-confusing-array-with.jsarraywith方括号字符串访问方式被排除。这是因为isMethodCall检查的是方法名的标识符形式optionalMember: falsearray?.with(-1, value)与array.with?.(-1, value)涉及可选链optionalCall: false、optionalMember: false接收者或调用本身可能不成立规则不冒险判定with_(-1, value)isMethodCall要求调用是成员调用object.with(...)普通函数调用自然被排除。这些边界条件全部集中在create中对isMethodCall的配置上rules/no-confusing-array-with.js并且要求minimumArguments: 1索引参数至少存在。快照invalid(7)的array.with(-1)表明即使省略第二个参数value规则照样报告——危险的不是缺参数而是负数索引本身invalid(8)的array.with(-1, value, extra)则证明多余参数不影响判定。修复建议与最佳实践基于规则的两条错误消息官方文档给出了对应的修复范式docs/rules/no-confusing-array-with.md负索引场景——用显式的长度守卫 length - 1替代// ❌ 数组为空时抛 RangeError const result array.with(-1, value); // ✅ 显式处理空数组且使用合法索引 const result array.length 0 ? array : array.with(array.length - 1, value);.length索引场景——语义上想要追加元素时应直接用展开或push的不可变写法// ❌ array.length 永远超出 [0, length-1] 范围 const result array.with(array.length, value); // ✅ 追加元素的不可变写法 const result [...array, value];如果只是想读取最后一个元素应改用array.at(-1)支持负索引想修改最后一个元素则先做空数组判断再用length - 1。规则只负责指出危险调用具体策略由开发者按语义选择。延伸阅读规则完整源码rules/no-confusing-array-with.js规则官方文档docs/rules/no-confusing-array-with.md完整测试用例valid/invalid 全量清单test/no-confusing-array-with.js快照二进制版本test/snapshots/no-confusing-array-with.js.snap底层工具静态数值求值 rules/utils/numeric.js、.length身份比较 rules/utils/comparison.js、非数组接收者过滤 rules/utils/should-skip-known-non-array-receiver.js【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考