ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

eslint-plugin-unicorn 之 no-loop-iterable-mutation:禁止在 for...of 迭代中修改被遍历的集合

eslint-plugin-unicorn 之 no-loop-iterable-mutation:禁止在 for...of 迭代中修改被遍历的集合 eslint-plugin-unicorn 之 no-loop-iterable-mutation禁止在 for...of 迭代中修改被遍历的集合【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn导读no-loop-iterable-mutation是 eslint-plugin-unicorn 提供的一条problem类型规则用于在静态分析阶段拦截边遍历边修改被遍历集合的危险写法。在for...of循环体内对正在被迭代的Array、Set、Map调用修改型方法会导致元素被跳过、重复访问或出现预期外的遍历顺序。阅读本文后你将掌握该规则的完整判定范围、被放行的安全模式如快照拷贝、同条目更新、Set/Map的删除-重插特例以及规则背后基于活迭代器引用 类型感知的源码实现原理。规则背景为什么不能在迭代中修改被遍历的集合JavaScript 的for...of循环基于迭代器协议工作循环开始时引擎从被遍历对象上取得一个活的迭代器每次迭代调用next()拉取下一个值。一旦迭代开始迭代器与底层集合是绑定的此时对集合本身的修改会直接影响迭代器的前进过程。例如对一个数组执行push新元素会追加到迭代序列尾部导致循环多遍历出本不该出现的元素执行shift会让所有元素前移迭代器会跳过原本应该访问的元素对Set、Map执行delete再add同一元素则会改变该条目在集合中的位置产生重复或重排。eslint-plugin-unicorn 将这类问题归结为一条规则不要在遍历过程中修改被遍历的集合本身。该规则在meta.docs中声明为recommended: true因此它随 ✅recommended配置默认启用在 ☑️unopinionated配置中则处于关闭状态见 configs 与 rules/index.js 的导出声明。规则的错误消息定义为Do not mutate {{iterable}} while iterating over it.其中{{iterable}}会替换为被修改的迭代对象在源码中的实际文本见 rules/no-loop-iterable-mutation.js。规则覆盖的修改方法与集合类型规则的核心判定建立在集合类型 → 可修改方法的映射表上。源码通过mutationMethodsByCollectionKind维护了三组方法集合同时定义了一个unknown兜底类型rules/no-loop-iterable-mutation.js集合类型被禁止的修改方法ArraycopyWithin、fill、pop、push、reverse、shift、sort、splice、unshiftSetadd、clear、deleteMapclear、delete、setunknown无法确定类型时上述全部方法并集当集合类型无法通过静态分析确定时规则采用最保守的unknown集合即任何可能修改数组、集合或映射的方法调用都会触发报告避免漏报。同时规则只关心活迭代器场景。被遍历对象可以是直接引用例如for (const item of items)通过.keys()、.values()、.entries()获得的迭代视图例如for (const key of map.keys())。这三类迭代方法与直接遍历统一由iteratorMethods与getLiveIterable()识别rules/no-loop-iterable-mutation.js。识别时要求调用必须是零参数的成员方法调用、对象部分是普通引用并且不包含可选链如items?.values()不会被当作活迭代器。被禁止的写法示例以下代码均会被规则报告对应文档 Examples 与 test/no-loop-iterable-mutation.js 中的 invalid 用例// ❌ 数组push 会往迭代尾部追加新元素 for (const item of items) { items.push(item.clone()); } // ❌ 数组shift 会打乱迭代器位置导致元素被跳过 for (const value of values) { values.shift(); } // ❌ Setdelete 后再 add 同一元素会改变元素在集合中的位置 for (const value of set) { set.delete(value); set.add(value); } // ❌ Setclear 清空整个集合 for (const value of set.values()) { set.clear(); } // ❌ Map直接遍历时按 key 写入新值非当前条目 for (const [key, value] of map) { map.set(otherKey, value); } // ❌ 数组通过计算属性调用同样会被识别 for (const item of items.values()) { itemspush; } // ❌ 数组Optional call 语法也会被报告 for (const item of items) { items.push?.(item); }从测试用例可以看到除了直接调用外以下变体同样在报告范围内items?.push(item)可选链调用itemspush计算属性访问this.items.push(item)、object.items.push(item)、collections[index].push(item)成员表达式与计算索引引用(items).push(item)带括号的引用for (const index of items.keys())中调用items.pop()。被放行的安全模式1. 快照拷贝后再修改先通过展开语法或Array.from()创建集合快照再对原集合进行修改是安全的// ✅ 先拷贝成快照再修改原数组 for (const item of [...items]) { items.push(item.clone()); } // ✅ Array.from 同样安全 for (const item of Array.from(items)) { items.push(item); }2. 遍历快照视图Object.keys / values / entriesObject.keys()、Object.values()、Object.entries()返回的是对象属性的静态快照数组而不是对象本身的活迭代器因此遍历它们并修改原对象是安全的// ✅ 遍历 Object.keys 快照逐个删除属性 for (const key of Object.keys(object)) { delete object[key]; } // ✅ 遍历 Object.values 快照给对象赋值 for (const value of Object.values(object)) { object.extra value; } // ✅ 遍历 Object.entries 快照 for (const [key] of Object.entries(object)) { delete object[key]; }这对应文档中snapshot loops fromObject.keys(),Object.values(), andObject.entries()的放行声明。3. Set / Map 的同条目更新same-current-entry对于Set和Map规则区分了只触碰当前正在遍历的条目与改动其他条目两种情况只放行前者// ✅ Set只 add 当前条目若已存在add 不改变位置 for (const value of set) { set.add(value); } // ✅ Set只 delete 当前条目 for (const value of set) { set.delete(value); } // ✅ Map用当前 key 覆盖当前条目的 value for (const key of map.keys()) { map.set(key, newValue); } // ✅ Map删除当前 key for (const [key] of map) { map.delete(key); } // ✅ Setdelete 当前条目后 add 回当前条目同一条目更新 for (const value of set) { set.delete(value), set.add(value); }这些场景在 test/no-loop-iterable-mutation.js 的 valid 用例中被大量覆盖包括set.keys()、set.values()、set.entries()、map.entries()以及计算属性形式set[keys]()的同条目操作。4. 有意忽略的场景规则明确不做报告的情形包括迭代器别名先const iterator items.values()再遍历iterator此时修改items不会被识别为修改活迭代对象非方法赋值如array.length 0这类直接属性写入不属于方法调用函数 / 类 / 箭头函数体内的修改规则通过skippedNodeTypes跳过ArrowFunctionExpression、ClassDeclaration、ClassExpression、FunctionDeclaration、FunctionExpression节点rules/no-loop-iterable-mutation.js因为延迟到函数体内执行的修改是否发生在迭代期间无法静态确定嵌套作用域遮蔽若内层重新声明了同名变量规则通过变量绑定分析isSameBinding确认引用的是不同变量后放行for await...of异步迭代规则入口显式跳过带await的循环node.await非修改型方法如对数组调用items.add(item)、对Set调用set.push(value)只要方法不在对应类型的修改表中就不报告。关键判定逻辑的源码实现活迭代器识别getLiveIterable规则只在ForOfStatement退出时context.onExit启动分析rules/no-loop-iterable-mutation.js。getLiveIterable()负责解析被遍历表达式若是普通引用直接返回{collectionKind, reference, method: direct}若是obj.keys() / obj.values() / obj.entries()形式的零参数调用则把obj作为迭代对象引用返回一旦出现可选链?.、带参数调用或非引用对象立即放弃识别返回undefined后续不再报告。这保证了规则不会误伤items.values(argument)这类带参调用不是标准迭代视图或items?.values()结果不确定。类型感知isArray / isSet / isMap集合类型通过getCollectionKind()判定它复用仓库的utils/index.js导出的isArray、isSet、isMap工具rules/utils/index.js。这些工具基于 type-helpers.js 的createBuiltinTypeCheckers构建是类型感知type-aware的判定器在普通 JS 下通过静态值推断getStaticType判断value instanceof Set/Map等在 TypeScript 项目里还能结合类型注解如Setstring、ReadonlySet、Mapstring, number、ReadonlyMap别名做出更精确的判定。因此同一个set.add(value)调用当set被判定为Set时add 当前条目会被放行而当集合类型无法确定unknown时add出现在并集表中行为与Set相同的同条目放行逻辑不再适用会按修改方法直接报告——测试中的const items []; for (const item of items) { items.add(item); }正是这种兜底行为的验证。同条目放行的三要素规则通过getLoopInformation()从循环绑定binding中提取三个关键标识符用于判断修改是否命中当前条目rules/no-loop-iterable-mutation.jsaddArgumentIdentifieradd的参数应等于当前条目setKeyIdentifierset的第一个参数应等于当前 keydeleteArgumentIdentifierdelete的参数应等于当前条目/key。这些标识符的提取因迭代方式而异entries()迭代取绑定解构数组的第一个元素for (const [key, value] of map.entries())中的keykeys()迭代取绑定本身直接遍历Set取绑定本身直接遍历Map时没有可用的 key解构出的第一项即 key规则据此识别当前 key。随后hasSameFirstArgument()通过isSameReferenceBinding()做引用级比对不仅要求 AST 节点文本相同还会借助findVariable确认两侧标识符解析到同一个变量绑定rules/no-loop-iterable-mutation.js从而正确区分内层同名变量遮蔽的场景。删除-重插模式hasEarlierCurrentDelete文档特别强调Delete-then-reinsert patterns are not allowed先删除再重插的模式不被允许。源码中的对应逻辑是add/set当前条目之所以被放行前提是该循环体内此前没有对同一条目执行过delete。hasEarlierCurrentDelete()会回溯调用点之前的兄弟语句并通过hasDirectCurrentDeleteStatement()递归检查BlockStatement与IfStatement分支配合trackBranchExits追踪分支是否必然退出rules/no-loop-iterable-mutation.js。因此// ❌ delete 当前条目后再 add视为先删后插位置已改变 for (const value of set) { set.delete(value); set.add(value); } // ✅ 分支互斥delete 和 add 不在同一次迭代中同时发生 for (const value of set) { if (shouldDelete) { set.delete(value); } else { set.add(value); } } // ✅ 中间有 continue/break 提前退出同样安全 for (const value of set) { if (shouldDelete) { set.delete(value); continue; } set.add(value); }这一分支感知分析在测试中覆盖了IfStatement互斥、switch分支以及continue提前跳出的多种组合。嵌套循环与去重报告规则同样处理嵌套遍历同一集合的场景。当内层for...of也遍历同一个活迭代对象时会生成nestedLoopInformation此时内层对内层当前条目的add/set会被放行因为内层迭代器位置取决于内层条目而对内层条目的delete仍会报告rules/no-loop-iterable-mutation.js。例如// ✅ 内层遍历中只操作内层当前条目 for (const value of set) { for (const innerValue of set) { set.add(innerValue); } } // ❌ 内层遍历中删除内层当前条目 for (const value of set) { for (const innerValue of set) { set.delete(innerValue); } }此外规则使用reportedCallExpressionsWeakSet保证同一个调用表达式只报告一次避免多层级遍历分析时重复输出。配置与使用方式在 ESLint 配置中启用该规则无需任何选项meta.schema为空数组。在经典eslintrc风格下{ plugins: [unicorn], rules: { unicorn/no-loop-iterable-mutation: error } }在 flat config 风格下import unicorn from eslint-plugin-unicorn; export default [ { plugins: {unicorn}, rules: { unicorn/no-loop-iterable-mutation: error, }, }, ];如果使用了仓库自带的 ✅recommended配置则无需手动声明即可生效。规则元数据属性值规则 IDunicorn/no-loop-iterable-mutation规则类型problem推荐配置✅recommended启用☑️unopinionated禁用配置选项无schema: []适用语言js/js见meta.languages消息模板Do not mutate {{iterable}} while iterating over it.总结no-loop-iterable-mutation用活迭代器 类型感知双重判定把迭代中修改被遍历集合这一隐性运行时问题提前到静态分析阶段暴露它覆盖Array、Set、Map三类集合在直接遍历与.keys()/.values()/.entries()迭代视图下的全部修改型方法同时精确放行快照遍历、Object.keys/values/entries快照循环、Set/Map同条目更新与分支互斥的删除-重插模式。理解了它的方法映射表、同条目识别与先删后插回溯逻辑你既能写出被该规则认可的迭代代码也能在阅读其他 ESLint 规则时快速定位其核心判定路径。参考实现与测试规则实现rules/no-loop-iterable-mutation.js规则注册rules/index.js测试用例test/no-loop-iterable-mutation.js类型判定工具rules/utils/is-set.js、rules/utils/is-map.js、rules/utils/type-helpers.js【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表