ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESLint 规则深度解析:no-unused-private-class-members 检测未使用的私有类成员

ESLint 规则深度解析:no-unused-private-class-members 检测未使用的私有类成员 ESLint 规则深度解析no-unused-private-class-members 检测未使用的私有类成员【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint本文基于 ESLint 官方文档 docs/src/rules/no-unused-private-class-members.md 编写并参考了 lib/rules/no-unused-private-class-members.js 源码及其测试用例 tests/lib/rules/no-unused-private-class-members.js从使用层面和实现层面完整剖析该规则。规则概览no-unused-private-class-members是 ESLint 内置的一条problem 型发现问题规则其作用是报告声明了但从未被使用的私有类成员。这里的私有类成员指 JavaScript 中以#开头的私有字段private field、私有方法private method与私有访问器private accessor它们是 ES2022 正式纳入规范的类私有特性。这类成员一旦声明却从未被读取或访问通常是不彻底的重构留下的残留成员既占用了代码空间又容易让读者产生困惑——它是否仍被外部依赖是否还有隐藏用途该规则通过静态分析给出明确答案帮助开发者清理这些死代码。规则的核心判定逻辑出自官方文档 Rule Details非常简单且严格私有字段或私有方法其值从未被读取即视为未使用私有访问器getter/setter从未被访问无论是读取还是写入即视为未使用。需要特别强调的是对于普通字段只写不读例如只在方法里被赋值、却从未被读取同样会被判定为未使用——这是该规则区别于no-unused-vars的关键设计详见下文只写不读也算未使用一节。规则配置与启用方式配置项该规则没有任何可配置选项schema: []见 lib/rules/no-unused-private-class-members.js在配置文件中只能选择开启或关闭无法通过参数调整其行为边界// eslint.config.jsflat config 写法 export default [ { rules: { no-unused-private-class-members: error, }, }, ];若使用旧式.eslintrc写法则为{ rules: { no-unused-private-class-members: error } }推荐配置中的位置从源码元数据看lib/rules/no-unused-private-class-members.js该规则标注为recommended: true即它已被纳入 ESLint 的推荐规则集。在 packages/js/src/configs/eslint-recommended.js 中可以看到它被配置为error级别。也就是说只要你的项目启用了eslint:recommendedflat config 中为eslint/js导出的recommended配置该规则就会默认以 error 级别生效无需手动开启。引入版本与类型声明根据 docs/src/_data/rule_versions.json 的记录该规则于ESLint 8.1.0版本引入。在 lib/types/rules.d.ts 中其 TypeScript 类型声明为Linter.RuleEntry[]同样印证了它不接受任何选项空元组类型。报告消息当规则触发时默认报告消息为{{classMemberName}} is defined but never used.例如实际输出形如#unusedMember is defined but never used.消息模板定义见 lib/rules/no-unused-private-class-members.js。触发规则的不正确代码示例以下是官方文档给出的全部错误示例逐一解释其触发原因/*eslint no-unused-private-class-members: error*/ class A { #unusedMember 5; }类A私有字段#unusedMember声明后从未被读取直接触发。class B { #usedOnlyInWrite 5; method() { this.#usedOnlyInWrite 42; } }类B私有字段#usedOnlyInWrite虽然在方法中被写入 42但从未被读取依然触发——这是只写不读场景。class C { #usedOnlyToUpdateItself 5; method() { this.#usedOnlyToUpdateItself; } }类C私有字段#usedOnlyToUpdateItself只被自增自增的结果从未被进一步使用同样触发。class D { #unusedMethod() {} }类D私有方法#unusedMethod从未被调用触发。class E { get #unusedAccessor() {} set #unusedAccessor(value) {} }类E私有访问器#unusedAccessorgetter 与 setter 成对声明从未被访问——既没有读取也没有写入触发。不触发规则的正确代码示例以下示例展示了被使用的判定边界/*eslint no-unused-private-class-members: error*/ class A { #usedMember 42; method() { return this.#usedMember; } }类A#usedMember在method()中被return读取视为已使用。class B { #usedMethod() { return 42; } anotherMethod() { return this.#usedMethod(); } }类B私有方法#usedMethod被anotherMethod()调用视为已使用。class C { get #usedAccessor() {} set #usedAccessor(value) {} method() { this.#usedAccessor 42; } }类C私有访问器#usedAccessor在method()中被写入 42。根据规则定义访问器只要被访问读或写即视为已使用因为 getter/setter 的定义体中可能包含副作用side effects不能因为只写就断定其无意义。何时不使用此规则官方文档明确指出如果你不想收到关于未使用私有类成员的提示可以放心地关闭此规则。{ rules: { no-unused-private-class-members: off } }常见的关闭场景包括类成员带有反射、装饰器或框架机制运行时可能通过非直接路径访问私有成员虽然从语言层面私有成员只能由类内部访问但某些元编程场景仍可能出现静态分析判定未使用的误报项目正处于重构过渡期希望先保留一批待接入的私有方法/字段避免被报错干扰团队风格上不介意保留备用成员且已通过其他手段如代码评审控制质量。深入源码规则是如何判定未使用的理解该规则的最佳途径是阅读其实现 lib/rules/no-unused-private-class-members.js。整个实现围绕三个 AST 节点访问器展开形成声明收集 → 使用标记 → 出口报告的三段式流水线。第一步ClassBody 入口收集全部私有成员当进入一个类的ClassBody节点时lib/rules/no-unused-private-class-members.js规则把该类声明过的所有私有成员放入一个Map并假定它们默认全部未使用ClassBody(classBodyNode) { const privateMembers new Map(); trackedClasses.unshift(privateMembers); for (const bodyMember of classBodyNode.body) { if ( bodyMember.type PropertyDefinition || bodyMember.type MethodDefinition ) { if (bodyMember.key.type PrivateIdentifier) { privateMembers.set(bodyMember.key.name, { declaredNode: bodyMember, hasReference: false, isAccessor: bodyMember.type MethodDefinition (bodyMember.kind set || bodyMember.kind get), }); } } } }这里有两个值得注意的设计细节trackedClasses是一个栈数组新类被unshift到栈顶。这是因为类可以嵌套如方法内部return class { ... }栈结构保证内层类先处理、外层类后处理私有成员名可以正确地归属到各自所在的类而不会跨类混淆。每个成员记录三个字段declaredNode声明节点、hasReference是否出现过任何引用、isAccessor是否为 getter/setter。isAccessor用于区分普通方法与访问器因为二者的使用判定标准不同。第二步PrivateIdentifier 访问器逐一标记已使用每当代码中出现一个#name形式的私有标识符时lib/rules/no-unused-private-class-members.js规则从栈中自上而下查找包含该名称的类PrivateIdentifier(privateIdentifierNode) { const classBody trackedClasses.find(classProperties classProperties.has(privateIdentifierNode.name), ); // ... if (memberDefinition.isUsed) { return; } // 声明本身的 #name 不计为使用 if ( privateIdentifierNode.parent.type PropertyDefinition || privateIdentifierNode.parent.type MethodDefinition ) { return; } memberDefinition.hasReference true; // 访问器任何访问读或写都算使用 if (memberDefinition.isAccessor) { memberDefinition.isUsed true; return; } // 纯写赋值不算使用 if (isWriteOnlyAssignment(privateIdentifierNode)) { return; } // 自增/解构模式等边界情况…… memberDefinition.isUsed true; }关键判断依次为声明位置本身的#name即#unusedMember 5中作为PropertyDefinition.key的标识符不计入使用否则规则将永远无法触发访问器getter/setter任何读写访问一律视为已使用源码注释明确指出getter/setter 定义中可能带有副作用普通字段/方法继续检查是否为纯写场景。第三步isWriteOnlyAssignment识别只写不读isWriteOnlyAssignment函数lib/rules/no-unused-private-class-members.js是区分写入与读写的核心。它的逻辑可以概括为只有当父节点是AssignmentExpression且运算符为纯、ForInStatement、ForOfStatement或AssignmentPattern且该成员位于赋值左侧parentStatement.left时才可能是纯写复合赋值运算符如、-通常视为读 写因为右侧隐式读取了当前值——但有一个例外如果这个复合赋值的结果被丢弃在一条空表达式语句中ExpressionStatement即this.#x 42;单独成句那么它仍然被当作纯写处理对应官方文档中类B的示例自增/自减this.#x单独成句时同样被判定为纯写对应类C的示例。第四步ClassBody:exit报告剩余未使用成员在类体遍历结束时lib/rules/no-unused-private-class-members.js从栈顶弹出该类收集的成员表凡isUsed仍为false的成员一律报告ClassBody:exit() { const unusedPrivateMembers trackedClasses.shift(); for (const [classMemberName, { declaredNode, hasReference, isUsed }] of unusedPrivateMembers.entries()) { if (isUsed) { continue; } context.report({ node: declaredNode, loc: declaredNode.key.loc, messageId: unusedPrivateClassMember, data: { classMemberName: #${classMemberName} }, suggest: [ /* 见下文自动修复 */ ], }); } }由于私有成员在语言层面只能被当前类内部访问这是 JavaScript 私有字段的硬性约束规则可以安全地断言所有对#name的引用都必然出现在其声明类的代码范围内因此不存在跨类使用的漏判问题。源码注释也明确说明了这一安全前提。规则的边界行为从测试用例看判定细节测试文件共 1400 余行覆盖了大量边界场景是理解该规则行为边界的权威参考。以下是几类代表性用例。只写不读与复合赋值class Foo { #usedOnlyInWrite 5; method() { this.#usedOnlyInWrite 42; // 纯赋值无读取 → 报错 } }class Foo { #usedOnlyInWriteStatement 5; method() { this.#usedOnlyInWriteStatement 42; // 结果被丢弃 → 报错 } }class C { #usedOnlyInIncrement; foo() { this.#usedOnlyInIncrement; // 自增结果被丢弃 → 报错 } }这三例分别对应纯赋值、复合赋值单独成句、自增单独成句三种写后即弃场景均被判定为未使用。视为已使用的读写场景反向来看只要写入的结果被消费过就不再触发规则class C { #usedMember; foo() { bar(this.#usedMember 1); // 的结果被作为参数读取 → 已使用 } }class Foo { #usedInForOfLoop; method() { for (const bar of this.#usedInForOfLoop) { } // 被迭代 → 已使用 } }class C { #usedInObjectAssignment; method() { ({ [this.#usedInObjectAssignment]: a } foo); // 作为计算属性键 → 已使用 } }相反作为解构赋值的目标位置纯写入模式则不算使用// ({ x: this.#unusedInDestructuring } bar); → 报错 // [...this.#unusedInRestPattern] bar; → 报错 // [this.#unusedInAssignmentPattern] bar; → 报错访问器的特殊规则只要 getter/setter 被访问过一次无论读还是写整对访问器都视为已使用class C { set #accessorWithSetterFirst(value) { doSomething(value); } get #accessorWithSetterFirst() { return something(); } method() { this.#accessorWithSetterFirst 1; // 触发读写 → 已使用 } }嵌套类的作用域隔离测试中专门覆盖了嵌套类场景验证trackedClasses栈机制的正确性。例如内层类声明并使用了与外层同名的私有成员时外层同名成员因从未被引用而依然报错反之只有内层类使用、外层从未引用时外层成员照常报错。源码注释lib/rules/no-unused-private-class-members.js解释了为何用标记isUsed而非删除成员的方式处理删除会导致后续引用错误地命中外层类的同名成员从而产生误判。自动修复一条内置的 Suggestion该规则在meta中声明了hasSuggestions: truelib/rules/no-unused-private-class-members.js意味着它会为每个问题提供一条建议suggestion级修复——删除未使用的私有类成员。注意它不是自动修复fix需要编辑器或--fix-type suggestion配合由开发者确认后手动应用。修复的前提没有外部引用修复器的核心逻辑位于 lib/rules/no-unused-private-class-members.js*fix(fixer) { if (hasReference) { return; // 存在引用哪怕是只写引用时不提供删除建议 } const removalRange getMemberRemovalRange(declaredNode); const semicolonInsertionToken getSemicolonInsertionToken(declaredNode); // ...删除并视情况补充分号 }也就是说只有当该成员从未出现过任何形式的引用hasReference为 false时才提供整体删除的建议。对于只写不读的成员有引用但无读取规则会报告错误但不提供删除建议——因为删除一个被赋值的成员会改变程序行为风险过高。注释的保留策略删除成员时如何处理其上的注释是这项建议最精细的部分。源码中getLeadingCommentslib/rules/no-unused-private-class-members.js、getTrailingCommentslib/rules/no-unused-private-class-members.js与getMemberRemovalRangelib/rules/no-unused-private-class-members.js协同实现了如下策略紧贴成员的前导注释如/** docs */、// remove me随成员一起删除行内注释如/* remove */ #unusedMember 1;中位于同行的块注释一并删除若注释行与相邻的其他代码共享同一行如/* keep */ #unusedMember 1; foo 1则保留注释因为该注释可能描述的是剩余代码而非被删成员若块注释跨行且与成员分离如foo 1; /*\n */ #unusedMember 1;只删除成员本身注释结构尽量保留行尾注释#unusedMember 1; // trailing随成员删除但当行尾还有其他代码时bar; #unused2; // keep注释保留因为// keep语义上可能属于bar;删除成员后若其后的#name令牌紧跟前一成员且无法安全衔接修复器还会通过getSemicolonInsertionTokenlib/rules/no-unused-private-class-members.js自动补充分号避免破坏类体的语法。以下测试用例直观展示了修复输出// 输入 class Foo { /** docs */ #unusedMember 1; } // 应用建议后 class Foo { }// 输入共享同一行的注释被保留 class Foo { /* keep */ #unusedMember 1; foo 1 } // 应用建议后 class Foo { /* keep */ foo 1 }// 输入行尾注释指向其他代码时被保留 class C { bar; #unused2; // keep } // 应用建议后 class C { bar; // keep }与 no-unused-vars 的关系与差异熟悉 ESLint 的开发者可能会联想到no-unused-vars未使用变量检测。二者的分工不同no-unused-vars也包含对类成员的检查但它对**私有成员只做是否存在任何引用**层面的判定即只要#member被赋值过就不会报告no-unused-private-class-members则更进一步要求值必须被读取。因此一个只在方法里被赋值、从不被读取的私有字段no-unused-vars会放过而no-unused-private-class-members会报错。换言之这条规则把不可达的死代码检测延伸到了可写但不可读的成员对重构残留的识别更严格尤其适合捕捉那些赋值了却没人消费的状态字段。实践建议默认开启由于该规则已进入eslint:recommended多数项目无需额外配置即可受益。若使用 flat config确认已引入eslint/js的recommended配置即可。配合编辑器建议启用编辑器的quick fix提示对无引用的未使用私有成员可一键删除对只写不读的成员规则只报错不自动删除需要人工判断是补上读取逻辑还是删除赋值。重构收尾利器在大型类中提取方法或移动逻辑后极易残留失去引用的私有字段运行npx eslint --rule no-unused-private-class-members: error src/即可快速扫描定位。必要的关闭场景当私有成员会被运行时反射、代码生成或框架内部机制间接触碰静态分析无法看到时可在文件级或规则级关闭该规则避免误报。延伸阅读规则实现源码lib/rules/no-unused-private-class-members.js规则测试用例边界行为全集tests/lib/rules/no-unused-private-class-members.jsESLint 推荐规则集配置packages/js/src/configs/eslint-recommended.js规则版本记录docs/src/_data/rule_versions.json类型声明lib/types/rules.d.ts规则索引lib/rules/index.js【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表