eslint-plugin-unicorn 的 no-global-object-property-assignment 规则:禁止在全局对象上赋值属性
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
no-global-object-property-assignment是 eslint-plugin-unicorn 提供的一条「问题类(problem)」规则,用于在静态检查阶段拦截对global、globalThis、self、window等全局对象属性的赋值操作。它通过把状态创建约束在模块作用域内,帮助开发者规避全局变量被意外覆盖、全局状态难追踪等隐患。读完本文,你将掌握该规则的完整触发条件、边界豁免场景、与 TypeScript 语法的兼容处理方式,以及在有意的全局赋值(如测试环境初始化、polyfill)场景下的标准关闭配置。
规则要解决的问题
全局对象是所有模块共享的运行时环境。在globalThis.foo = value这类代码中,状态的创建点散落在任意文件中,代码评审和调试时很难判断某个全局属性从何而来、被谁修改;更严重的是,它可能意外覆盖运行时已经存在的全局属性(例如给window.name、self.location等内置属性赋值),造成难以定位的隐蔽 bug。
规则文档对该问题的定性是:
Global object mutation makes it hard to see where state is created and can accidentally overwrite existing globals.
因此该规则建议改用模块作用域、显式导入导出、依赖注入或局部单例来传递状态,而不是把状态挂在全局对象上。
该规则的元数据声明为type: 'problem',并标记为recommended: 'unopinionated'(参见 规则实现),同时它也出现在recommended推荐配置中;其schema: []表明该规则不接受任何选项,属于纯开/关型规则。规则在 rules/index.js 中注册,启用后按默认配置生效。
哪些全局对象名会被监控
规则内部维护了一个固定的全局对象名称集合(参见 规则实现):
const globalObjectNames = new Set([ 'global', 'globalThis', 'self', 'window', ]);只要MemberExpression的对象部分(object)是这些标识符之一,且满足后续判定条件,就会被报告。但注意:名称匹配只是第一道关卡,规则还会校验该标识符是否真的是「全局引用」。
关键判定逻辑:真的全局,而不是被遮蔽的局部变量
仅凭名称判断会产生误报——例如某个函数内声明了名为window的局部参数,此时window.foo = 1操作的是局部变量,不应被拦截。规则通过isGlobalIdentifier工具函数(参见 is-global-identifier.js)解决这个问题:
export default function isGlobalIdentifier(node, context) { return node.type === 'Identifier' && (context.sourceCode.isGlobalReference(node) || isUnresolvedVariable(node, context)); }其判定依据是:该标识符要么是 ESLint 认可的已声明全局引用,要么是未解析(隐式)全局引用,同时排除被局部作用域遮蔽的情况。测试用例验证了这一点(参见 测试文件):
function test(window) { window.foo = 1; // ✅ 不报错:window 是局部参数 } const global = {}; global.foo = 1; // ✅ 不报错:global 是局部 const const globalThis = {}; globalThis.foo = 1; // ✅ 不报错:局部变量遮蔽 const self = {}; self.foo++; // ✅ 不报错 const root = globalThis; root.foo = 1; // ✅ 不报错:别名引用,名称不在监控集合内什么算「赋值属性」:可写目标的完整判定
规则不是只看=赋值,而是通过isWritableTarget组合了两个判定函数(参见 is-left-hand-side.js 与 规则实现):
- isLeftHandSide:覆盖赋值表达式、更新表达式、以及解构模式中的写入位置:
AssignmentExpression/AssignmentPattern的左值(如globalThis.foo = 1)UpdateExpression的操作数(如global.foo++、window.foo += 1中的+=也属于赋值)ArrayPattern/RestElement/ObjectPattern中的解构目标(如[globalThis.foo] = array)
- isForLoopLeftHandSide:覆盖
for...of/for...in的循环左值(如for (globalThis.foo of iterable) {})。
同时,规则会递归穿透 TypeScript 表达式包装(见下文),并显式排除delete操作(见下文边界)。
边界情况:这些写法是允许的
规则在报告前有一组反向排除条件,命中任意一条即放行。结合测试用例(test/no-global-object-property-assignment.js)可归纳为以下几类:
1. 只读访问不会被拦截
globalThis.foo; // ✅ 读取属性 window.foo(); // ✅ 调用方法读取全局对象属性本身不产生状态写入,因此完全合法。
2.delete全局属性不受限
delete globalThis.foo; // ✅ 允许规则在判定中显式通过isDeleteExpressionArgument排除了UnaryExpression形式的delete操作(参见 规则实现),测试中也将其列为 valid 用例。
3. 计算属性名(动态键)默认放行
globalThis[property] = value; // ✅ 属性名为变量时不做静态判定当getPropertyName(node, ...)无法解析出静态属性名(返回null)时,规则直接返回不报告(参见 规则实现)。不过需要留意:静态字符串键仍会被拦截,例如globalThis["foo"] = 1在测试中被判定为 invalid。
4. 间接 API 不在本规则管辖范围
Object.assign(globalThis, {foo: value}); // ✅ 不报错(属其他规则的范畴) Reflect.set(globalThis, 'foo', value); // ✅ 不报错规则只监听MemberExpression赋值形态,通过Object.assign、Reflect.set等间接写入不触发报告(见测试 valid 用例)。这属于该规则的已知边界,写入全局属性依然不推荐,但本规则只负责最直接的语法形态。
TypeScript 兼容:自动穿透类型包装
规则对 TypeScript 表达式包装做了透明化处理:通过getEffectiveAssignmentTarget向上穿透包装节点(参见 规则实现),并借助unwrapTypeScriptExpression/isTypeScriptExpressionWrapper(参见 unwrap-typescript-expression.js)展开对象部分。这意味着以下写法同样会被报告:
globalThis!.foo = 1; // ❌ 非空断言 (globalThis as any).foo = 1; // ❌ 类型断言 (<any>globalThis).foo = 1; // ❌ 尖括号断言 (globalThis satisfies any).foo = 1; // ❌ satisfies 表达式 globalThis.foo! = 1; // ❌ 对属性做非空断言以上五例均在 TypeScript parser 的测试用例中被标记为 invalid(参见 测试文件)。规则在meta.languages中声明支持'js/js'语言。
错误与正确示例对照
❌ 会触发报告的写法
globalThis.foo = value; // 直接赋值 window.foo += 1; // 复合赋值 self.foo ||= value; // 逻辑赋值 global.foo++; // 自增 globalThis["foo"] = 1; // 静态字符串键 ({foo: globalThis.foo} = object); // 解构赋值 [globalThis.foo] = array; // 解构赋值 for (globalThis.foo of iterable) {} // for...of 左值 for (globalThis.foo in object) {} // for...in 左值✅ 推荐的替代写法
export const foo = value; // 模块作用域 + 显式导出 const singleton = { // 局部单例 foo: value, }; // 依赖注入: function init({foo}) { /* ... */ }有意赋值时如何关闭规则
规则文档明确指出:测试环境初始化(例如用 jsdom 赋值globalThis.window)和polyfill / shim是常见的有意赋值场景。此时不应逐行禁用,而应通过配置文件针对整个文件或目录关闭(以下配置来自 规则文档):
{ files: ['test/**', '**/*.test.js'], rules: { 'unicorn/no-global-object-property-assignment': 'off', }, }将豁免范围收敛到test/**或**/*.test.js这类明确的测试目录,既能保持生产代码的全局对象纯净性,又不会让测试环境的必要设置(如注入window、document)被反复打断。若只是个别语句有意为之,也可使用行内注释局部禁用该规则。
小结
no-global-object-property-assignment的核心价值在于把「全局对象写入」这一隐式、难以追踪的状态变更行为在 CI 阶段显式拦截下来。实际使用时需要注意三点:
- 判定是语义级的:名称匹配 + 全局引用校验 + 写入位置校验三重条件缺一不可,局部遮蔽、别名引用、只读访问均不会误报;
- 边界设计清晰:
delete与动态计算属性名默认放行,Object.assign/Reflect.set等间接写入不在管辖范围; - 豁免路径明确:测试环境与 polyfill 场景通过配置文件按目录关闭即可,无需牺牲生产代码的全局对象安全。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考