eslint-plugin-unicorn 规则详解:no-error-property-assignment —— 禁止向内置 Error 对象属性赋值
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
no-error-property-assignment是 eslint-plugin-unicorn 中一条面向“错误处理代码卫生”的规则:它禁止在Error及其子类(如TypeError、AggregateError)构造完成之后,再对name、stack、cause、errors这类由内置构造器负责初始化的元数据属性进行赋值。阅读本文后,你将掌握该规则的完整检测范围(包括Object.assign与直接赋值两种形态、变量追踪、TypeScript 语法支持)、其报错消息与定位规则、哪些写法会被放行,以及如何在你的 ESLint 配置中启用它。本文以该规则的官方文档 docs/rules/no-error-property-assignment.md 为骨架,结合规则源码 rules/no-error-property-assignment.js、测试用例 test/no-error-property-assignment.js 及其 AVA 快照报告 test/snapshots/no-error-property-assignment.js.md 展开。
规则概览与设计动机
内置的Error构造器在创建错误对象时会同步初始化并暴露一组元数据属性,包括:
name:错误名称(如TypeError);stack:调用堆栈信息;cause:通过构造选项传入的底层原因(ES2022 起);AggregateError#errors:聚合错误的子错误数组。
这些属性服务于日志、堆栈追踪与错误处理流程。若在错误对象构造之后再覆盖它们,日志、堆栈信息或错误处理逻辑都可能被误导——例如篡改stack会让问题定位失效,覆盖cause会丢失原始异常链。因此规则建议:在构造错误时通过构造器支持的参数传入数据,或改用应用自定义的属性(如error.myCode)。
快照报告 test/snapshots/no-error-property-assignment.js.md 记录了该规则在全部 36 个 invalid 用例(32 个 JavaScript 用例 + 4 个 TypeScript 用例)上的实际输出,是理解规则行为边界最直观的素材,下文将逐类解读。
检测的内置错误构造器与属性清单
内置错误构造器清单
规则只关心语法上可确定为内置错误的实例。源码 rules/shared/builtin-errors.js 维护了内置构造器名清单:
const builtinErrors = [ 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'AggregateError', 'SuppressedError', ];除 ECMAScript 标准内置的 7 类错误(Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError)外,还包括AggregateError与较新的SuppressedError(配合Error Suppression提案使用)。
被禁止的属性集合
规则源码 rules/no-error-property-assignment.js 中定义了两组属性:
const errorProperties = new Set([ 'name', 'stack', 'cause', ]); const aggregateErrorProperties = new Set([ ...errorProperties, 'errors', ]); const getDisallowedProperties = constructorName => constructorName === 'AggregateError' ? aggregateErrorProperties : errorProperties;- 普通内置错误(含
SuppressedError):禁止赋值name、stack、cause; AggregateError:额外禁止errors。
因此快照中出现这样的对比:对new Error()赋值{errors}不会被标记(非 AggregateError 上errors属于自定义属性),而对new AggregateError([], "message")赋值{errors}会被标记。
报错消息模板
所有违规共用同一个MESSAGE_ID与消息模板(见 rules/no-error-property-assignment.js):
Do not assign to `{{property}}` on a built-in error.消息中的{{property}}会被替换为实际的属性名,如快照中反复出现的:
Do not assign to \name` on a built-in error.`Do not assign to \errors` on a built-in error.`
被标记的代码形态(invalid 用例全解读)
快照报告中的 36 个 invalid 用例,从语法形态上可分为五大类。
形态一:对字面量构造的内置错误调用Object.assign
目标对象直接是内置错误的构造表达式,Object.assign的源对象中包含被禁止属性:
// 快照 invalid(1) ~ invalid(5) Object.assign(new Error(), {name}); // 报 name Object.assign(Error(), {stack}); // 报 stack(无 new 的调用形态同样识别) Object.assign(new TypeError(), {cause}); // 报 cause Object.assign(new AggregateError([], "message"), {errors}); // 报 errors Object.assign(new AggregateError([], "message"), {name, stack, cause, errors}); // 一次报 4 处(Error 1/4 ~ 4/4)注意 invalid(5) 展示了“一个语句多处违规”的行为:快照中依次输出Error 1/4到Error 4/4,每处违规独立定位、独立报告。
形态二:多个源对象与属性键的多种写法
规则对源对象属性键的静态解析很严格,字符串字面量键、计算键、方法属性都能被识别:
// 快照 invalid(6):只有第二个源对象中的 name 被报,code 不在禁止清单内 Object.assign(new Error(), {code}, {name}); // 快照 invalid(7):字符串键 Object.assign(new Error(), {"stack": stack}); // 快照 invalid(8):计算键(静态字符串字面量) Object.assign(new Error(), {["cause"]: cause}); // 快照 invalid(9):方法属性简写 Object.assign(new Error(), {name() {}});形态三:经变量追踪的赋值
规则会记住“已知是内置错误的变量”,即使错误实例先赋给变量,后续通过变量赋值或Object.assign依然会被拦截:
// 快照 invalid(10):const 声明后经变量 Object.assign const error = new Error(); Object.assign(error, {name}); // 快照 invalid(11):let 声明后经赋值得到 TypeError,再 Object.assign let error; error = new TypeError(); Object.assign(error, {'stack': stack}); // 快照 invalid(12)/(13)/(14):直接对变量属性赋值 const error = new Error(); error.name = 'Custom'; let error2; error2 = new TypeError(); error2.stack = stack; const error3 = new Error(); error3['name'] = name; // 方括号 + 字符串字面量同样识别形态四:直接属性赋值(含链式与裸调用)
不经过中间变量、直接对构造结果赋值,同样会被标记:
// 快照 invalid(30):链式 new 表达式 new Error().stack = stack; // 快照 invalid(31):无 new 的函数调用形态 Error().cause = cause; // 快照 invalid(32):链式 AggregateError new AggregateError([], "message").errors = errors;形态五:作用域与变量帧追踪
这是快照中覆盖最密集的部分(invalid(15) ~ invalid(29)),验证了规则对各类块级作用域、循环与var提升场景的追踪能力:
if块内的声明与赋值(invalid(15)/(16)/(17)):const/let声明的错误变量在块内赋值属性,或跨嵌套块使用,均能追踪;- 函数作用域边界(invalid(18)):
let error;在外层,function setup() { error = new Error(); error.name = name; }在同一函数体内完成构造与赋值,会被标记; var声明与提升(invalid(19)/(20)/(21)):var error = new Error()声明于if块或裸块内,块外使用error.name = name,规则利用变量帧传播机制仍能确认其类型;switchcase 作用域(invalid(23)/(24)):switch内case分支中的构造与赋值;for循环初始化(invalid(25)/(26)/(27)):for (const error = new Error(); condition;)与for (error = new Error(); ...)两种初始化写法;- 变量重赋值类型更新(invalid(28)/(29)):
let error = new Error(); { error = new AggregateError([]); } error.errors = errors;—— 变量先被赋为Error,随后被重赋为AggregateError,规则会把追踪信息更新为最新类型,于是errors属性成为违规点。
TypeScript 语法支持
规则源码明确采用“纯语法”的方式支持 TypeScript:只剥离as断言与satisfies表达式(通过unwrapTypeScriptExpression),不依赖 type-aware parser services(见 rules/no-error-property-assignment.js)。快照中的 4 个 TypeScript invalid 用例:
// 快照 TS invalid(1)~(4) const error = new Error() as Error; error.name = 'Custom'; (new Error() as Error).stack = stack; Object.assign(new Error() as Error, {name}); Object.assign( new AggregateError([], "message") satisfies AggregateError, {errors}, );源码注释解释了为何不做类型推断:一个名为Error的类型既可以描述自定义子类,也可能描述来自其他模块的值,类型感知匹配会超出“语法上已知的内置实例”这一边界,并使结果依赖 parser 配置而变得不确定。
报错消息与定位方式
快照报告清晰展示了每个违规的定位规则——精确指向被禁止的属性键本身:
> 1 | Object.assign(new Error(), {name}) | ^^^^ Do not assign to `name` on a built-in error.- 对
Object.assign:定位到源对象中违规属性键(如name、"stack"、["cause"])所在位置,而非整个Object.assign调用; - 对直接赋值:定位到成员表达式中的属性部分(如
error.name = ...中的name,error['name'] = ...中的'name',new Error().stack = stack中的stack)。
定位精度与源码实现一致:getObjectAssignProblems对每个违规属性用getPropertyProblem(property.key, propertyName)报告(属性键节点),getDirectAssignmentProblem则用getPropertyProblem(memberExpression.property, propertyName)报告(成员属性节点)。
不会被标记的情况(valid 用例)
快照只记录 invalid 输出,但规则的真实行为边界需要结合测试文件 test/no-error-property-assignment.js 的 valid 用例理解:
// ✅ 允许:目标不是已知内置错误(来源未知的变量) const error = getError(); error.name = name; Object.assign(error, {name}); // ✅ 允许:不在禁止清单内的属性(message、code 等) Object.assign(new Error(), {message}); Object.assign(new Error(), {code}); const error2 = new Error(); error2.message = message; // ✅ 允许:非 AggregateError 上的 errors Object.assign(new Error(), {errors}); // ✅ 允许:非静态可解析的属性键 const error3 = new Error(); error3[computed] = name; error3["mess" + "age"] = message; // ✅ 允许:读取与删除不属于“赋值” const error4 = new Error(); error4.name; delete error4.name; // ✅ 允许:内置构造器名被遮蔽(本地定义了同名函数/对象) const Error = function () {}; const error5 = new Error(); error5.name = name; const Object = {assign() {}}; Object.assign(new Error(), {name}); // 这里 Object 是本地的,不是全局 Object // ✅ 允许:变量被重赋值为非错误对象(或条件分支中可能不是错误) let error6 = new Error(); error6 = {}; error6.name = name; let error7 = new Error(); if (condition) { error7 = {}; } error7.name = name; // ✅ 允许:跨函数边界无法确定类型时放弃推断 let error8; function setup() { error8 = new Error(); } error8.name = name; // ✅ 允许:自定义 Error 子类内部的 this 赋值 class CustomError extends Error { constructor() { super(); this.name = 'CustomError'; } }上述 valid 行为背后的实现要点:
- 全局引用检查:
getErrorConstructorName要求context.sourceCode.isGlobalReference(node.callee),即构造器必须是全局Error/TypeError/AggregateError等;本地遮蔽同名函数或对象时直接放行; - 仅静态属性名:
getStaticPropertyName与getStaticPropertyNameFromProperty只解析标识符属性与非计算字符串字面量键,计算表达式(error[computed])无法静态解析,直接跳过; - 跨函数边界保守处理:变量帧追踪以函数为硬边界,
let error;在外层、赋值发生在其他函数时,无法安全传播类型信息,选择不报告; - 条件分支谨慎更新:
if (condition) { error = {}; }这类条件分支中的重赋值会被视为“不再确定是内置错误”,从而放弃后续检查(对应源码中的'conditional'追踪模式)。
启用方式与配置
推荐配置
规则源码 rules/no-error-property-assignment.js 声明:
meta.type: 'problem':属于“代码确实有问题”的规则;docs.recommended: 'unopinionated':默认进入unopinionated推荐配置;schema: []:没有任何可配置选项,开与关之外无需任何参数;languages: ['js/js']:作用于 JavaScript(含 JSX 语法)。
规则已在 readme.md 的规则总表中登记,✅表示包含于recommended配置,☑️表示包含于unopinionated配置。若你直接使用插件提供的recommended扁平配置,该规则即默认开启。
手动配置
也可在 ESLint 扁平配置中手动启用:
// eslint.config.js import unicorn from 'eslint-plugin-unicorn'; export default [ // ...其他配置 { plugins: {unicorn}, rules: { 'unicorn/no-error-property-assignment': 'error', }, }, ];该规则在 rules/index.js 中作为具名导出注册,规则 id 即no-error-property-assignment。
与源码实现的对应:变量帧追踪原理
快照中大量“先构造、后赋值”的用例之所以能被识别,依赖源码实现的一套轻量级作用域帧(frame)追踪机制(rules/no-error-property-assignment.js):
- 以
Program、BlockStatement、StaticBlock、SwitchCase为单位建立帧栈,随遍历进出push/pop(见context.on/context.onExit); - 遍历
VariableDeclarator与AssignmentExpression时,把“已知是内置错误”的变量及其构造器名记入当前帧的Map; - 判断赋值语句的追踪模式:
for初始化、直接子语句为normal(可确定性更新),if分支体与无大括号循环体为conditional(条件性更新); - 涉及跨块传播时检查“透明帧”(
BlockStatement且父节点为块类结构、非函数边界),一旦遇到函数边界(isFunctionBoundary)则停止传播——这正是 valid 用例中“跨函数赋值不报告”的原因; - 变量被重赋值时,用最新构造器名覆盖旧记录,因此
let error = new Error(); { error = new AggregateError([]); } error.errors = errors能被正确标记。
这套机制只依赖 AST 语法信息,不调用 parser services、不做类型推导,因此结果稳定、可复现——这也是快照报告能够精确对应到每个用例逐行输出的原因。快照文件本身由 AVA 测试框架在npm test时自动生成/校验(文件头部注明 Generated by AVA),若规则行为变更,快照会被更新以反映新的真实输出。
小结
no-error-property-assignment通过“语法上已知的内置错误”这一严格边界,在不引入类型系统复杂性的前提下,拦截了对name、stack、cause(AggregateError另加errors)的覆盖式赋值,覆盖Object.assign与直接赋值两大类形态,并原生支持 TypeScript 的as/satisfies语法。其变量帧追踪机制兼顾了块级作用域、var提升、重赋值与函数边界,做到“该查的精准查、不确定的果断放行”。配合 docs/rules/no-error-property-assignment.md 与快照报告,你可以快速验证规则在你代码库中的实际行为,并用'unicorn/no-error-property-assignment': 'error'将其纳入持续集成的代码检查。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考