eslint-plugin-unicorn 规则实战:throw-new-error 如何强制使用new创建错误对象
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
本文聚焦 eslint-plugin-unicorn 的
throw-new-error规则:它强制在创建错误对象时使用new关键字,涵盖规则背景、配置方式、源码检测逻辑、自动修复机制,以及基于 AVA 快照测试的 27 个无效用例全解,帮助你彻底理解该规则的判定边界与修复行为。
规则背景与动机
JavaScript 语言允许不借助new关键字直接调用Error()或TypeError()等内置错误构造函数,其行为与new Error()几乎等价。但这种写法不够明确:调用普通函数与构造错误对象在语义上容易混淆,可读性较差。
eslint-plugin-unicorn 的throw-new-error规则正是为此设计:要求创建错误对象时必须显式使用new。规则的完整说明见 docs/rules/throw-new-error.md,其核心主张是:
While it's possible to create a new error without using the
newkeyword, it's better to be explicit.
该规则属于suggestion类型,并且可被--fix自动修复。在配置层面,它被同时纳入两套推荐配置(见规则文档头部的配置声明):
- ✅
recommended:推荐配置 - ☑️
unopinionated:非观点性配置(该规则在recommended中实际标注为unopinionated,见 rules/throw-new-error.js)
规则效果速览
内置错误构造函数
// ❌ 错误写法 const error = Error('unicorn'); // ✅ 正确写法 const error = new Error('unicorn');// ❌ 错误写法 throw TypeError('unicorn'); // ✅ 正确写法 throw new TypeError('unicorn');成员表达式中的错误构造
// ❌ 错误写法 throw lib.TypeError('unicorn'); // ✅ 正确写法 throw new lib.TypeError('unicorn');源码级检测逻辑解析
规则实现位于 rules/throw-new-error.js,整体流程围绕CallExpression节点展开。核心逻辑可分四步:
1. 名称匹配规则
规则定义了一个自定义错误命名正则:
const customError = /^(?:[A-Z][\da-z]*)*Error$/;该正则要求 callee 名称必须以Error结尾,且前面的每一段都要满足「大写字母开头 + 数字或小写字母」的模式。这意味着它不仅匹配内置错误(Error、TypeError、EvalError、RangeError、ReferenceError、SyntaxError、URIError),也匹配用户自定义错误类(如CustomError、FooBarBazError、ABCError、Abc3Error)。
2. 匹配的节点形态
规则只检查两种 callee 形态(见 rules/throw-new-error.js):
Identifier:Error()、TypeError()、CustomError()这类直接调用;MemberExpression(非计算属性、property 为Identifier):lib.Error()、lib.mod.Error()、lib[mod].Error()这类通过对象属性访问的错误构造。
// 关键判断条件 (callee.type === 'Identifier' && customError.test(callee.name)) || ( callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier' && customError.test(callee.property.name) )3. 明确排除的场景
以下情况规则不会报错(对应测试文件 test/throw-new-error.js 中的valid用例):
| 排除场景 | 示例 | 原因 |
|---|---|---|
已使用new | throw new Error() | 本就不存在违规 |
名称不匹配FooError风格 | throw getError()、throw lib.getError() | 不是错误构造函数命名 |
非CallExpression | throw CustomError | 只是引用,没有调用 |
| callee 不是 Identifier/MemberExpression | throw getErrorConstructor()() | 返回值不确定,无法判断 |
| 计算属性成员表达式 | throw lib[Error]() | computed为 true,属性名是变量 |
| 属性不是 Identifier | throw lib["Error"]() | 字符串属性名不匹配Identifier判断 |
| 可选链调用 | throw Error?.()、throw lib?.Error()、throw lib?.foo.Error() | new无法作用于可选链调用 |
| 可选链位于 callee 内部 | throw lib?.foo.Error().message | 即使调用不在链最外层也排除 |
Effect 库的Data.TaggedError | class QueryError extends Data.TaggedError('QueryError') {} | 这是 Effect 库的特殊 API,见 issue #2654 |
规则对可选链的排除逻辑很关键:new关键字无法作用于可选链调用(如Error?.()不能写成new Error?.()),因此当调用本身是可选链(node.optional)或 callee 中包含可选链元素(hasOptionalChainElement(callee))时直接返回(见 rules/throw-new-error.js)。
4. 装饰器调用的特殊处理
规则对直接作为装饰器调用的表达式做了豁免(node.parent.type === 'Decorator'时返回),避免误伤 TypeScript 装饰器语法,例如:
@RegisterServiceError() export class SomeError extends Error {}但注意:装饰器的参数仍会被检查。测试用例@Decorator(Error()) export class SomeError extends Error {}就是无效代码,因为Error()作为装饰器参数被调用(见 test/throw-new-error.js)。
自动修复机制:从Error()到new Error()
规则报错时的消息为Use `new` when creating an error.(messageId为throw-new-error),同时提供自动修复。修复逻辑委托给switchCallExpressionToNewExpression(见 rules/fix/switch-call-expression-to-new-expression.js),该函数也同时被new-for-builtins规则复用,实现在 rules/fix/index.js。
修复器由三部分 yield 组成:
1. 关键字周围空格修复
fixSpaceAroundKeyword(见 rules/fix/fix-space-around-keywords.js)会在调用表达式紧邻await、of等关键字时补上空格,避免修复后产生awaitnew Error()之类的语法错误。它识别两类有问题的 token:
Keyword类型且值全是小写字母;Identifier且值为of(对应for...of)或await。
2. 插入new前缀
yield fixer.insertTextBefore(node, 'new ');3. 为复杂 callee 添加括号
修复函数会检查 callee 是否包含调用表达式(如getGlobalThis().Error、utils.getGlobalThis().Error)。如果成员表达式内部含有CallExpression,则必须加括号包裹,否则会改变求值顺序:
// 修复前 throw getGlobalThis().Error() // 修复后 throw new (getGlobalThis().Error)()判断逻辑实现在 rules/utils/should-add-parentheses-to-new-expression-callee.js:递归下钻MemberExpression.object直到非成员表达式节点,若最终是CallExpression则需要加括号。同时,如果 callee 本身已被括号包裹(isParenthesized),则不再重复添加。
快照测试全景:27 个无效用例深度解读
规则的测试采用 AVA 的snapshot 模式(test.snapshot),快照文件为 test/snapshots/throw-new-error.js.md,对应源测试文件 test/throw-new-error.js。每个快照记录「输入 → 报错位置与消息 → 修复输出」三部分。下面按场景归类解读全部 27 个无效用例。
场景一:内置错误构造函数直接调用(用例 1、12-17)
// 输入 throw Error() // → throw new Error() throw TypeError() // → throw new TypeError() throw EvalError() // → throw new EvalError() throw RangeError() // → throw new RangeError() throw ReferenceError() // → throw new ReferenceError() throw SyntaxError() // → throw new SyntaxError() throw URIError() // → throw new URIError()这类最简单:Error及全部内置错误子类(TypeError、EvalError、RangeError、ReferenceError、SyntaxError、URIError)都匹配customError正则。修复后都加上new前缀。
场景二:带参数的调用(用例 7-11)
throw Error('foo') // → throw new Error('foo') throw CustomError('foo') // → throw new CustomError('foo') throw FooBarBazError('foo') // → throw new FooBarBazError('foo') throw ABCError('foo') // → throw new ABCError('foo') throw Abc3Error('foo') // → throw new Abc3Error('foo')注意Abc3Error也被识别——正则(?:[A-Z][\da-z]*)*中的\d允许段内出现数字(Abc3满足「大写开头 + 数字小写混合」),这验证了命名规则对含数字错误类名的覆盖。
场景三:成员表达式(用例 3-6)
throw lib.Error() // → throw new lib.Error() throw lib.mod.Error() // → throw new lib.mod.Error() throw lib[mod].Error() // → throw new lib[mod].Error() throw (lib.mod).Error() // → throw new (lib.mod).Error()这里展示了成员表达式修复的两个细节:
lib[mod].Error()中lib[mod]是计算属性,但规则的检查只看callee.property(即.Error这一层),因此仍然命中;(lib.mod).Error()修复时new插入在括号外层:new (lib.mod).Error()。
场景四:冗余括号(用例 2、18-19)
throw (Error)() // → throw new (Error)() throw (( URIError() )) // → throw (( new URIError() )) throw (( URIError ))() // → throw new (( URIError ))()三者的修复位置不同:(Error)()中new加在括号外;(( URIError() ))中Error()本身是 callee,new插入在最内层Error前;(( URIError ))()中调用发生在括号外,new加在整体前。快照的报错高亮区间(^^^^^^^)也精确指向实际调用位置。
场景五:全局对象链上的错误(用例 20-22)
throw getGlobalThis().Error() // → throw new (getGlobalThis().Error)() throw utils.getGlobalThis().Error() // → throw new (utils.getGlobalThis().Error)() throw (( getGlobalThis().Error ))() // → throw new (( getGlobalThis().Error ))()这组用例直接验证了「callee 含调用表达式必须加括号」的修复逻辑:getGlobalThis().Error是成员表达式且其object是CallExpression,因此必须包裹成(getGlobalThis().Error)再前置new,否则new getGlobalThis().Error()的语义会出错。
场景六:非 throw 上下文(用例 23-27)
规则并不局限于throw语句,任何创建错误对象的CallExpression都会被检查:
// 变量声明 const error = Error() // → const error = new Error() // 函数参数 throw Object.assign(Error(), {foo}) // → throw Object.assign(new Error(), {foo}) // Promise reject 回调 new Promise((resolve, reject) => { reject(Error('message')); // → reject(new Error('message')); }); // 数组索引访问 function foo() { return[globalThis][0].Error('message'); // → return new [globalThis][0].Error('message'); } // 装饰器参数(TypeScript + decorators 解析器) @Decorator(Error()) // → @Decorator(new Error()) export class SomeError extends Error {}其中return[globalThis][0].Error('message')展示了复杂计算成员表达式的修复结果new [globalThis][0].Error('message')——由于 callee 的最内层 object 是ArrayExpression而非CallExpression,不需要加括号。
@Decorator(Error())用例使用了 TypeScript 解析器与decorators: true的 parserOptions(见 test/throw-new-error.js),证明规则对装饰器参数中的错误构造依然生效,同时验证了修复不会破坏装饰器语法。
快照工作流说明
快照文件由 AVA 测试框架自动生成与校验(文件头标注 "Generated by AVA")。当规则行为或消息文案变化时,需要更新快照(通常通过npm test -- --update-snapshots或 AVA 的-u参数)。快照的「Input / Error Message / Output」三段式结构,为回归测试提供了逐字节的精确比对,任何修复输出的细微变化都会在 CI 中被捕获。
规则注册与整体集成
该规则通过 rules/index.js 注册为throw-new-error:
export {default as 'throw-new-error'} from './throw-new-error.js';在实际项目中启用该规则:
// eslint.config.js(flat config 写法) import unicorn from 'eslint-plugin-unicorn'; export default [ { plugins: {unicorn}, rules: { 'unicorn/throw-new-error': 'error', }, }, ];启用后运行npx eslint . --fix即可自动修复所有未使用new的错误构造。
总结
throw-new-error是 eslint-plugin-unicorn 中一个「小而精」的规则:核心逻辑仅几十行,却通过精准的命名正则、可选链豁免、装饰器特判和 Effect 库兼容处理,覆盖了错误构造的绝大多数真实场景。其自动修复器还复用了new-for-builtins的公共修复基础设施,对await、of相邻关键字和含调用表达式的 callee 做了语法安全处理。27 个快照用例完整记录了每种边界情况的报错位置与修复输出,是理解该规则行为最权威的参考,也展示了项目「文档 → 实现 → 测试快照」三位一体的工程质量。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考