eslint-plugin-unicorn 的 no-accessor-recursion 规则:彻底拦截 getter/setter 中的递归访问
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
no-accessor-recursion是 eslint-plugin-unicorn 提供的一条问题型(problem)规则,专门用来禁止在对象与类的 getter/setter 访问器中通过this递归访问自身同名属性,从源头避免无限递归导致的栈溢出(RangeError: Maximum call stack size exceeded)。阅读本文后,你将掌握该规则的全部触发场景与放行边界、其基于 ESLint 作用域分析的底层实现原理,以及如何在项目配置中启用它。
为什么要禁止访问器内的this递归访问
在 JavaScript 中,get与set访问器的核心特征是:读写属性时会自动调用对应的访问器函数。因此,如果在 getter 内部再次读取同名属性:
const foo = { get bar() { return this.bar; } };当代码执行foo.bar时,读取操作会再次触发 getter,getter 又读取this.bar……如此循环往复,直到调用栈被耗尽,抛出RangeError: Maximum call stack size exceeded。setter 同样如此:
const foo = { set bar(value) { this.bar = value; } };foo.bar = 1会反复触发 setter,形成死循环。这类 bug 在大型代码库中一旦混入,往往要在运行时才能暴露,且错误信息对定位根因帮助有限。该规则的职责,就是在静态分析阶段把这种写法直接标记出来。
规则的启用状态与定位
- 规则类型:
problem(问题型,用于发现可能导致运行时错误的代码)。 - 默认启用配置:✅
recommended、☑️unopinionated两个预设配置中均默认开启(源码见 rules/no-accessor-recursion.js 中的meta.docs.recommended声明)。 - 默认无任何可配置选项(
defaultOptions: []),开箱即用,属于"零配置"规则。 - 支持语言:
js/js(当前仓库中绝大多数规则均面向 JavaScript)。
由于它属于"默认开启"的规则,使用官方recommended配置集或unopinionated配置集的用户会自动获得该检查,无需额外配置。项目中配置预设的入口可参考 configs 目录下的配置文件。
规则会报错的反模式示例
以下四组代码分别覆盖了对象 getter、类 getter、对象 setter、类 setter四种最典型的错误写法,均为该规则会报告no-accessor-recursion/error的场景(消息文本为:Disallow recursive access tothiswithin {{kind}}ters.,其中{{kind}}会被替换为get或set):
// ❌ 对象字面量 getter const foo = { get bar() { return this.bar; } };// ❌ 类 getter class Foo { get bar() { return this.bar; } }// ❌ 对象字面量 setter const foo = { set bar(value) { this.bar = value; } };// ❌ 类 setter class Foo { set bar(value) { this.bar = value; } }四段代码的共同特征完全一致:访问器内部对this的成员访问,键名与访问器自身键名相同。这正是判定递归的充要条件。
从源码看检测原理:三步定位递归
规则实现位于 rules/no-accessor-recursion.js,核心思路清晰且工程化:监听每个ThisExpression节点,沿作用域链向上找到最近的函数作用域,再判断该函数是否正好是某个 getter/setter 的函数体,最后核对this访问的键名与访问器键名是否一致。具体分为三步:
第一步:找到最近的函数作用域。工具函数getClosestFunctionScope(rules/no-accessor-recursion.js)从当前节点沿scope.upper向上遍历,一旦遇到class作用域立即终止(避免穿透到外层类),并跳过箭头函数——因为箭头函数没有自己的this,它内部的this属于外层函数。因此,getter 内嵌套的箭头函数里写this.bar同样会被识别为递归。
第二步:确认该函数体属于合法的访问器。isValidProperty(rules/no-accessor-recursion.js)要求父节点是Property(对象字面量成员)或MethodDefinition(类成员),kind为get或set,且键名不是计算属性(!property.computed)。
第三步:按访问器种类分别判定"读"与"写"。
- getter 中调用
isPropertyRead(rules/no-accessor-recursion.js),它由两条路径组成:isMemberAccess(点号访问,如this.bar)与isRecursiveDestructuringAccess(解构访问,如const {bar} = this)。 - setter 中调用
isPropertyWrite(rules/no-accessor-recursion.js),除了基础的点号访问外,还显式覆盖了多种"写"语义:赋值表达式与默认值模式(this.bar = value、[this.bar = value] = ...)、自增自减(++this.bar)、解构赋值目标([this.bar] = arr、[...this.bar] = arr)、for...of/for...in循环左值、以及对象模式属性(({property: this.bar} = obj))。
值得一提的实现细节是isSameKey(rules/no-accessor-recursion.js)通过比较节点的type与name两个字段判断键名是否相同,因此它天然支持私有字段(PrivateIdentifier,如this.#bar)的同名递归检测,同时isIdentifier(rules/no-accessor-recursion.js)将Identifier与PrivateIdentifier一并纳入点号访问的判断范围。
规则的检测边界:哪些"看似递归"的写法不会误报
通过 test/no-accessor-recursion.js 中的valid用例列表,可以清晰还原规则精心设计的放行边界,避免对合理写法产生误报:
- 普通方法不受影响:
bar()方法体内this.bar = void 0; return this.bar;合法(方法与访问器是两套键名空间之外的行为)。 - 键名不同的访问:getter 内读
this._bar、this.baz等不同名属性合法;同一类中 getterbar读私有字段this.#bar(且#bar是独立 getter)也合法。 - 深属性链:setter 内写
this.bar.baz = value合法——它只修改bar的深层子属性,不会再次触发bar的 setter(测试用例注释为 "Deep property setter")。 this的别名:const self = this; return self.bar;合法——规则只追踪字面量this表达式,不追踪引用别名(测试用例注释为 "Define this to alias")。- 计算属性:getter 键名为
[bar]的访问器、或内部用this[bar]访问(方括号内不是标识符键)均合法,因为计算键无法静态确定是否同名。 - 函数作用域隔离:getter 内嵌套的普通函数
function baz() { return this.bar; }合法——该函数有自己的this;getter 内嵌套的对象字面量里的其他 getter 互不影响。 - setter 右侧读取:setter 内
a = this.bar合法——等号右侧是读取bar,而 setter 递归只关心对bar的写入。 - 静态块与类字段:getter 内嵌套类里的
static { this.bar }、类字段初始化baz = this.bar均合法——其this属于嵌套类本身。
在invalid用例中,除了四种基础反模式,还覆盖了更深层的变体:getter 内return this.bar.baz(深链读取)、块语句if (true) { return this.bar; }内的递归、多层嵌套箭头函数内的递归、等号右侧的 getter 递归读取(a = this.bar)、私有字段同名递归(get #bar() { return this.#bar })、静态 getter(static get bar() { return this.bar })、以及解构递归(const {bar} = this)。setter 的写操作变体则以数组批量构造了 9 种用例,包括自增自减、数组解构、rest 解构、for...of/for...in左值等(见 test/no-accessor-recursion.js)。
如何修复被标记的代码
修复原则只有一条:让访问器内部不再读写自己的同名键。最直接的做法是访问一个不同的属性作为底层存储:
// ✅ 对象:getter 返回底层字段 const foo = { get bar() { return this._bar; } };// ✅ 类:setter 写入底层字段 class Foo { set bar(value) { this._bar = value; } }对 setter 的合法修复还需要注意:setter 通常与同名的 getter 成对出现,用于读写控制。若确实需要暴露同名属性并附带副作用,业界惯例是引入_bar之类的私有底层字段(或类私有字段#bar)作为真实存储,访问器只作为门面。这也是文档示例 (docs/rules/no-accessor-recursion.md) 中采用的修复范式。
在你的项目中启用与验证
该规则随 eslint-plugin-unicorn 一起安装,启用方式与其他规则完全一致。若使用扁平化配置(flat config),在插件配置中按需声明即可;由于它默认包含在recommended与unopinionated两个预设中,多数用户无需显式书写。如果你希望单独显式开启它:
import unicorn from 'eslint-plugin-unicorn'; export default [ unicorn.configs['flat/recommended'], // 已包含 no-accessor-recursion // 或按需开启 { plugins: {unicorn}, rules: { 'unicorn/no-accessor-recursion': 'error', }, }, ];规则无任何配置选项,'error'与'warn'的差异仅在于上报级别。仓库自带的测试(test/no-accessor-recursion.js)同时覆盖了 ES module、CommonJS(sourceType: 'commonjs')与脚本(sourceType: 'script')三种源码类型,验证了规则在不同模块体系下的稳定性;对应的快照文件位于 test/snapshots 目录,可通过npm run test:js(内部使用 AVA)执行回归验证。
小结
no-accessor-recursion通过静态作用域分析,在编译期就拦截了 getter/setter 中针对this的同名递归读写,覆盖面包括对象与类、读与写、点号与解构、公有键与私有键、以及多种赋值左值形态,同时借助"键名相同 + 最近函数作用域"的精确判定,对别名、深属性链、计算属性、嵌套函数等易混淆场景保持了克制。对于维护大型面向对象风格代码库的团队,它是一条低成本、高收益的默认防线。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考