
读懂 no-undeclared-class-members从 eslint-plugin-unicorn 的测试快照报告解析类成员未声明检测规则【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn本文以仓库中 test/snapshots/no-undeclared-class-members.js.md 这份 AVA 快照报告为主体逐条解读no-undeclared-class-members规则的全部失效用例报错信息与建议修复并结合 rules/no-undeclared-class-members.js 的源码还原该规则如何收集“已声明成员”、如何划界this的作用域以及在什么条件下才会给出“声明为类字段”的自动建议。读完本文你可以完整掌握这条规则的行为边界、TypeScript 支持细节及其实现原理。这份快照报告是什么test/snapshots/no-undeclared-class-members.js.md 是针对测试文件 test/no-undeclared-class-members.js 生成的快照报告文件头部明确说明Snapshot report fortest/no-undeclared-class-members.jsThe actual snapshot is saved inno-undeclared-class-members.js.snap. Generated by AVA.也就是说它和同目录下二进制格式的 test/snapshots/no-undeclared-class-members.js.snap 内容一一对应只是以人类可读的 Markdown 形式呈现。报告结构如下每个用例以## invalid(N): 代码作为标题对应测试文件invalid数组中的第 N 个用例 Input块展示带行号的输入代码␊表示换行 Error i/n块展示第 i 条报错的代码帧code frame用^^^标注报错范围并附上消息文本若规则给出了建议suggestion还会追加Suggestion 1/1: 描述:块展示应用建议后的完整代码。从源码看这套报告的生成逻辑在 test/utils/snapshot-rule-tester.js它通过 ESLint 的Linter以 flat config 运行规则插件名固定为rule-to-test用babel/code-frame渲染每个报错visualizeEslintMessage并且每条建议的产物都会被再次verify验证确保“应用建议后代码必须合法”runVerify(output)。这份报告因此可以当作该规则的完整“行为规范”来阅读。规则概览无配置项的推荐级规则从 rules/no-undeclared-class-members.js 的meta定义可以看到该规则的核心属性属性值说明typeproblem属于“发现问题”类规则而非风格类recommendedtrue包含在 recommended 配置中hasSuggestionstrue可提供“声明为类字段”的建议schema[]没有任何可配置选项languagesjs/js面向 JavaScript含 TypeScript 语法解析规则只定义了两个消息模板源码第 3~8 行报错Class member {{name}} is used but not declared.建议Declare {{name}} as a class field.这正是快照报告中每条Error与Suggestion块里看到的文案。规则的职责很明确类中通过this访问的每个成员都必须在类体内声明字段、访问器、方法或在构造函数中被简单赋值否则报错。快照报告逐条解读14 个 JavaScript 失效用例基础访问形态读、调用、可选链都会检查前 4 个用例覆盖不同的成员访问形态class Foo { getName() { return this.name; } } // 属性读取 class Foo { callName() { this.name(); } } // 方法调用 class Foo { callName() { this.name?.(); } } // 可选调用 class Foo { getName() { return this?.name; } } // this 上的可选链四个用例分别报出 3 | return this.name; | ^^^^^^^^^ Class member name is used but not declared. 3 | this.name?.(); | ^^^^^^^^^ Class member name is used but not declared.注意第 4 个用例this?.name报错范围是this?.name整体。从源码看原因是ChainExpression可选链被列入了transparentExpressionWrapperTypes源码第 19~26 行规则会一层层剥掉这类“透明包装”再判断内层是否为this.name这种成员访问所以this?.name、this?.name?.()这类写法不会被绕过。类字段初始化器里读this也会被检查class Foo { name this.value; }报错Class membervalueis used but not declared.指向this.value。这说明字段初始值表达式也在检查范围内——字段初始化器本质上是构造函数中执行的方法代码。而左侧的name本身作为字段声明自然不算“未声明”对比下方第 6 条用例。简单赋值触发报错 “声明为类字段”建议class Foo { setName(name) { this.name name; } }快照报告在此处不仅有报错还附带了Suggestion 1/1: Declarenameas a class field.:应用建议后的代码为class Foo { name; setName(name) { this.name name; } }这条建议是建议suggestion而非自动修复fix它要求开发者显式选择采纳。给出建议的条件在 源码getProblemsForClassBody中写得很清楚需同时满足三点该成员访问是简单赋值目标——父节点是AssignmentExpression、位于左值、运算符为isSimpleAssignmentTarget源码第 132~137 行不在构造函数内!isInConstructor——构造函数里的this.x ...是常见的合法初始化模式规则将其视为“隐式声明”而非报错点更不提建议同名成员只建议一次suggestedNames集合去重。复合赋值与自增报错但不给建议class Foo { setName(name) { this.name name; } } class Foo { setName() { this.name; } }两个用例都报出同样的错误但快照中没有 Suggestion 块。这与上面第 1 点一致和不属于operator 的简单赋值规则无法确定“声明一个字段”是否是你想要的语义复合赋值隐含先读取字段声明会改变执行时机因此只报错、不主动建议。箭头函数继承方法内的thisclass Foo { getName() { return () this.name; } }this.name仍然被报错。对照测试文件中对应的 valid 用例class Foo { getName() { function getName() { return this.name; // 不报错 } return getName(); } }这里的差异来自 源码getThisOwnerClassBody向上追溯父节点时遇到ClassBody就归属该类遇到非箭头函数FunctionDeclaration/FunctionExpression且它不是类方法本身时this链断裂直接返回undefined——普通函数声明里的this与方法无关规则不去管它。而箭头函数不重新绑定this所以方法内return () this.name依然归属于本类并被检查。构造函数中的“隐式声明”左值简单赋值不算未声明class Foo { constructor() { this.name this.value; // 只报 value不报 name } }快照中此用例仅报一处错指向右侧的this.value。原因是 源码getDeclaredClassMemberNames在收集声明名时会专门扫描构造函数函数体顶层出现的this.name ...简单赋值会被加入“已声明”集合constructor本身也在初始集合中所以this.constructor永远合法。但this.value没有任何声明照常报错。更微妙的情况出现在第 11 个用例class Foo { constructor(callbacks) { callbacks.push(() { this.name foo; }); } getName() { return this.name; } }快照报告此用例有2 条错误Error 1/2指向this.name fooError 2/2指向return this.name;且均无建议。两个原因构造函数扫描时的shouldSkip会跳过箭头函数、嵌套函数与嵌套类源码第 209~214 行所以回调里的this.name foo不会被计入隐式声明——规则对“延迟执行的赋值”保持保守两处访问都位于构造函数或其继承的箭头函数上下文内按建议条件第 2 点构造函数内不给出“声明字段”的建议。同名成员多处报错时建议只出现一次class Foo { setName(name) { this.name name; this.name name.trim(); } }快照中两条错误Error 1/2、Error 2/2都报出但Suggestion 块只挂在第一条错误下Error 2/2块里没有 Suggestion。这就是suggestedNames去重的直接体现同一字段名在同一个类里只提示一次声明位置避免重复的插入建议。建议插入位置与成员前注释的协作class Foo { // Sets the name. setName(name) { this.name name; } }应用建议后的输出class Foo { name; // Sets the name. setName(name) { this.name name; } }从 源码getInsertClassFieldSuggestion可以看到插入逻辑找到类体第一个成员取getCommentsBefore(firstMember)的第一条注释作为插入锚点sourceCode.getCommentsBefore(firstMember)[0] ?? firstMember然后在该锚点所在行的行首插入${memberIndent}name;\n。因此注释仍然跟随其所属的方法新字段声明干净地落在注释之前缩进则复用第一个成员的缩进getIndentString。单行类体不给建议最后一个 JS 用例是一行代码class Foo { setName(name) { this.name name; } }快照中该用例只有报错没有 Suggestion 块。原因是建议函数中有一道保护如果第一个成员与类体左花括号位于同一行firstMemberLocation.line openingBrace 所在行直接return undefined放弃建议源码第 244~246 行——在单行紧凑写法中强行插入多行字段声明会破坏可读性规则选择沉默。同理空类体的建议是插入到右花括号前源码第 256~262 行。TypeScript 支持透明包装与参数属性快照报告后半部分invalid(1)~(3)编号重新从 1 开始是配置了 TypeScript 解析器的第二批用例。三个失效用例class Foo { constructor(name: string) {} getName() { return this.name; // 报错name 未声明 } } class Foo { getName() { return this.name as string; // 报错 } } class Foo { getName() { return this!.name; // 报错 } }三条规则各对应一个实现细节普通 TS 构造参数不等于字段声明。第一个用例中constructor(name: string)没有public等修饰符不会生成this.name赋值所以this.name照常报错。对照测试文件里的 valid 用例class Foo { constructor(public name: string) {} getName() { return this.name; } } class Foo { constructor(protected count 0) {} getCount() { return this.count; } }参数属性TSParameterProperty会被 源码getParameterPropertyName识别并加入已声明集合支持直接标识符和带默认值的AssignmentPattern两种形态。as断言、非空断言等 TS 包装是“透明”的。this.name as stringTSAsExpression和this!.nameTSNonNullExpression都能被removeTransparentWrapper剥壳后识别为成员访问并报错。完整的透明包装列表见 源码第 19~26 行ChainExpression、TSAsExpression、TSInstantiationExpression、TSNonNullExpression、TSSatisfiesExpression、TSTypeAssertion。declare与abstract成员算作已声明。测试文件的 valid 用例包含declare name: string;和abstract name: string;对应源码中classMemberTypes集合里的TSAbstractAccessorProperty、TSAbstractMethodDefinition、TSAbstractPropertyDefinition源码第 10~17 行。另外已声明成员的收集只认静态名字getStaticName标识符 key、字符串字面量 key、无插值的模板字符串 key 都可以计算属性[name]、this[name]解析不出静态名因此不参与“声明”认定——这与 valid 用例return this[name];动态访问不检查的行为一致。被静默放行的场景valid 用例全景快照报告只记录 invalid 用例的报错但要完整理解规则边界还需看 test/no-undeclared-class-members.js 中的 12 个 valid 用例断言零报错场景代码片段放行原因源码依据字段已声明name; getName() { return this.name; }字段名在已声明集合中访问器声明get name() {...}后读this.nameMethodDefinition计入声明setter 中转set name(value) { this.value value; }value是已声明字段方法声明name() {}后调this.name()方法也是类成员构造函数简单赋值constructor(name) { this.name name; }隐式声明扫描继承类class Foo extends Bar { ... this.name }有superClass的类整体跳过见下嵌套普通函数方法内function f() { return this.name; }非箭头函数切断this链私有字段#name;后读this.#name私有名非静态成员访问提取不出声明名计算属性访问this[name]computed成员不检查Object.assign(this, data)构造函数中静态分析无法确定键名不检查静态方法内static getName() { return this.name; }isInStaticContext静态上下文里this指类非实例成员静态块static { this.name foo; }同上StaticBlock被识别this.constructorreturn this.constructor;已声明集合初始就含constructor其中“继承类整体跳过”值得单独强调在 源码create函数 的onExit(Program)中if (classBody.parent.superClass) { continue; }——因为父类成员无法被本文件的静态分析看见规则对继承类完全闭嘴宁可漏报也不误报。实现机制小结两阶段收集 统一判定综合以上用例该规则的执行流程可以概括为三步收集阶段遍历期间context.on(MemberExpression)把所有能提取出静态名的this.xxx访问记入memberAccessescontext.on(ClassBody)记入classBodies声明集合构建每个类体getDeclaredClassMemberNames汇总字段/方法/访问器含 TS 抽象与 declare 形态的静态名、TS 参数属性名以及构造函数体顶层的this.x ...简单赋值名判定与输出Program退出时shouldReportMemberAccess对每个访问做四重检查——访问的this归属当前类体、不在静态上下文、不在成员 key/装饰器定义内部、名字不在已声明集合中——全部通过才报错满足“简单赋值目标 非构造函数 未建议过”的报错才附加插入字段的建议。这种“先全量收集、后统一判定”的结构使规则能够处理跨成员引用如 getter 读字段、字段初始值读其他字段这类单个节点内无法判定的场景。如何验证与更新这份快照运行测试仓库测试基于 AVA直接运行npx ava test/no-undeclared-class-members.js即可只跑这条规则的快照测试更新快照修改规则后执行npm run fix:snapshots即ava --update-snapshots见 package.json 的 scripts重新生成.snap与.md报告核对行为修改规则后对照本仓库中test/snapshots/no-undeclared-class-members.js.md的 17 个 invalid 用例14 个 JS 3 个 TS检查报错位置、消息文案与建议输出是否仍然逐字一致。总结这份快照报告虽由测试工具自动生成却是no-undeclared-class-members规则最精确的行为文档它逐字记录了规则在 17 种典型错误模式下的报错范围、消息文本与建议修复产物。结合 rules/no-undeclared-class-members.js 的源码可以看到规则在“严格检查实例成员声明”与“对继承、动态访问、构造函数赋值保持宽容”之间做了清晰取舍——extends的类、计算属性、Object.assign(this, ...)一律放行而可选链、TS 断言等包装语法则被透明剥离、绝不遗漏。对于维护类代码规范的项目这条零配置的推荐级规则配合其“声明为类字段”建议是杜绝运行时undefined成员访问的有效静态防线。【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考