eslint-plugin-unicorn 的 isolated-functions 规则:阻止孤立函数捕获外部变量
2026/9/18 21:05:53 网站建设 项目流程

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、子进程、浏览器自动化、远程执行)而必须与外部作用域隔离的函数,是否错误地使用了外部作用域变量、thissuper。阅读本文后,你将掌握该规则的四种配置维度(functionsselectorscommentsoverrideGlobals)以及它在recommended配置中的默认行为,并能结合源码理解其作用域分析原理,在自己的项目中正确启用与定制。

为什么函数会被要求“隔离”

某些函数在运行时并不在当前 JavaScript 执行上下文中执行,而是被序列化、传输到其他上下文(worker、子进程、浏览器页面、远程服务器)后再运行。此时函数体对外部作用域的闭包变量将不可见,强行引用会在运行时抛错。这类函数必须做到“自包含”——只依赖自身参数、自身局部变量以及真正意义上的全局变量。

常见需要隔离的场景:

  • 传给makeSynchronous()的函数(在 worker 或子进程中执行)
  • 传给workerize()的函数
  • Puppeteer / Playwright 风格代码中传给page.evaluate()的函数
  • 通过Function.prototype.toString()被序列化的函数
  • Server actions 或其他远程执行上下文
  • 带有特定 JSDoc 注解的函数

本规则的目标正是提前在静态分析阶段揪出这些隐患,把运行期错误转化为编辑器里可修复的 lint 错误。

规则默认行为

该规则默认在recommended配置中启用,在unopinionated配置中被禁用,meta.typeproblem(意味着它报告的通常是确实需要修复的问题而非风格偏好)。这一开关状态定义在规则文件 rules/isolated-functions.js 的meta.docs.recommended: true中。

默认情况下,规则允许隔离函数使用:

  • ES 全局变量(如ArrayMapURL),具体集合按languageOptions.ecmaVersion对应的globals包版本解析(见源码中globals[es${context.languageOptions.ecmaVersion}] ?? globals.builtins一行);
  • ESLint 从配置或/* global */注释解析出的全局变量(如consolefetch);
  • 函数自身的参数、局部变量,以及嵌套的、拥有独立this上下文的普通函数和类方法内部的引用。

除此之外,任何对外部作用域变量的捕获都会被报告。

规则如何判定一个函数是“孤立的”

从源码 rules/isolated-functions.js 的reasonForBeingIsolatedFunction函数可以看出,一个函数会被视为隔离函数,当且仅当命中以下四种途径之一,且每种途径都会生成一条“原因”(reason),最终拼进报错信息中:

  1. 紧随其后的注释命中comments列表(如/** @isolated */),报告原因形如follows comment "@isolated"
  2. 作为functions列表中命名的函数的实参,报告原因形如callee of function named "makeSynchronous"
  3. 命中内置的默认调用场景
    • 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"等;
  4. 命中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在隔离函数中使用thisUnexpected \this` in isolated function. ...`
super在隔离函数中使用superUnexpected \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'));

fetchconsole/* 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: () => {}})

serializeisolatememoize这类泛化名称默认不启用——因为并非所有调用都会真正序列化函数,需要你根据项目实际把对应名称加入functions

需要注意的边界(均有测试用例覆盖):

  • 内置场景要求函数必须是第一个实参browser.execute(() => 'browser', () => foo.slice())中第二个函数不会被识别);
  • executeScriptfunc必须是对象字面量中静态的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 的*前缀并转小写。

标记注释同样适用于:对象方法、值为函数表达式或箭头函数的对象属性、类方法。这些隔离函数不能使用thissuper,所需状态应通过参数传入。注释能“附着”的节点还包括变量声明、导出声明等(源码canCommentApplyToParent列出了VariableDeclaratorVariableDeclarationExportNamedDeclarationExportDefaultDeclaration以及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.globalsoverrideGlobals均无法让被捕获的模块变量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(即当前作用域无法解析的引用数组)。对每个引用:

  1. 若其父节点是TSTypeReferenceTSTypeQuery(TypeScript 类型上下文),直接跳过——类型在编译期即被擦除,不会进入运行时闭包。测试中验证了type X = typeof myType extends MyType ? true : false;等类型引用在隔离函数内是合法的;
  2. 通过getAllowedGlobalValue判定该引用是否为“允许的全局变量”;
  3. 若允许但只读且发生写入,则附加(global variable is not writable)原因后上报;若可写则放行;若为'off'或非全局变量则上报externally-scoped-variable

getAllowedGlobalValue内部先检查overrideGlobals,再检查configuredGlobals(由globals包按ecmaVersion提供的 ES 全局变量与languageOptions.globals合并而成),最后用sourceCode.isGlobalReference(identifier)确保只有真正的全局引用才被放行——这正是“模块变量永远无法通过 globals 配置洗白”的机制来源。

嵌套作用域的问题收集

getFunctionContextProblems以递归方式遍历函数体 AST:凡遇到ThisExpressionSuper节点即上报对应消息;遇到非箭头函数的嵌套函数、类声明则视情况终止(普通函数与类拥有自己的this/super上下文),但对箭头函数(词法继承this)和类表达式会继续深入。类表达式的superClass与计算属性名也在检查范围内。

边界情况与测试佐证

规则配套了 815 行的测试文件 test/isolated-functions.js,其中值得注意的行为边界包括:

  • 泛化函数名不隔离memoizeserializeisolate默认不触发;
  • pageevaluate不启用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.executepage.evaluateexecuteScript识别,零配置即可获得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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询