尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

eslint-plugin-unicorn prefer-classlist-toggle 规则全解析:从快照测试到自动修复的源码级指南

eslint-plugin-unicorn prefer-classlist-toggle 规则全解析:从快照测试到自动修复的源码级指南 eslint-plugin-unicorn prefer-classlist-toggle 规则全解析从快照测试到自动修复的源码级指南【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn条件式切换 CSS 类名是前端代码中最常见的 DOM 操作之一而if/else、三元表达式或计算属性名等写法往往让同一个「布尔意图」被拆散在多个分支中。eslint-plugin-unicorn的prefer-classlist-toggle规则专为此而生它识别所有条件化调用classList.add()/classList.remove()的等价写法并自动重写为一次Element#classList.toggle()调用。本文以该规则的 AVA 快照测试报告test/snapshots/prefer-classlist-toggle.js.md共 1598 行、54 个无效用例为骨架结合规则源码 rules/prefer-classlist-toggle.js 与测试用例 test/prefer-classlist-toggle.js逐层拆解规则的全部检测模式、自动修复逻辑与边界处理。读完本文你将能准确预判该规则在任意复杂表达式下的报错与修复行为并在自己的项目中正确启用与验证它。规则定位为什么用classList.toggle()代替条件调用根据规则官方文档 docs/rules/prefer-classlist-toggle.md该规则鼓励使用Element#classList.toggle()而不是条件性地调用classList.add()和classList.remove()toggle()expresses the boolean intent in one operation and avoids keeping separate add and remove branches in sync.即toggle()用一次操作表达了布尔意图并避免 add/remove 两个分支长期维护不同步。它同时支持两个参数形态element.classList.toggle(className)—— 不传布尔值由浏览器按当前是否存在自动切换element.classList.toggle(className, condition)—— 传入显式布尔值强制开关。该规则在 readme.md 的规则清单中标记为 可由--fix自动修复与 可由编辑器 suggestion 手动修复官方文档标注其被recommended与unopinionated两套预设配置启用规则元数据中type: suggestion、fixable: code、hasSuggestions: true见 rules/prefer-classlist-toggle.js并且目前限定作用于js/js语言。快照测试报告是什么解读 AVA 快照文件结构关联文档test/snapshots/prefer-classlist-toggle.js.md是由 AVA 测试框架自动生成的快照报告Markdown 可读版本声明实际快照保存在prefer-classlist-toggle.js.snap对应二进制/文本快照文件 test/snapshots/prefer-classlist-toggle.js.snap。它的每个条目固定包含三部分Input—— 触发规则的原始源码带行列号Message—— 报告的错误位置^标注范围与消息文本Output / Suggestion—— 自动修复fix或编辑器建议suggestion产出的目标代码。报告中出现的两条核心消息与源码messages定义rules/prefer-classlist-toggle.js一一对应const MESSAGE_ID_ERROR prefer-classlist-toggle/error; const MESSAGE_ID_SUGGESTION prefer-classlist-toggle/suggestion; const messages { [MESSAGE_ID_ERROR]: Prefer using Element#classList.toggle() to toggle class names., [MESSAGE_ID_SUGGESTION]: Replace with Element#classList.toggle()., };快照中的 54 个无效用例按三大检测模式分组下面逐一展开。检测模式一if/else语句中分支调用 add/remove这是快照报告的第一大组26 个用例也是规则监听IfStatement节点源码第 136 行context.on([IfStatement, ConditionalExpression], …)的主战场。规则对两个分支做归一化处理BlockStatement若只有一条语句则解包其唯一语句再逐层剥开ExpressionStatement与ChainExpression源码第 137-156 行。基本形态与取反最常见的触发形态快照 invalid(1)// Input if (condition) { element.classList.add(className); } else { element.classList.remove(className); } // Output element.classList.toggle(className, condition);规则要求两个分支必须是同一元素、同一类名上互为相反的add/remove调用源码中isSameElementAndClassName用isSameReference校验元素与类名引用一致第 90-92 行。当add在consequent、remove在alternate时条件原样作为toggle()第二参数若分支顺序颠倒快照 invalid(7)// Input if (condition) { element.classList.remove(className); } else { element.classList.add(className); } // Output element.classList.toggle(className, !condition);remove位于第一个分支意味着条件为真时应当移除类因此第二参数被取反为!condition。这一逻辑在源码中由const isNegative consequent.callee.property.name remove判定rules/prefer-classlist-toggle.js。无块语句与混合块形式if语句不写花括号同样会被识别报告 invalid(2)、invalid(3) 展示了if (condition) stmt; else stmt;以及单侧带花括号的混合形式修复结果完全一致——这正是源码中BlockStatement单语句解包逻辑的价值。可选链optional chaining保留invalid(4)、invalid(5) 验证了当元素访问带可选链时修复必须保留?.// Input if (condition) { element?.classList.add(className); } else { element.classList.remove(className); } // Output element?.classList.toggle(className, condition);源码中isOptional判定会检查consequent.callee.object.optional或alternate.callee.object.optional任一为真第 183 行因此只要其中一个分支用了element?.输出的toggle()也使用可选调用。括号保留与序列表达式invalid(8) 中(( element )).classList的多余括号被完整保留在输出中(( element )).classList.toggle(className, condition);invalid(9)、invalid(10) 则验证了if (0, condition)这类SequenceExpression条件输出同样保留括号element.classList.toggle(className, (0, condition))。这些行为对应源码getConditionText的两条路径第 55-79 行SequenceExpression必须加括号防止语法歧义一元!取反时通过shouldAddParenthesesToUnaryExpressionArgument判断是否需要包裹括号。ASI自动分号插入防护invalid(12)、invalid(13) 是极其细致的工程化处理。当if语句前一行以表达式结尾、或语句后紧跟数组方法调用时若直接替换可能因缺少分号改变语义规则会在输出前前置分号// invalid(12) 输出 foo ;(( element )).classList.toggle(className, condition); // invalid(13) 输出 (( element )).classList.toggle(className, condition); [].forEach(foo);源码通过needsSemicolon(sourceCode.getTokenBefore(node), context, text)检查前一个 token 与上下文是否需要分号需要则在修复文本前拼接;第 191-193 行。TypeScript 语法支持invalid(25)、invalid(26) 使用 TypeScript 解析器验证了类型断言与非空断言场景// invalid(25) (element as Element).classList.toggle(className, condition); // invalid(26) element!.classList.toggle(className, condition);对应的测试用例在 test/prefer-classlist-toggle.js 中通过languageOptions: {parser: parsers.typescript}指定 TS 解析器运行说明该规则可在 TS 项目中直接生效。检测模式二三元表达式调用 add/remove第二大组9 个用例监听ConditionalExpression节点识别condition ? element.classList.add(x) : element.classList.remove(x)形态快照该组 invalid(1)// Input condition ? element.classList.add(className) : element.classList.remove(className) // Output element.classList.toggle(className, condition)反向分支invalid(4)同样取反element.classList.toggle(className, !condition)可选链分支invalid(2)、invalid(3)保持element?.classList.toggle(...)。值得注意的是当三元表达式嵌入语句上下文时规则退化为提供 suggestion 而非直接修复。快照该组 invalid(5)、invalid(6) 中三元表达式分别作为if (...)的 test 和return的值输出均显示为 Suggestion 1/1: Replace withElement#classList.toggle(). 而非直接改写。这正是源码getProblem中的分流逻辑第 38-49 行const shouldUseSuggestion valueNode.type ! IfStatement !(isExpressionStatement(valueNode) || isExpressionStatement(valueNode.parent)); if (shouldUseSuggestion) { problem.suggest [{messageId: MESSAGE_ID_SUGGESTION, fix}]; } else { problem.fix fix; }只有当值节点本身就是IfStatement、或是或直接位于ExpressionStatement时才能安全自动修复否则如作为if条件、return值等表达式位置改为给出编辑器建议避免自动改动可能带来的副作用。检测模式三计算属性名选择 add/remove第三大组19 个用例对应源码中独立的第二个context.on(ConditionalExpression, …)监听第 206-243 行识别element.classListcondition ? add : remove这种用三元表达式动态选择方法名的写法。判定条件第 209-219 行非常严格三元两个分支必须是add/remove字符串字面量且值不同该三元必须是某个MemberExpression的计算属性computed: true该成员表达式必须是非可选的classList属性访问外层必须是对该成员的单参数调用且调用不可选。快照该组 invalid(1)// Input element.classListcondition ? add : remove // Output element.classList.toggle(className, condition)反向invalid(2)输出element.classList.toggle(className, !condition)可选链元素invalid(3)输出element?.classList.toggle(className, condition)。当调用出现在箭头函数体内invalid(4)const toggle (element) element.classList[...]时与模式二同理降级为 suggestion。修复实现上该模式采用增量修补而非整体替换fix先通过insertTextAfter在类名参数后插入, condition若条件文本非空再用replaceMemberExpressionProperty把计算属性访问替换为.toggle第 229-240 行。条件为复杂表达式时括号逻辑同样生效index % 2会输出!(index % 2)invalid(6)(index % 2)保持!(index % 2)invalid(7)(0, condition)保持(0, condition)invalid(8)。特殊优化contains条件自动折叠三组快照中都包含一组以element.classList.contains(className)作为条件的用例对应源码中getClassListContainsCall第 94-113 行实现的语义化简当条件本身就是检查同一元素是否包含同一类名时整个表达式等价于无参toggle()无需再传布尔值。典型如快照 IfStatement 组 invalid(14)// Input if (element.classList.contains(className)) { element.classList.remove(className); } else { element.classList.add(className); } // Output element.classList.toggle(className);源码逻辑是若条件剥开!前缀取反、剥开ChainExpression后是一个classList.contains(...)调用且与分支中的 add/remove 调用元素与类名均相同则conditionText置空第 182 行toggle()省略第二参数。此处必须强调contains与分支方法调用共享同一元素与类名是优化成立的前提快照中的反例恰好从反面验证了规则不会误优化invalid(19)条件查的是not-same-class-name而分支操作className→ 输出element.classList.toggle(className, !element.classList.contains(not-same-class-name))invalid(20)条件作用于element.notClassList非classList属性→ 不折叠invalid(21)条件是裸函数调用contains(className)→ 不折叠invalid(22)条件作用于notSameElement→ 不折叠。折叠还要求contains调用不可选当条件写成element.classList.contains?.(className)invalid(16)或element.classList?.contains(className)invalid(17)时getClassListContainsCall不返回匹配该函数要求optionalCall: false, optionalMember: false见第 81-88 行的isClassListMethodCall约束输出退化为显式取反// invalid(16) 输出 element.classList.toggle(className, !element.classList.contains?.(className));因为可选调用可能返回undefined无法保证与无参toggle()语义等价规则选择保守处理——这也是宁可多写一个布尔参数也不改变运行时语义的工程取舍。分支顺序与取反的组合也全部覆盖条件为contains且 consequent 是remove快照 IfStatement invalid(14)→ 无参toggle()条件为contains且 consequent 是addinvalid(23)→element.classList.toggle(className, element.classList.contains(className))正序显式布尔因为此时 toggle 需要强制添加条件为!contains且 consequent 是addinvalid(24)→ 又折叠为无参toggle(className)。三元表达式组与计算属性名组的 17 个 contains 相关用例快照第 964-1597 行逐一验证了同一套折叠逻辑在三种语法形态下的一致性。修复器实现的机制细节将三个模式的快照输出与 rules/prefer-classlist-toggle.js 对照可以归纳出修复文本的完整拼装规则fix生成器第 175-200 行const elementText getParenthesizedText(consequent.callee.object.object, context); const classNameText getParenthesizedText(consequent.arguments[0], context); const isNegative consequent.callee.property.name remove; const classListContainsCall getClassListContainsCall(conditionNode, isNegative, consequent); const conditionText classListContainsCall ? : getConditionText(conditionNode, context, isNegative); const isOptional consequent.callee.object.optional || alternate.callee.object.optional || classListContainsCall?.callee.object.optional; let text ${elementText}${isOptional ? ? : }.classList.toggle(${classNameText}${conditionText ? , ${conditionText} : });关键点包括元素文本与类名文本均保留原始括号getParenthesizedText因此(( element ))、(((className)))这类写法在输出中原样保留快照 invalid(8)、invalid(11)可选链传播任一分支或折叠的 contains 调用可选则输出?.条件文本三态contains 折叠 → 空正常 →condition反向 →!condition必要时补括号语句上下文补分号非表达式位置IfStatement分支自动追加;并视前文决定是否前置;防御 ASI表达式上下文修空位isExpression为真时三元/计算属性名模式输出后调用fixSpaceAroundKeyword处理关键字两侧空格第 197-199 行。测试覆盖valid 用例划定规则边界测试文件 test/prefer-classlist-toggle.js 除快照中的无效用例外还为三大模式分别定义了 valid 用例这些不报错的样本精确划定了规则边界值得逐类理解不满足相反配对的不报错两个分支都是removeif (condition) { remove } else { remove }两个分支都是add分支类名不同add(className1)/remove(className2)分支元素不同element1/element2。方法名不精确匹配的不报错notAdd/notRemove等非add/remove方法名element.notClassList.add(...)对象不是classLista.add(...)非成员classList上的调用源码isClassList要求property: classList, computed: false。调用形态不匹配的不报错add/remove带额外参数add(className, extraArgument)或零参数——源码isClassListMethodCall强制argumentsLength: 1可选的add?./classList?.add源码要求optionalCall: false, optionalMember: false顺序执行的element.classList.add(className); element.classList.remove(className);非同一分支结构BlockStatement内不止一条语句解包后仍有两条语句嵌套块if (condition) { { add } } else { remove }三元模式中两侧方法相同的condition ? add(x) : add(x)。这些 valid 样本与快照的 invalid 样本共同构成规则行为的完整规格说明任何对规则的修改都必须保持这两组用例同时通过——这正是快照测试的价值所在一旦规则实现变化导致输出差异AVA 会在prefer-classlist-toggle.js.snap中报告差异开发者据此审阅行为变更。在实际项目中启用与验证该规则随eslint-plugin-unicorn一起分发。根据 readme.md 的说明当前版本要求 ESLint 10.4、使用 flat config 与 ESM。手动配置不使用预设时在eslint.config.js中启用import unicorn from eslint-plugin-unicorn; import {defineConfig} from eslint/config; import globals from globals; export default defineConfig([ { files: [**/*.js], languageOptions: { globals: globals.builtin, }, plugins: { unicorn, }, rules: { unicorn/prefer-classlist-toggle: error, }, }, ]);由于规则同时标记fixable: code与hasSuggestions: true可直接运行npx eslint --fix批量修复多数触发点无法安全自动修复的表达式上下文如三元作为if条件或return值会在编辑器中以 suggestion 形式出现手动接受替换即可。TypeScript 项目只需为对应文件配置typescript-eslint/parser快照中(element as Element)与element!的用例即证明规则对 TS 语法完全兼容。小结prefer-classlist-toggle规则是 eslint-plugin-unicorn 中语义化简 防御性修复的典型代表。通过 54 个快照用例可以看到它不只是做字符串级替换它处理分支顺序与取反、可选链传播、括号保留、序列表达式、ASI 分号防护、TypeScript 断言并利用contains条件做语义等价的无参toggle()折叠同时在无法保证安全的表达式上下文主动降级为 suggestion。对使用者而言理解快照报告就等于拿到了规则行为的完整规格书对贡献者而言test/prefer-classlist-toggle.js 的 valid/invalid 分组与快照更新流程则提供了可复用的测试驱动开发范式。【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表