eslint-plugin-unicorn 的 isolated-functions 规则:阻止孤立函数捕获外部变量
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
isolated-functions是 eslint-plugin-unicorn 提供的一条problem类型规则,用于检测那些因执行上下文限制(如 worker、子进程、浏览器自动化、远程执行)而必须与外部作用域隔离的函数,是否错误地使用了外部作用域变量、this或super。阅读本文后,你将掌握该规则的四种配置维度(functions、selectors、comments、overrideGlobals)以及它在recommended配置中的默认行为,并能结合源码理解其作用域分析原理,在自己的项目中正确启用与定制。
为什么函数会被要求“隔离”
某些函数在运行时并不在当前 JavaScript 执行上下文中执行,而是被序列化、传输到其他上下文(worker、子进程、浏览器页面、远程服务器)后再运行。此时函数体对外部作用域的闭包变量将不可见,强行引用会在运行时抛错。这类函数必须做到“自包含”——只依赖自身参数、自身局部变量以及真正意义上的全局变量。
常见需要隔离的场景:
- 传给
makeSynchronous()的函数(在 worker 或子进程中执行) - 传给
workerize()的函数 - Puppeteer / Playwright 风格代码中传给
page.evaluate()的函数 - 通过
Function.prototype.toString()被序列化的函数 - Server actions 或其他远程执行上下文
- 带有特定 JSDoc 注解的函数
本规则的目标正是提前在静态分析阶段揪出这些隐患,把运行期错误转化为编辑器里可修复的 lint 错误。
规则默认行为
该规则默认在recommended配置中启用,在unopinionated配置中被禁用,meta.type为problem(意味着它报告的通常是确实需要修复的问题而非风格偏好)。这一开关状态定义在规则文件 rules/isolated-functions.js 的meta.docs.recommended: true中。
默认情况下,规则允许隔离函数使用:
- ES 全局变量(如
Array、Map、URL),具体集合按languageOptions.ecmaVersion对应的globals包版本解析(见源码中globals[es${context.languageOptions.ecmaVersion}] ?? globals.builtins一行); - ESLint 从配置或
/* global */注释解析出的全局变量(如console、fetch); - 函数自身的参数、局部变量,以及嵌套的、拥有独立
this上下文的普通函数和类方法内部的引用。
除此之外,任何对外部作用域变量的捕获都会被报告。
规则如何判定一个函数是“孤立的”
从源码 rules/isolated-functions.js 的reasonForBeingIsolatedFunction函数可以看出,一个函数会被视为隔离函数,当且仅当命中以下四种途径之一,且每种途径都会生成一条“原因”(reason),最终拼进报错信息中:
- 紧随其后的注释命中
comments列表(如/** @isolated */),报告原因形如follows comment "@isolated"; - 作为
functions列表中命名的函数的实参,报告原因形如callee of function named "makeSynchronous"; - 命中内置的默认调用场景:
browser.execute(fn)→callee of method named "browser.execute"page.evaluate(fn)→callee of method named "page.evaluate"chrome.scripting.executeScript({func: () => {}})/browser.scripting.executeScript({func: () => {}})→property "func" passed to "chrome.scripting.executeScript"等;
- 命中
selectors中配置的 ESLint AST 选择器,报告原因形如matches selector "..."。
判定完成后,规则通过nodeScope.through(ESLint scope-manager 中“未能在此作用域内解析的引用”列表)扫描函数体内所有悬空引用,并据此上报三类错误:
| messageId | 触发条件 | 报错文案 |
|---|---|---|
externally-scoped-variable | 引用外部作用域变量 | Variable {{name}} not defined in scope of isolated function. Function is isolated because: {{reason}}. |
this-expression | 在隔离函数中使用this | Unexpected \this` in isolated function. ...` |
super | 在隔离函数中使用super | Unexpected \super` in isolated function. ...` |
注意:this/super的检查只针对隔离函数自身的词法上下文。测试用例test/isolated-functions.js中明确验证了“@isolated函数可以包含拥有自身this的嵌套普通函数”与“可以包含拥有自身this的嵌套类方法”,因为这两者的this不依赖外部对象状态。
快速上手示例
下面的例子来自规则文档 docs/rules/isolated-functions.md,展示了规则的核心报错与两种修复方式:
/* global fetch, console */ import makeSynchronous from 'make-synchronous'; const url = 'https://example.com'; const getText = makeSynchronous(async () => { const response = await fetch(url); // ❌ 'url' is not defined in isolated function scope return response.text(); }); // ✅ 在隔离函数作用域内定义所有变量 const getText = makeSynchronous(async () => { const url = 'https://example.com'; // 变量定义在函数作用域内 const response = await fetch(url); return response.text(); }); // ✅ 或者把变量作为参数传入 const getText = makeSynchronous(async (url) => { // 变量作为参数 const response = await fetch(url); return response.text(); }); console.log(getText('https://example.com'));fetch与console因/* global */注释被允许使用,而url是模块作用域变量,会被判定为外部作用域变量。
JSDoc 注解方式同样受支持,且适用于对象方法、类方法等场景:
const foo = 'hi'; /** @isolated */ function abc() { return foo.slice(); // ❌ 'foo' is not defined in isolated function scope } const object = { /** @isolated */ method() { return this.foo; // ❌ 'this' depends on external object state }, }; // ✅ /** @isolated */ function abc() { const foo = 'hi'; // 变量定义在函数作用域内 return foo.slice(); }Options 详解
该规则接受一个对象类型的选项,整体 schema 位于 rules/isolated-functions.js 的schema中,且顶层additionalProperties: false,即只能配置以下四个键。
functions
- 类型:
string[] - 默认值:
['makeSynchronous', 'workerize']
作为这些函数实参传入的函数将被视为隔离函数。默认值之外的常见隔离执行 API 也会被自动识别:
browser.execute(fn)page.evaluate(fn)chrome.scripting.executeScript({func: () => {}})browser.scripting.executeScript({func: () => {}})
serialize、isolate、memoize这类泛化名称默认不启用——因为并非所有调用都会真正序列化函数,需要你根据项目实际把对应名称加入functions。
需要注意的边界(均有测试用例覆盖):
- 内置场景要求函数必须是第一个实参(
browser.execute(() => 'browser', () => foo.slice())中第二个函数不会被识别); executeScript的func必须是对象字面量中静态的func属性,且该对象必须是executeScript的第一个实参;动态计算属性名([func])或标识符形式的属性值({func})不会被识别;page['evaluate']、browser['execute']这类计算属性调用默认不识别。
selectors
- 类型:
string[] - 默认值:
[]
用于自定义命名约定或框架特定模式的 ESLint AST 选择器列表。选择器必须匹配到“应被视为隔离的函数节点”;若要隔离“传给某个调用表达式的函数实参”,则用选择器语法直接选中该函数实参:
{ 'unicorn/isolated-functions': [ 'error', { selectors: [ 'FunctionDeclaration[id.name=/lambdaHandler.*/]', 'CallExpression[callee.property.name=/CodemodScript/] > :function' ] } ] }comments
- 类型:
string[] - 默认值:
['@isolated']
带有这些标记的行注释、块注释或 JSDoc 注释的函数将被视为隔离。所谓“标记”的定义是:注释内容要么就是该标记本身,要么以标记开头、后跟连字符再接说明文字,例如// @isolated - this function will be stringified。源码中匹配逻辑为previousComment === comment || previousComment.startsWith(${comment} -) || previousComment.startsWith(${comment} --),匹配前会去除 JSDoc 的*前缀并转小写。
标记注释同样适用于:对象方法、值为函数表达式或箭头函数的对象属性、类方法。这些隔离函数不能使用this或super,所需状态应通过参数传入。注释能“附着”的节点还包括变量声明、导出声明等(源码canCommentApplyToParent列出了VariableDeclarator、VariableDeclaration、ExportNamedDeclaration、ExportDefaultDeclaration以及Property/MethodDefinition)。
{ 'unicorn/isolated-functions': [ 'error', { comments: [ '@isolated', '@remote' ] } ] }overrideGlobals
- 类型:
object - 默认值:
{}
逐名覆盖全局变量的处理方式。空对象表示不做任何覆盖,ES 全局变量与 ESLint 解析出的全局变量仍然生效。每个键为全局变量名,值为:
| 值 | 行为 |
|---|---|
'readonly' | 允许读取,禁止写入 |
'writable'(schema 中亦接受'writeable') | 允许读取与写入 |
'off' | 不允许使用 |
{ 'unicorn/isolated-functions': [ 'error', { overrideGlobals: { console: 'writable', // 允许且可写 fetch: 'readonly', // 允许但只读 process: 'off' // 不允许 } } ] }从源码getAllowedGlobalValue可以看出优先级关系:overrideGlobals条目 > 配置中显式设为'off'的全局变量 > ES/配置全局变量。写入只读全局变量时,报错信息会附加(global variable is not writable)后缀。另外,无论 globals 如何配置,模块/脚本中真实声明的变量永远不能通过 globals 配置“洗白”——测试用例test/isolated-functions.js验证了/* global foo */、languageOptions.globals与overrideGlobals均无法让被捕获的模块变量const foo = 'hi'通过检查。
配置示例合集
自定义函数名
{ 'unicorn/isolated-functions': [ 'error', { functions: [ 'makeSynchronous', 'workerize', 'createWorker', 'serializeFunction' ] } ] }Lambda 函数命名约定
{ 'unicorn/isolated-functions': [ 'error', { selectors: [ 'FunctionDeclaration[id.name=/lambdaHandler.*/]' ] } ] }const foo = 'hi'; function lambdaHandlerFoo() { // ❌ 将被标记为隔离 return foo.slice(); } function someOtherFunction() { // ✅ 不会标记 return foo.slice(); } createLambda({ name: 'fooLambda', code: lambdaHandlerFoo.toString(), // 函数将被序列化 });配置全局变量
/* global console, fetch */ makeSynchronous(async () => { console.log('Starting...'); // ✅ 若 console 已配置为全局变量则允许 const response = await fetch('https://api.example.com'); // ✅ 若 fetch 已配置为全局变量则允许 return response.text(); });覆盖特定全局变量
overrideGlobals只覆盖列出的名字,不会替换 ES 全局变量或来自配置//* global */注释的全局变量。
{ 'unicorn/isolated-functions': [ 'error', { overrideGlobals: { console: 'writable', // 允许且可写 fetch: 'readonly', // 允许但只读 URL: 'readonly' // 允许但只读 } } ] }// ✅ 使用的全局变量要么是 ES 全局变量、ESLint 解析出的全局变量,要么被显式覆盖 makeSynchronous(async () => { console.log('Starting...'); // ✅ 允许的全局变量 const response = await fetch('https://api.example.com'); // ✅ 允许的全局变量 const url = new URL(response.url); // ✅ 允许的全局变量 return response.text(); }); makeSynchronous(async () => { const response = await fetch('https://api.example.com', { headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` // ❌ 'process' 未配置为 ESLint 全局变量或 overrideGlobals 条目 } }); const url = new URL(response.url); return response.text(); }); // ❌ 尝试写入只读全局变量 makeSynchronous(async () => { fetch = null; // ❌ 'fetch' 是只读的 console.log('Starting...'); });预定义全局变量集合
如需一次性启用一批预定义全局变量,可像在languageOptions中使用globals包那样操作:
import {defineConfig} from 'eslint/config'; import unicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; export default defineConfig([ { plugins: { unicorn, }, languageOptions: { globals: { ...globals.builtin, ...globals.applescript, ...globals.greasemonkey, }, }, rules: { 'unicorn/isolated-functions': [ 'error', ], }, }, ]);源码实现原理深度解析
作用域分析的入口
规则在context.onExit(functionTypes, ...)中注册了函数节点的退出钩子,并在每个函数退出时尝试为其寻找“隔离原因”;同时为每个配置的 selector 注册对应的退出钩子(源码 rules/isolated-functions.js 第 324–344 行)。一旦找到原因,便调用reportIsolatedFunctionProblems(node, reason)。
悬空引用与 TS 类型豁免
reportIsolatedFunctionProblems遍历nodeScope.through(即当前作用域无法解析的引用数组)。对每个引用:
- 若其父节点是
TSTypeReference或TSTypeQuery(TypeScript 类型上下文),直接跳过——类型在编译期即被擦除,不会进入运行时闭包。测试中验证了type X = typeof myType extends MyType ? true : false;等类型引用在隔离函数内是合法的; - 通过
getAllowedGlobalValue判定该引用是否为“允许的全局变量”; - 若允许但只读且发生写入,则附加
(global variable is not writable)原因后上报;若可写则放行;若为'off'或非全局变量则上报externally-scoped-variable。
getAllowedGlobalValue内部先检查overrideGlobals,再检查configuredGlobals(由globals包按ecmaVersion提供的 ES 全局变量与languageOptions.globals合并而成),最后用sourceCode.isGlobalReference(identifier)确保只有真正的全局引用才被放行——这正是“模块变量永远无法通过 globals 配置洗白”的机制来源。
嵌套作用域的问题收集
getFunctionContextProblems以递归方式遍历函数体 AST:凡遇到ThisExpression或Super节点即上报对应消息;遇到非箭头函数的嵌套函数、类声明则视情况终止(普通函数与类拥有自己的this/super上下文),但对箭头函数(词法继承this)和类表达式会继续深入。类表达式的superClass与计算属性名也在检查范围内。
边界情况与测试佐证
规则配套了 815 行的测试文件 test/isolated-functions.js,其中值得注意的行为边界包括:
- 泛化函数名不隔离:
memoize、serialize、isolate默认不触发; - 非
page的evaluate不启用:frame.evaluate(() => foo.slice())默认不报错,文档建议通过selectors为不使用page变量名的项目 API 主动开启; - 写入只读全局变量会被报告:
location = new URL(...)、process.env.FOO = 'bar'这类属性级写入合法,但process = {env: {}}这种整体赋值会因process只读而报错; /* global Array:off */或languageOptions.globals: {Array: 'off'}可显式禁用 ES 全局变量,overrideGlobals: {Array: 'off'}亦然;- 继承的属性名不被当作全局变量:
makeSynchronous(() => constructor)会被报告为外部变量。
总结与实践建议
isolated-functions面向的是“函数被搬离原执行上下文”这一日渐常见的编程模式(worker 化、浏览器自动化、函数序列化、服务端远程执行)。使用时建议遵循以下原则:
- 对 worker 化 / 自动化代码优先依赖默认的
functions与内置browser.execute、page.evaluate、executeScript识别,零配置即可获得recommended级别的保护; - 对框架自定义命名约定使用
selectors,对可读性优先的注解方式使用comments; - 全局变量的放行遵循“显式优于隐式”:通过
languageOptions.globals或/* global */声明真实全局,用overrideGlobals精确收紧读写权限,不要试图用它“赦免”真正的模块级闭包捕获; - 记住隔离函数的黄金法则——状态走参数、工具走全局、上下文走局部,这样无论函数被序列化到哪,行为都保持一致。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考