From 6715b877759d35ba6e970781edddc8b312b229b2 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 2 Jul 2025 17:21:17 -0400 Subject: [PATCH 01/29] New rule: `isolated-functions` --- configs/recommended.js | 1 + docs/rules/isolated-functions.md | 216 +++++++++++++++++++++++++++++++ readme.md | 1 + rules/isolated-functions.js | 172 ++++++++++++++++++++++++ test/isolated-functions.mjs | 193 +++++++++++++++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 docs/rules/isolated-functions.md create mode 100644 rules/isolated-functions.js create mode 100644 test/isolated-functions.mjs diff --git a/configs/recommended.js b/configs/recommended.js index d8ee1eb42a..3ddac40955 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -22,6 +22,7 @@ module.exports = { 'unicorn/expiring-todo-comments': 'error', 'unicorn/explicit-length-check': 'error', 'unicorn/filename-case': 'error', + 'unicorn/isolated-functions': 'error', 'unicorn/import-style': 'error', 'unicorn/new-for-builtins': 'error', 'unicorn/no-abusive-eslint-disable': 'error', diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md new file mode 100644 index 0000000000..83f74800ec --- /dev/null +++ b/docs/rules/isolated-functions.md @@ -0,0 +1,216 @@ +# Prevent usage of variables from outside the scope of isolated functions + +πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs). + + + + +Some functions need to be isolated from their surrounding scope due to execution context constraints. For example, functions passed to `makeSynchronous()` are executed in a subprocess and cannot access variables from outside their scope. This rule helps identify when functions are using external variables that may cause runtime errors. + +Common scenarios where functions must be isolated: +- Functions passed to `makeSynchronous()` (executed in subprocess) +- Functions that will be serialized via `Function.prototype.toString()` +- Server actions or other remote execution contexts +- Functions with specific JSDoc annotations + +## Fail + +```js +import makeSynchronous from 'make-synchronous'; + +export const fetchSync = () => { + const url = 'https://example.com'; + const getText = makeSynchronous(async () => { + const res = await fetch(url); // ❌ 'url' is not defined in isolated function scope + return res.text(); + }); + console.log(getText()); +}; +``` + +```js +const foo = 'hi'; + +/** @isolated */ +function abc() { + return foo.slice(); // ❌ 'foo' is not defined in isolated function scope +} +``` + +```js +const foo = 'hi'; + +/** @isolated */ +const abc = () => foo.slice(); // ❌ 'foo' is not defined in isolated function scope +``` + +## Pass + +```js +import makeSynchronous from 'make-synchronous'; + +export const fetchSync = () => { + const getText = makeSynchronous(async () => { + const url = 'https://example.com'; // βœ… Variable defined within function scope + const res = await fetch(url); + return res.text(); + }); + console.log(getText()); +}; +``` + +```js +import makeSynchronous from 'make-synchronous'; + +export const fetchSync = () => { + const getText = makeSynchronous(async (url) => { // βœ… Variable passed as parameter + const res = await fetch(url); + return res.text(); + }); + console.log(getText('https://example.com')); +}; +``` + +```js +/** @isolated */ +function abc() { + const foo = 'hi'; // βœ… Variable defined within function scope + return foo.slice(); +} +``` + +## Options + +Type: `object` + +### functions + +Type: `string[]`\ +Default: `['makeSynchronous']` + +Array of function names that create isolated execution contexts. Functions passed as arguments to these functions will be considered isolated. + +### selectors + +Type: `string[]`\ +Default: `[]` + +Array of [ESLint selectors](https://eslint.org/docs/developer-guide/selectors) to identify isolated functions. Useful for custom naming conventions or framework-specific patterns. + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]'] + } + ] +} +``` + +### comments + +Type: `string[]`\ +Default: `['@isolated']` + +Array of comment strings that mark functions as isolated. Functions with JSDoc comments containing these strings will be considered isolated. + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + comments: ['@isolated', '@remote'] + } + ] +} +``` + +### globals + +Type: `boolean | string[]`\ +Default: `false` + +Controls how global variables are handled: + +- `false` (default): Global variables are not allowed in isolated functions +- `true`: All globals from ESLint's language options are allowed +- `string[]`: Only the specified global variable names are allowed + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + globals: ['console', 'fetch'] // Only allow these globals + } + ] +} +``` + +## Examples + +### Custom function names + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + functions: ['makeSynchronous', 'createWorker', 'serializeFunction'] + } + ] +} +``` + +### Lambda function naming convention + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]'] + } + ] +} +``` + +```js +const foo = 'hi'; + +function lambdaHandlerFoo() { // ❌ Will be flagged as isolated + return foo.slice(); +} + +function someOtherFunction() { // βœ… Not flagged + return foo.slice(); +} + +createLambda({ + name: 'fooLambda', + code: lambdaHandlerFoo.toString(), // Function will be serialized +}); +``` + +### Allowing specific globals + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + globals: ['console', 'fetch', 'URL'] + } + ] +} +``` + +```js +makeSynchronous(async () => { + console.log('Starting...'); // βœ… Allowed global + const response = await fetch('https://api.example.com'); // βœ… Allowed global + const url = new URL(response.url); // βœ… Allowed global + return response.text(); +}); +``` \ No newline at end of file diff --git a/readme.md b/readme.md index e223570eac..8c7000e40b 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c | [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. | βœ… | πŸ”§ | πŸ’‘ | | [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. | βœ… | | | | [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | βœ… | | | +| [isolated-functions](docs/rules/isolated-functions.md) | Prevent usage of variables from outside the scope of isolated functions. | βœ… | | | | [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | βœ… | πŸ”§ | | | [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | βœ… | | | | [no-array-callback-reference](docs/rules/no-array-callback-reference.md) | Prevent passing a function reference directly to iterator methods. | βœ… | | πŸ’‘ | diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js new file mode 100644 index 0000000000..6b30a7fd71 --- /dev/null +++ b/rules/isolated-functions.js @@ -0,0 +1,172 @@ +'use strict'; +const esquery = require('esquery'); +const functionTypes = require('./ast/function-types.js'); + +const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable'; +const messages = { + [MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE]: 'Variable {{name}} not defined in scope of isolated function. Function is isolated because: {{reason}}.', +}; + +const parsedEsquerySelectors = new Map(); +const parseEsquerySelector = selector => { + if (!parsedEsquerySelectors.has(selector)) { + parsedEsquerySelectors.set(selector, esquery.parse(selector)); + } + + return parsedEsquerySelectors.get(selector); +}; + +/** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ +const defaultOptions = { + functions: ['makeSynchronous'], + selectors: [], + comments: ['@isolated'], + globals: false, +}; + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + const {sourceCode} = context; + /** @type {typeof defaultOptions} */ + const userOptions = context.options[0]; + const options = { + ...defaultOptions, + ...userOptions, + }; + + options.comments = options.comments.map(comment => comment.toLowerCase()); + const allowedGlobals = options.globals === true + ? Object.keys(context.languageOptions.globals) + : (Array.isArray(options.globals) + ? options.globals + : []); + + /** @param {import('estree').Node} node */ + const checkForExternallyScopedVariables = node => { + const reason = reasaonForBeingIsolatedFunction(node); + if (!reason) { + return; + } + + const nodeScope = sourceCode.getScope(node); + + for (const ref of nodeScope.through) { + const {identifier} = ref; + + if (allowedGlobals.includes(identifier.name)) { + continue; + } + + // If (!options.considerTypeOf && hasTypeOfOperator(identifier)) { + // return; + // } + + context.report({ + node: identifier, + messageId: MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE, + // Message: `Variable ${identifier.name} is used from outside the scope of an isolated function. Function is isolated because: ${reason}.`, + data: {name: identifier.name, reason}, + }); + } + }; + + /** @param {import('estree').Node & {parent?: import('estree').Node}} node */ + const reasaonForBeingIsolatedFunction = node => { + if (options.comments.length > 0) { + let previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); + let commentableNode = node; + while (previousToken?.type !== 'Block' && (commentableNode.parent.type === 'VariableDeclarator' || commentableNode.parent.type === 'VariableDeclaration')) { + // Search up to find jsdoc comments on the parent declaration `/** @isolated */ const foo = () => abc` + commentableNode = commentableNode.parent; + previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true}); + } + + if (previousToken?.type === 'Block') { + const previousComment = previousToken.value.trim().toLowerCase(); + const match = options.comments.find(comment => previousComment.includes(comment)); + if (match) { + return `follows comment containing ${JSON.stringify(match)}`; + } + } + } + + if ( + options.functions.length > 0 + && node.parent.type === 'CallExpression' + && node.parent.arguments.includes(node) + && node.parent.callee.type === 'Identifier' + && options.functions.includes(node.parent.callee.name) + ) { + return `callee of function named ${JSON.stringify(node.parent.callee.name)}`; + } + + if (options.selectors.length > 0) { + const ancestors = sourceCode.getAncestors(node).reverse(); + const matchedSelector = options.selectors.find(selector => esquery.matches(node, parseEsquerySelector(selector), ancestors)); + if (matchedSelector) { + return `matches selector ${JSON.stringify(matchedSelector)}`; + } + } + + return undefined; + }; + + return Object.fromEntries(functionTypes.map(type => [ + `${type}:exit`, + checkForExternallyScopedVariables, + ])); +}; + +/** @type {import('json-schema').JSONSchema7[]} */ +const schema = [ + { + type: 'object', + additionalProperties: false, + properties: { + tags: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + globals: { + oneOf: [{type: 'boolean'}, {type: 'array', items: {type: 'string'}}], + }, + functions: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + selectors: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + comments: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + }, + }, +]; + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + create, + meta: { + type: 'problem', + docs: { + description: 'Prevent usage of variables from outside the scope of isolated functions.', + }, + schema, + messages, + }, +}; diff --git a/test/isolated-functions.mjs b/test/isolated-functions.mjs new file mode 100644 index 0000000000..308a93c89a --- /dev/null +++ b/test/isolated-functions.mjs @@ -0,0 +1,193 @@ +import stripIndent from 'strip-indent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +const error = data => ({messageId: 'externally-scoped-variable', data}); +const fooInMakeSynchronousError = error({name: 'foo', reason: 'callee of function named "makeSynchronous"'}); + +test({ + /** @type {import('eslint').RuleTester.InvalidTestCase[]} */ + invalid: [ + { + name: 'out of scope variable under makeSynchronous (arrow function)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(() => foo.slice()); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'out of scope variable under makeSynchronous (async arrow function)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(async () => foo.slice()); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'out of scope variable under makeSynchronous (function expression)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(function () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'out of scope variable under makeSynchronous (async function expression)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(async function () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'out of scope variable under makeSynchronous (named function expression)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(function abc () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'out of scope variable under makeSynchronous (named async function expression)', + code: stripIndent(` + const foo = 'hi'; + makeSynchronous(async function abc () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: '@isolated comment on function declaration', + code: stripIndent(` + const foo = 'hi'; + /** @isolated */ + function abc () { + return foo.slice(); + } + `), + errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + }, + { + name: '@isolated comment on arrow function', + code: stripIndent(` + const foo = 'hi'; + /** @isolated */ + const abc = () => foo.slice(); + `), + errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + }, + { + name: 'global variables not allowed by default', + globals: {foo: true}, + code: stripIndent(` + makeSynchronous(function () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'global variables can be explicitly disallowed', + globals: {foo: true}, + options: [{globals: false}], + code: stripIndent(` + makeSynchronous(function () { + return foo.slice(); + }); + `), + errors: [fooInMakeSynchronousError], + }, + { + name: 'make a function isolated by a selector', + // In this case, we're imagining some naming convention for lambda functions that will be created via `fn.toString()` + options: [{selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]']}], + code: stripIndent(` + const foo = 'hi'; + + function lambdaHandlerFoo() { + return foo.slice(); + } + + function someOtherFunction() { + return foo.slice(); + } + + createLambda({ + name: 'fooLambda', + code: lambdaHandlerFoo.toString(), + }); + `), + errors: [ + error({name: 'foo', reason: 'matches selector "FunctionDeclaration[id.name=/lambdaHandler.*/]"'}), + ], + }, + ], + /** @type {import('eslint').RuleTester.ValidTestCase[]} */ + valid: [ + { + name: 'variable defined in scope of isolated function', + code: stripIndent(` + makeSynchronous(() => { + const foo = 'hi'; + return foo.slice(); + }); + `), + }, + { + name: 'variable defined as parameter of isolated function', + code: stripIndent(` + makeSynchronous(foo => { + return foo.slice(); + }); + `), + }, + { + name: 'inner function can access outer function parameters', + code: stripIndent(` + /** @isolated */ + function abc () { + const foo = 'hi'; + const slice = () => foo.slice(); + return slice(); + } + `), + }, + { + name: 'variable defined as parameter of isolated function (async)', + code: stripIndent(` + makeSynchronous(async function (foo) { + return foo.slice(); + }); + `), + }, + { + name: 'can allow global variables from language options', + globals: {foo: true}, + options: [{globals: true}], + code: stripIndent(` + makeSynchronous(function () { + return foo.slice(); + }); + `), + }, + { + name: 'allow global variables separate from language options', + globals: {abc: true}, + options: [{globals: ['foo']}], + code: stripIndent(` + makeSynchronous(function () { + return foo.slice(); + }); + `), + }, + ], +}); From 27ac79cb776b115ab413d7dc9da6d6ebecf30148 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 2 Jul 2025 17:47:15 -0400 Subject: [PATCH 02/29] esm is cool --- docs/rules/isolated-functions.md | 2 +- readme.md | 1 + rules/index.js | 1 + rules/isolated-functions.js | 13 +++---------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 83f74800ec..c521e8dd15 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -1,6 +1,6 @@ # Prevent usage of variables from outside the scope of isolated functions -πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs). +πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). diff --git a/readme.md b/readme.md index 13e74df1e4..66063e4f58 100644 --- a/readme.md +++ b/readme.md @@ -72,6 +72,7 @@ export default [ | [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. | βœ… | πŸ”§ | πŸ’‘ | | [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. | βœ… | | | | [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | βœ… | | | +| [isolated-functions](docs/rules/isolated-functions.md) | Prevent usage of variables from outside the scope of isolated functions. | βœ… | | | | [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | βœ… | πŸ”§ | πŸ’‘ | | [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | βœ… | | | | [no-accessor-recursion](docs/rules/no-accessor-recursion.md) | Disallow recursive access to `this` within getters and setters. | βœ… | | | diff --git a/rules/index.js b/rules/index.js index b0a372a5aa..8f5fcd91eb 100644 --- a/rules/index.js +++ b/rules/index.js @@ -16,6 +16,7 @@ export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js'; export {default as 'explicit-length-check'} from './explicit-length-check.js'; export {default as 'filename-case'} from './filename-case.js'; export {default as 'import-style'} from './import-style.js'; +export {default as 'isolated-functions'} from './isolated-functions.js'; export {default as 'new-for-builtins'} from './new-for-builtins.js'; export {default as 'no-abusive-eslint-disable'} from './no-abusive-eslint-disable.js'; export {default as 'no-accessor-recursion'} from './no-accessor-recursion.js'; diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index f97f2abd25..0a0567581d 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -1,6 +1,6 @@ 'use strict'; -const esquery = require('esquery'); -const functionTypes = require('./ast/function-types.js'); +import esquery from 'esquery'; +import functionTypes from './ast/function-types.js'; const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable'; const messages = { @@ -123,13 +123,6 @@ const schema = [ type: 'object', additionalProperties: false, properties: { - tags: { - type: 'array', - uniqueItems: true, - items: { - type: 'string', - }, - }, globals: { oneOf: [{type: 'boolean'}, {type: 'array', items: {type: 'string'}}], }, @@ -159,7 +152,7 @@ const schema = [ ]; /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +export default { create, meta: { type: 'problem', From 4fbb759e7fc7e6ee436b7ea0f921a275eabe40da Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Wed, 2 Jul 2025 17:52:20 -0400 Subject: [PATCH 03/29] lint --- rules/isolated-functions.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 0a0567581d..b8b2f46563 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -50,21 +50,18 @@ const create = context => { const nodeScope = sourceCode.getScope(node); - for (const ref of nodeScope.through) { - const {identifier} = ref; + for (const reference of nodeScope.through) { + const {identifier} = reference; if (allowedGlobals.includes(identifier.name)) { continue; } - // If (!options.considerTypeOf && hasTypeOfOperator(identifier)) { - // return; - // } + // Could consider checking for typeof operator here, like in no-undef? context.report({ node: identifier, messageId: MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE, - // Message: `Variable ${identifier.name} is used from outside the scope of an isolated function. Function is isolated because: ${reason}.`, data: {name: identifier.name, reason}, }); } @@ -108,7 +105,7 @@ const create = context => { } } - return undefined; + }; return Object.fromEntries(functionTypes.map(type => [ From 55036f155a4f2d4156d9f114e1a043ab34199ca7 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 3 Jul 2025 00:29:24 -0400 Subject: [PATCH 04/29] exts --- docs/rules/isolated-functions.md | 3 +- rules/isolated-functions.js | 29 +++++++++---------- ...ed-functions.mjs => isolated-functions.js} | 19 ++++++++---- 3 files changed, 29 insertions(+), 22 deletions(-) rename test/{isolated-functions.mjs => isolated-functions.js} (91%) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index c521e8dd15..c72038663c 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -8,6 +8,7 @@ Some functions need to be isolated from their surrounding scope due to execution context constraints. For example, functions passed to `makeSynchronous()` are executed in a subprocess and cannot access variables from outside their scope. This rule helps identify when functions are using external variables that may cause runtime errors. Common scenarios where functions must be isolated: + - Functions passed to `makeSynchronous()` (executed in subprocess) - Functions that will be serialized via `Function.prototype.toString()` - Server actions or other remote execution contexts @@ -213,4 +214,4 @@ makeSynchronous(async () => { const url = new URL(response.url); // βœ… Allowed global return response.text(); }); -``` \ No newline at end of file +``` diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index b8b2f46563..6811ef5926 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -16,21 +16,17 @@ const parseEsquerySelector = selector => { return parsedEsquerySelectors.get(selector); }; -/** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ -const defaultOptions = { - functions: ['makeSynchronous'], - selectors: [], - comments: ['@isolated'], - globals: false, -}; - /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; - /** @type {typeof defaultOptions} */ + /** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ const userOptions = context.options[0]; + /** @type {typeof userOptions} */ const options = { - ...defaultOptions, + functions: ['makeSynchronous'], + selectors: [], + comments: ['@isolated'], + globals: false, ...userOptions, }; @@ -43,7 +39,7 @@ const create = context => { /** @param {import('estree').Node} node */ const checkForExternallyScopedVariables = node => { - const reason = reasaonForBeingIsolatedFunction(node); + const reason = reasonForBeingIsolatedFunction(node); if (!reason) { return; } @@ -68,17 +64,20 @@ const create = context => { }; /** @param {import('estree').Node & {parent?: import('estree').Node}} node */ - const reasaonForBeingIsolatedFunction = node => { + const reasonForBeingIsolatedFunction = node => { if (options.comments.length > 0) { let previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); let commentableNode = node; - while (previousToken?.type !== 'Block' && (commentableNode.parent.type === 'VariableDeclarator' || commentableNode.parent.type === 'VariableDeclaration')) { + while ( + (previousToken?.type !== 'Block' && previousToken?.type !== 'Line') + && (commentableNode.parent.type === 'VariableDeclarator' || commentableNode.parent.type === 'VariableDeclaration') + ) { // Search up to find jsdoc comments on the parent declaration `/** @isolated */ const foo = () => abc` commentableNode = commentableNode.parent; previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true}); } - if (previousToken?.type === 'Block') { + if (previousToken?.type === 'Block' || previousToken?.type === 'Line') { const previousComment = previousToken.value.trim().toLowerCase(); const match = options.comments.find(comment => previousComment.includes(comment)); if (match) { @@ -104,8 +103,6 @@ const create = context => { return `matches selector ${JSON.stringify(matchedSelector)}`; } } - - }; return Object.fromEntries(functionTypes.map(type => [ diff --git a/test/isolated-functions.mjs b/test/isolated-functions.js similarity index 91% rename from test/isolated-functions.mjs rename to test/isolated-functions.js index 308a93c89a..718bb02d7c 100644 --- a/test/isolated-functions.mjs +++ b/test/isolated-functions.js @@ -1,5 +1,5 @@ import stripIndent from 'strip-indent'; -import {getTester} from './utils/test.mjs'; +import {getTester} from './utils/test.js'; const {test} = getTester(import.meta); @@ -85,9 +85,18 @@ test({ `), errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], }, + { + name: '@isolated inline comment', + code: stripIndent(` + const foo = 'hi'; + // @isolated + const abc = () => foo.slice(); + `), + errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + }, { name: 'global variables not allowed by default', - globals: {foo: true}, + languageOptions: {globals: {foo: true}}, code: stripIndent(` makeSynchronous(function () { return foo.slice(); @@ -97,7 +106,7 @@ test({ }, { name: 'global variables can be explicitly disallowed', - globals: {foo: true}, + languageOptions: {globals: {foo: true}}, options: [{globals: false}], code: stripIndent(` makeSynchronous(function () { @@ -171,7 +180,7 @@ test({ }, { name: 'can allow global variables from language options', - globals: {foo: true}, + languageOptions: {globals: {foo: true}}, options: [{globals: true}], code: stripIndent(` makeSynchronous(function () { @@ -181,7 +190,7 @@ test({ }, { name: 'allow global variables separate from language options', - globals: {abc: true}, + languageOptions: {globals: {abc: true}}, options: [{globals: ['foo']}], code: stripIndent(` makeSynchronous(function () { From ea09c9ec8cdda7bfca5919b06a0f872e602c0548 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 3 Jul 2025 08:40:02 -0400 Subject: [PATCH 05/29] allow globals by default --- docs/rules/isolated-functions.md | 21 +++++++++++++++++--- rules/isolated-functions.js | 33 ++++++++++++++++++-------------- test/isolated-functions.js | 21 ++++++++++---------- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index c72038663c..20d1d5af0a 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -14,6 +14,8 @@ Common scenarios where functions must be isolated: - Server actions or other remote execution contexts - Functions with specific JSDoc annotations +By default, this rule allows global variables (like `console`, `fetch`, etc.) in isolated functions, but prevents usage of variables from the surrounding scope. + ## Fail ```js @@ -80,6 +82,19 @@ function abc() { } ``` +```js +import makeSynchronous from 'make-synchronous'; + +export const fetchSync = () => { + const getText = makeSynchronous(async () => { + console.log('Starting...'); // βœ… Global variables are allowed by default + const res = await fetch('https://example.com'); // βœ… Global variables are allowed by default + return res.text(); + }); + console.log(getText()); +}; +``` + ## Options Type: `object` @@ -130,12 +145,12 @@ Array of comment strings that mark functions as isolated. Functions with JSDoc c ### globals Type: `boolean | string[]`\ -Default: `false` +Default: `true` Controls how global variables are handled: -- `false` (default): Global variables are not allowed in isolated functions -- `true`: All globals from ESLint's language options are allowed +- `false`: Global variables are not allowed in isolated functions +- `true` (default): All globals from ESLint's language options are allowed - `string[]`: Only the specified global variable names are allowed ```js diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 6811ef5926..50945edaff 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -1,4 +1,3 @@ -'use strict'; import esquery from 'esquery'; import functionTypes from './ast/function-types.js'; @@ -16,26 +15,31 @@ const parseEsquerySelector = selector => { return parsedEsquerySelectors.get(selector); }; +/** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ +const defaultOptions = { + functions: ['makeSynchronous'], + selectors: [], + comments: ['@isolated'], + globals: true, +}; + /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; - /** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ - const userOptions = context.options[0]; - /** @type {typeof userOptions} */ + /** @type {typeof defaultOptions} */ const options = { - functions: ['makeSynchronous'], - selectors: [], - comments: ['@isolated'], - globals: false, - ...userOptions, + ...defaultOptions, + ...context.options[0], }; options.comments = options.comments.map(comment => comment.toLowerCase()); - const allowedGlobals = options.globals === true - ? Object.keys(context.languageOptions.globals) - : (Array.isArray(options.globals) - ? options.globals - : []); + /** @type {string[]} */ + let allowedGlobals = []; + if (options.globals === true) { + allowedGlobals = Object.keys(context.languageOptions.globals); + } else if (Array.isArray(options.globals)) { + allowedGlobals = options.globals; + } /** @param {import('estree').Node} node */ const checkForExternallyScopedVariables = node => { @@ -155,6 +159,7 @@ export default { recommended: true, }, schema, + defaultOptions: [defaultOptions], messages, }, }; diff --git a/test/isolated-functions.js b/test/isolated-functions.js index 718bb02d7c..ba8c5710b6 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -94,16 +94,6 @@ test({ `), errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], }, - { - name: 'global variables not allowed by default', - languageOptions: {globals: {foo: true}}, - code: stripIndent(` - makeSynchronous(function () { - return foo.slice(); - }); - `), - errors: [fooInMakeSynchronousError], - }, { name: 'global variables can be explicitly disallowed', languageOptions: {globals: {foo: true}}, @@ -179,7 +169,16 @@ test({ `), }, { - name: 'can allow global variables from language options', + name: 'can implicitly allow global variables from language options', + languageOptions: {globals: {foo: true}}, + code: stripIndent(` + makeSynchronous(function () { + return foo.slice(); + }); + `), + }, + { + name: 'can explicitly allow global variables from language options', languageOptions: {globals: {foo: true}}, options: [{globals: true}], code: stripIndent(` From 26e70a526e7b556bb2098c9bbee1a3e6227a5a66 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 07:03:28 -0400 Subject: [PATCH 06/29] set (updated 07:04) --- rules/isolated-functions.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 50945edaff..fa74d6258c 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -33,13 +33,9 @@ const create = context => { }; options.comments = options.comments.map(comment => comment.toLowerCase()); - /** @type {string[]} */ - let allowedGlobals = []; - if (options.globals === true) { - allowedGlobals = Object.keys(context.languageOptions.globals); - } else if (Array.isArray(options.globals)) { - allowedGlobals = options.globals; - } + const allowedGlobals = options.globals === true + ? new Set(Object.keys(context.languageOptions.globals)) + : new Set(options.globals || []); /** @param {import('estree').Node} node */ const checkForExternallyScopedVariables = node => { @@ -53,7 +49,7 @@ const create = context => { for (const reference of nodeScope.through) { const {identifier} = reference; - if (allowedGlobals.includes(identifier.name)) { + if (allowedGlobals.has(identifier.name)) { continue; } From a52c3ee2bc93a6dc2ea5e9ddbcfd8d4f7a96b2d6 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 07:17:34 -0400 Subject: [PATCH 07/29] rm .reverse() --- rules/isolated-functions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index fa74d6258c..7cbfce1a12 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -97,7 +97,7 @@ const create = context => { } if (options.selectors.length > 0) { - const ancestors = sourceCode.getAncestors(node).reverse(); + const ancestors = sourceCode.getAncestors(node); const matchedSelector = options.selectors.find(selector => esquery.matches(node, parseEsquerySelector(selector), ancestors)); if (matchedSelector) { return `matches selector ${JSON.stringify(matchedSelector)}`; From 60f072de33dd2b5940cce6988286ca86a641d4aa Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 7 Jul 2025 13:41:30 +0200 Subject: [PATCH 08/29] Update isolated-functions.md --- docs/rules/isolated-functions.md | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 20d1d5af0a..1bb3c7421d 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -5,11 +5,11 @@ -Some functions need to be isolated from their surrounding scope due to execution context constraints. For example, functions passed to `makeSynchronous()` are executed in a subprocess and cannot access variables from outside their scope. This rule helps identify when functions are using external variables that may cause runtime errors. +Some functions need to be isolated from their surrounding scope due to execution context constraints. For example, functions passed to [`makeSynchronous()`](https://github.com/sindresorhus/make-synchronous) are executed in a worker or subprocess and cannot access variables from outside their scope. This rule helps identify when functions are using external variables that may cause runtime errors. Common scenarios where functions must be isolated: -- Functions passed to `makeSynchronous()` (executed in subprocess) +- Functions passed to `makeSynchronous()` (executed in worker) - Functions that will be serialized via `Function.prototype.toString()` - Server actions or other remote execution contexts - Functions with specific JSDoc annotations @@ -23,10 +23,12 @@ import makeSynchronous from 'make-synchronous'; export const fetchSync = () => { const url = 'https://example.com'; + const getText = makeSynchronous(async () => { const res = await fetch(url); // ❌ 'url' is not defined in isolated function scope return res.text(); }); + console.log(getText()); }; ``` @@ -58,6 +60,7 @@ export const fetchSync = () => { const res = await fetch(url); return res.text(); }); + console.log(getText()); }; ``` @@ -70,6 +73,7 @@ export const fetchSync = () => { const res = await fetch(url); return res.text(); }); + console.log(getText('https://example.com')); }; ``` @@ -91,6 +95,7 @@ export const fetchSync = () => { const res = await fetch('https://example.com'); // βœ… Global variables are allowed by default return res.text(); }); + console.log(getText()); }; ``` @@ -118,7 +123,9 @@ Array of [ESLint selectors](https://eslint.org/docs/developer-guide/selectors) t 'unicorn/isolated-functions': [ 'error', { - selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]'] + selectors: [ + 'FunctionDeclaration[id.name=/lambdaHandler.*/]' + ] } ] } @@ -136,7 +143,10 @@ Array of comment strings that mark functions as isolated. Functions with JSDoc c 'unicorn/isolated-functions': [ 'error', { - comments: ['@isolated', '@remote'] + comments: [ + '@isolated', + '@remote' + ] } ] } @@ -173,7 +183,11 @@ Controls how global variables are handled: 'unicorn/isolated-functions': [ 'error', { - functions: ['makeSynchronous', 'createWorker', 'serializeFunction'] + functions: [ + 'makeSynchronous', + 'createWorker', + 'serializeFunction' + ] } ] } @@ -186,7 +200,9 @@ Controls how global variables are handled: 'unicorn/isolated-functions': [ 'error', { - selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]'] + selectors: [ + 'FunctionDeclaration[id.name=/lambdaHandler.*/]' + ] } ] } @@ -216,7 +232,11 @@ createLambda({ 'unicorn/isolated-functions': [ 'error', { - globals: ['console', 'fetch', 'URL'] + globals: [ + 'console', + 'fetch', + 'URL' + ] } ] } From 7786865af66146664a51791d05c69fbdf118e26c Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 7 Jul 2025 13:48:57 +0200 Subject: [PATCH 09/29] Update isolated-functions.js --- rules/isolated-functions.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 7cbfce1a12..53262f9009 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -33,6 +33,7 @@ const create = context => { }; options.comments = options.comments.map(comment => comment.toLowerCase()); + const allowedGlobals = options.globals === true ? new Set(Object.keys(context.languageOptions.globals)) : new Set(options.globals || []); @@ -118,7 +119,17 @@ const schema = [ additionalProperties: false, properties: { globals: { - oneOf: [{type: 'boolean'}, {type: 'array', items: {type: 'string'}}], + oneOf: [ + { + type: 'boolean', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], }, functions: { type: 'array', From b3649e9a3ce922a373641163817649b189b36da4 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 07:52:03 -0400 Subject: [PATCH 10/29] docs: group fail/pass examples closer --- docs/rules/isolated-functions.md | 63 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 1bb3c7421d..585bcfa1cc 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -16,7 +16,7 @@ Common scenarios where functions must be isolated: By default, this rule allows global variables (like `console`, `fetch`, etc.) in isolated functions, but prevents usage of variables from the surrounding scope. -## Fail +## Examples ```js import makeSynchronous from 'make-synchronous'; @@ -31,45 +31,36 @@ export const fetchSync = () => { console.log(getText()); }; -``` -```js -const foo = 'hi'; - -/** @isolated */ -function abc() { - return foo.slice(); // ❌ 'foo' is not defined in isolated function scope -} -``` - -```js -const foo = 'hi'; +// βœ… +export const fetchSync = () => { + const getText = makeSynchronous(async () => { + const url = 'https://example.com'; // Variable defined within function scope + const res = await fetch(url); + return res.text(); + }); -/** @isolated */ -const abc = () => foo.slice(); // ❌ 'foo' is not defined in isolated function scope + console.log(getText()); +}; ``` -## Pass - ```js import makeSynchronous from 'make-synchronous'; export const fetchSync = () => { + const url = 'https://example.com'; + const getText = makeSynchronous(async () => { - const url = 'https://example.com'; // βœ… Variable defined within function scope - const res = await fetch(url); + const res = await fetch(url); // ❌ 'url' is not defined in isolated function scope return res.text(); }); console.log(getText()); }; -``` - -```js -import makeSynchronous from 'make-synchronous'; +// βœ… export const fetchSync = () => { - const getText = makeSynchronous(async (url) => { // βœ… Variable passed as parameter + const getText = makeSynchronous(async (url) => { // Variable passed as parameter const res = await fetch(url); return res.text(); }); @@ -79,13 +70,35 @@ export const fetchSync = () => { ``` ```js +const foo = 'hi'; + +/** @isolated */ +function abc() { + return foo.slice(); // ❌ 'foo' is not defined in isolated function scope +} + +// βœ… /** @isolated */ function abc() { - const foo = 'hi'; // βœ… Variable defined within function scope + const foo = 'hi'; // Variable defined within function scope return foo.slice(); } ``` +```js +const foo = 'hi'; + +/** @isolated */ +const abc = () => foo.slice(); // ❌ 'foo' is not defined in isolated function scope + +// βœ… +/** @isolated */ +const abc = () => { + const foo = 'hi'; // Variable defined within function scope + return foo.slice(); +}; +``` + ```js import makeSynchronous from 'make-synchronous'; From 9c92506ac2aac67d6af6444fe2449eac740af8f7 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 07:54:43 -0400 Subject: [PATCH 11/29] docs: block fn --- docs/rules/isolated-functions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 585bcfa1cc..a08e9c8bdd 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -89,7 +89,9 @@ function abc() { const foo = 'hi'; /** @isolated */ -const abc = () => foo.slice(); // ❌ 'foo' is not defined in isolated function scope +const abc = () => { + return foo.slice(); // ❌ 'foo' is not defined in isolated function scope +}; // βœ… /** @isolated */ From 30eaa9d7683d542523e76ad385fb4f6ce25a0a3a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 08:01:45 -0400 Subject: [PATCH 12/29] more better docs (updated 08:02) --- docs/rules/isolated-functions.md | 61 ++++++++------------------------ 1 file changed, 14 insertions(+), 47 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index a08e9c8bdd..28e3280249 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -32,7 +32,7 @@ export const fetchSync = () => { console.log(getText()); }; -// βœ… +// βœ… Define all variables within isolated function's scope export const fetchSync = () => { const getText = makeSynchronous(async () => { const url = 'https://example.com'; // Variable defined within function scope @@ -42,23 +42,8 @@ export const fetchSync = () => { console.log(getText()); }; -``` - -```js -import makeSynchronous from 'make-synchronous'; -export const fetchSync = () => { - const url = 'https://example.com'; - - const getText = makeSynchronous(async () => { - const res = await fetch(url); // ❌ 'url' is not defined in isolated function scope - return res.text(); - }); - - console.log(getText()); -}; - -// βœ… +// βœ… Alternative: Pass as parameter export const fetchSync = () => { const getText = makeSynchronous(async (url) => { // Variable passed as parameter const res = await fetch(url); @@ -68,6 +53,7 @@ export const fetchSync = () => { console.log(getText('https://example.com')); }; ``` +``` ```js const foo = 'hi'; @@ -85,36 +71,6 @@ function abc() { } ``` -```js -const foo = 'hi'; - -/** @isolated */ -const abc = () => { - return foo.slice(); // ❌ 'foo' is not defined in isolated function scope -}; - -// βœ… -/** @isolated */ -const abc = () => { - const foo = 'hi'; // Variable defined within function scope - return foo.slice(); -}; -``` - -```js -import makeSynchronous from 'make-synchronous'; - -export const fetchSync = () => { - const getText = makeSynchronous(async () => { - console.log('Starting...'); // βœ… Global variables are allowed by default - const res = await fetch('https://example.com'); // βœ… Global variables are allowed by default - return res.text(); - }); - - console.log(getText()); -}; -``` - ## Options Type: `object` @@ -258,10 +214,21 @@ createLambda({ ``` ```js +// βœ… All globals used are explicitly allowed makeSynchronous(async () => { console.log('Starting...'); // βœ… Allowed global const response = await fetch('https://api.example.com'); // βœ… Allowed global const url = new URL(response.url); // βœ… Allowed global return response.text(); }); + +makeSynchronous(async () => { + const response = await fetch('https://api.example.com', { + headers: { + 'Authorization': `Bearer ${process.env.API_TOKEN}` // ❌ 'process' is not in allowed globals + } + }); + const url = new URL(response.url); + return response.text(); +}); ``` From f4b8b666ab8c879feacba30ed50328a6541d83dd Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 13:52:01 -0400 Subject: [PATCH 13/29] match builtin eslint globals behavior --- docs/rules/isolated-functions.md | 69 ++++++++++++++++++++++++++------ rules/isolated-functions.js | 37 +++++++++-------- test/isolated-functions.js | 43 ++++++++++++++------ 3 files changed, 104 insertions(+), 45 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 28e3280249..5bf3dc09a6 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -14,7 +14,7 @@ Common scenarios where functions must be isolated: - Server actions or other remote execution contexts - Functions with specific JSDoc annotations -By default, this rule allows global variables (like `console`, `fetch`, etc.) in isolated functions, but prevents usage of variables from the surrounding scope. +By default, this rule uses ESLint's language options globals and allows global variables (like `console`, `fetch`, etc.) in isolated functions, but prevents usage of variables from the surrounding scope. ## Examples @@ -125,21 +125,25 @@ Array of comment strings that mark functions as isolated. Functions with JSDoc c ### globals -Type: `boolean | string[]`\ -Default: `true` +Type: `object`\ +Default: `undefined` (uses ESLint's language options globals) -Controls how global variables are handled: +Controls how global variables are handled. When not specified, uses ESLint's language options globals. When specified as an object, each key is a global variable name and the value controls its behavior: -- `false`: Global variables are not allowed in isolated functions -- `true` (default): All globals from ESLint's language options are allowed -- `string[]`: Only the specified global variable names are allowed +- `'readonly'`: Global variable is allowed but cannot be written to (depreacted form `false` also accepted) +- `'writable'`: Global variable is allowed and can be read/written (deprecated forms `true` and `'writeable'` also accepted) +- `'off'`: Global variable is not allowed ```js { 'unicorn/isolated-functions': [ 'error', { - globals: ['console', 'fetch'] // Only allow these globals + globals: { + console: 'writable', // Allowed and writable + fetch: 'readonly', // Allowed but readonly + process: 'off' // Not allowed + } } ] } @@ -196,6 +200,39 @@ createLambda({ }); ``` +### Default behavior (using ESLint's language options) + +```js +// Uses ESLint's language options globals by default +makeSynchronous(async () => { + console.log('Starting...'); // βœ… Allowed if console is in language options + const response = await fetch('https://api.example.com'); // βœ… Allowed if fetch is in language options + return response.text(); +}); +``` + +### Disallowing all globals + +```js +{ + 'unicorn/isolated-functions': [ + 'error', + { + globals: {} // Empty object disallows all globals + } + ] +} +``` + +```js +// ❌ All globals are disallowed +makeSynchronous(async () => { + console.log('Starting...'); // ❌ 'console' is not allowed + const response = await fetch('https://api.example.com'); // ❌ 'fetch' is not allowed + return response.text(); +}); +``` + ### Allowing specific globals ```js @@ -203,11 +240,11 @@ createLambda({ 'unicorn/isolated-functions': [ 'error', { - globals: [ - 'console', - 'fetch', - 'URL' - ] + globals: { + console: 'writable', // Allowed and writable + fetch: 'readonly', // Allowed but readonly + URL: 'readonly' // Allowed but readonly + } } ] } @@ -231,4 +268,10 @@ makeSynchronous(async () => { const url = new URL(response.url); return response.text(); }); + +// ❌ Attempting to write to readonly global +makeSynchronous(async () => { + fetch = null; // ❌ 'fetch' is readonly + console.log('Starting...'); +}); ``` diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 53262f9009..8393a2215c 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -15,12 +15,11 @@ const parseEsquerySelector = selector => { return parsedEsquerySelectors.get(selector); }; -/** @type {{functions: string[], selectors: string[], comments: string[], globals: boolean | string[]}} */ +/** @type {{functions: string[], selectors: string[], comments: string[], globals?: import('eslint').Linter.Globals}} */ const defaultOptions = { functions: ['makeSynchronous'], selectors: [], comments: ['@isolated'], - globals: true, }; /** @param {import('eslint').Rule.RuleContext} context */ @@ -34,24 +33,32 @@ const create = context => { options.comments = options.comments.map(comment => comment.toLowerCase()); - const allowedGlobals = options.globals === true - ? new Set(Object.keys(context.languageOptions.globals)) - : new Set(options.globals || []); + const allowedGlobals = options.globals ?? context.languageOptions.globals; /** @param {import('estree').Node} node */ const checkForExternallyScopedVariables = node => { - const reason = reasonForBeingIsolatedFunction(node); + let reason = reasonForBeingIsolatedFunction(node); if (!reason) { return; } const nodeScope = sourceCode.getScope(node); + // `through`: "The array of references which could not be resolved in this scope" https://eslint.org/docs/latest/extend/scope-manager-interface#scope-interface for (const reference of nodeScope.through) { const {identifier} = reference; + if (identifier.name in allowedGlobals && allowedGlobals[identifier.name] !== 'off') { + if (reference.isReadOnly()) { + continue; + } + + const globalsValue = allowedGlobals[identifier.name]; + const isGlobalWritable = globalsValue === true || globalsValue === 'writable' || globalsValue === 'writeable'; + if (isGlobalWritable) { + continue; + } - if (allowedGlobals.has(identifier.name)) { - continue; + reason += ' (global variable is not writable)'; } // Could consider checking for typeof operator here, like in no-undef? @@ -119,17 +126,9 @@ const schema = [ additionalProperties: false, properties: { globals: { - oneOf: [ - { - type: 'boolean', - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], + additionalProperties: { + anyOf: [{type: 'boolean'}, {type: 'string', enum: ['readonly', 'writable', 'writeable', 'off']}], + }, }, functions: { type: 'array', diff --git a/test/isolated-functions.js b/test/isolated-functions.js index ba8c5710b6..71ff1b90ba 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -95,9 +95,9 @@ test({ errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], }, { - name: 'global variables can be explicitly disallowed', + name: 'all global variables can be explicitly disallowed', languageOptions: {globals: {foo: true}}, - options: [{globals: false}], + options: [{globals: {}}], code: stripIndent(` makeSynchronous(function () { return foo.slice(); @@ -105,6 +105,33 @@ test({ `), errors: [fooInMakeSynchronousError], }, + { + name: 'individual global variables can be explicitly disallowed', + options: [{globals: {URLSearchParams: 'readonly', URL: 'off'}}], + code: stripIndent(` + makeSynchronous(function () { + return new URL('https://example.com?') + new URLSearchParams({a: 'b'}).toString(); + }); + `), + errors: [error({name: 'URL', reason: 'callee of function named "makeSynchronous"'})], + }, + { + name: 'check globals writability', + code: stripIndent(` + makeSynchronous(function () { + location = new URL('https://example.com'); + process = {env: {}}; + process.env.FOO = 'bar'; + }); + `), + errors: [ + // Only one error, `location = new URL('https://example.com')` and `process.env.FOO = 'bar'` are fine, the problem is `process = {...}`. + error({ + name: 'process', + reason: 'callee of function named "makeSynchronous" (global variable is not writable)', + }), + ], + }, { name: 'make a function isolated by a selector', // In this case, we're imagining some naming convention for lambda functions that will be created via `fn.toString()` @@ -177,20 +204,10 @@ test({ }); `), }, - { - name: 'can explicitly allow global variables from language options', - languageOptions: {globals: {foo: true}}, - options: [{globals: true}], - code: stripIndent(` - makeSynchronous(function () { - return foo.slice(); - }); - `), - }, { name: 'allow global variables separate from language options', languageOptions: {globals: {abc: true}}, - options: [{globals: ['foo']}], + options: [{globals: {foo: true}}], code: stripIndent(` makeSynchronous(function () { return foo.slice(); From 543f7517f1cefd58e126f3cc71e4de4b4dfdde66 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Mon, 7 Jul 2025 13:57:10 -0400 Subject: [PATCH 14/29] Document how to use predefined global variables --- docs/rules/isolated-functions.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 5bf3dc09a6..f0c0ffaa3f 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -275,3 +275,28 @@ makeSynchronous(async () => { console.log('Starting...'); }); ``` + +### Predefined global variables + +To enable a predefined set of globals, use the [`globals` package](https://npmjs.com/package/globals) similarly to how you would use it in `languageOptions` (see [ESLint docs on globals](https://eslint.org/docs/latest/use/configure/language-options#predefined-global-variables)): + +```js +import globals from 'globals' + +export default [ + { + rules: { + 'unicorn/isolated-functions': [ + 'error', + { + globals: { + ...globals.builtin, + ...globals.applescript, + ...globals.greasemonkey, + }, + }, + ], + }, + }, +] +``` From c712120ef5f4e6790688d7001bd470147f442b6d Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:38:01 -0400 Subject: [PATCH 15/29] fix markdown --- docs/rules/isolated-functions.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index f0c0ffaa3f..629b3538ba 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -53,7 +53,6 @@ export const fetchSync = () => { console.log(getText('https://example.com')); }; ``` -``` ```js const foo = 'hi'; From 8f21993826f3531abbd7f04048b410e71ef3df17 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 16 Jul 2025 15:52:24 +0200 Subject: [PATCH 16/29] Update isolated-functions.md --- docs/rules/isolated-functions.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 629b3538ba..567a52601f 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -25,8 +25,8 @@ export const fetchSync = () => { const url = 'https://example.com'; const getText = makeSynchronous(async () => { - const res = await fetch(url); // ❌ 'url' is not defined in isolated function scope - return res.text(); + const response = await fetch(url); // ❌ 'url' is not defined in isolated function scope + return response.text(); }); console.log(getText()); @@ -36,8 +36,8 @@ export const fetchSync = () => { export const fetchSync = () => { const getText = makeSynchronous(async () => { const url = 'https://example.com'; // Variable defined within function scope - const res = await fetch(url); - return res.text(); + const response = await fetch(url); + return response.text(); }); console.log(getText()); @@ -46,8 +46,8 @@ export const fetchSync = () => { // βœ… Alternative: Pass as parameter export const fetchSync = () => { const getText = makeSynchronous(async (url) => { // Variable passed as parameter - const res = await fetch(url); - return res.text(); + const response = await fetch(url); + return response.text(); }); console.log(getText('https://example.com')); @@ -129,7 +129,7 @@ Default: `undefined` (uses ESLint's language options globals) Controls how global variables are handled. When not specified, uses ESLint's language options globals. When specified as an object, each key is a global variable name and the value controls its behavior: -- `'readonly'`: Global variable is allowed but cannot be written to (depreacted form `false` also accepted) +- `'readonly'`: Global variable is allowed but cannot be written to (deprecated form `false` also accepted) - `'writable'`: Global variable is allowed and can be read/written (deprecated forms `true` and `'writeable'` also accepted) - `'off'`: Global variable is not allowed @@ -264,7 +264,9 @@ makeSynchronous(async () => { 'Authorization': `Bearer ${process.env.API_TOKEN}` // ❌ 'process' is not in allowed globals } }); + const url = new URL(response.url); + return response.text(); }); From 9d9ecf01afc9319f2bb89d15f546a8ce046eb55f Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:34:12 +0100 Subject: [PATCH 17/29] PR comments --- docs/rules/isolated-functions.md | 4 ++-- rules/isolated-functions.js | 2 +- test/isolated-functions.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 567a52601f..07cf594834 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -129,8 +129,8 @@ Default: `undefined` (uses ESLint's language options globals) Controls how global variables are handled. When not specified, uses ESLint's language options globals. When specified as an object, each key is a global variable name and the value controls its behavior: -- `'readonly'`: Global variable is allowed but cannot be written to (deprecated form `false` also accepted) -- `'writable'`: Global variable is allowed and can be read/written (deprecated forms `true` and `'writeable'` also accepted) +- `'readonly'`: Global variable is allowed but cannot be written to +- `'writable'`: Global variable is allowed and can be read/written - `'off'`: Global variable is not allowed ```js diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 8393a2215c..c677cfde34 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -19,7 +19,7 @@ const parseEsquerySelector = selector => { const defaultOptions = { functions: ['makeSynchronous'], selectors: [], - comments: ['@isolated'], + comments: ['isolated'], }; /** @param {import('eslint').Rule.RuleContext} context */ diff --git a/test/isolated-functions.js b/test/isolated-functions.js index 71ff1b90ba..b9fbb19f28 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -74,7 +74,7 @@ test({ return foo.slice(); } `), - errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: '@isolated comment on arrow function', @@ -83,7 +83,7 @@ test({ /** @isolated */ const abc = () => foo.slice(); `), - errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: '@isolated inline comment', @@ -92,7 +92,7 @@ test({ // @isolated const abc = () => foo.slice(); `), - errors: [error({name: 'foo', reason: 'follows comment containing "@isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: 'all global variables can be explicitly disallowed', From dbf974f16d22b7b68bc632e7d43c952fcff9da46 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:39:19 +0100 Subject: [PATCH 18/29] outdent + lint --- docs/rules/isolated-functions.md | 2 +- test/isolated-functions.js | 78 ++++++++++++++++---------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 07cf594834..8394559f64 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -1,6 +1,6 @@ # Prevent usage of variables from outside the scope of isolated functions -πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). +πŸ’ΌπŸš« This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). This rule is _disabled_ in the β˜‘οΈ `unopinionated` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). diff --git a/test/isolated-functions.js b/test/isolated-functions.js index b9fbb19f28..721f8dc3c8 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -1,4 +1,4 @@ -import stripIndent from 'strip-indent'; +import outdent from 'outdent'; import {getTester} from './utils/test.js'; const {test} = getTester(import.meta); @@ -11,119 +11,119 @@ test({ invalid: [ { name: 'out of scope variable under makeSynchronous (arrow function)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(() => foo.slice()); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'out of scope variable under makeSynchronous (async arrow function)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(async () => foo.slice()); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'out of scope variable under makeSynchronous (function expression)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(function () { return foo.slice(); }); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'out of scope variable under makeSynchronous (async function expression)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(async function () { return foo.slice(); }); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'out of scope variable under makeSynchronous (named function expression)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(function abc () { return foo.slice(); }); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'out of scope variable under makeSynchronous (named async function expression)', - code: stripIndent(` + code: outdent` const foo = 'hi'; makeSynchronous(async function abc () { return foo.slice(); }); - `), + `, errors: [fooInMakeSynchronousError], }, { name: '@isolated comment on function declaration', - code: stripIndent(` + code: outdent` const foo = 'hi'; /** @isolated */ function abc () { return foo.slice(); } - `), + `, errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: '@isolated comment on arrow function', - code: stripIndent(` + code: outdent` const foo = 'hi'; /** @isolated */ const abc = () => foo.slice(); - `), + `, errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: '@isolated inline comment', - code: stripIndent(` + code: outdent` const foo = 'hi'; // @isolated const abc = () => foo.slice(); - `), + `, errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], }, { name: 'all global variables can be explicitly disallowed', languageOptions: {globals: {foo: true}}, options: [{globals: {}}], - code: stripIndent(` + code: outdent` makeSynchronous(function () { return foo.slice(); }); - `), + `, errors: [fooInMakeSynchronousError], }, { name: 'individual global variables can be explicitly disallowed', options: [{globals: {URLSearchParams: 'readonly', URL: 'off'}}], - code: stripIndent(` + code: outdent` makeSynchronous(function () { return new URL('https://example.com?') + new URLSearchParams({a: 'b'}).toString(); }); - `), + `, errors: [error({name: 'URL', reason: 'callee of function named "makeSynchronous"'})], }, { name: 'check globals writability', - code: stripIndent(` + code: outdent` makeSynchronous(function () { location = new URL('https://example.com'); process = {env: {}}; process.env.FOO = 'bar'; }); - `), + `, errors: [ // Only one error, `location = new URL('https://example.com')` and `process.env.FOO = 'bar'` are fine, the problem is `process = {...}`. error({ @@ -136,7 +136,7 @@ test({ name: 'make a function isolated by a selector', // In this case, we're imagining some naming convention for lambda functions that will be created via `fn.toString()` options: [{selectors: ['FunctionDeclaration[id.name=/lambdaHandler.*/]']}], - code: stripIndent(` + code: outdent` const foo = 'hi'; function lambdaHandlerFoo() { @@ -151,7 +151,7 @@ test({ name: 'fooLambda', code: lambdaHandlerFoo.toString(), }); - `), + `, errors: [ error({name: 'foo', reason: 'matches selector "FunctionDeclaration[id.name=/lambdaHandler.*/]"'}), ], @@ -161,58 +161,58 @@ test({ valid: [ { name: 'variable defined in scope of isolated function', - code: stripIndent(` + code: outdent` makeSynchronous(() => { const foo = 'hi'; return foo.slice(); }); - `), + `, }, { name: 'variable defined as parameter of isolated function', - code: stripIndent(` + code: outdent` makeSynchronous(foo => { return foo.slice(); }); - `), + `, }, { name: 'inner function can access outer function parameters', - code: stripIndent(` + code: outdent` /** @isolated */ function abc () { const foo = 'hi'; const slice = () => foo.slice(); return slice(); } - `), + `, }, { name: 'variable defined as parameter of isolated function (async)', - code: stripIndent(` + code: outdent` makeSynchronous(async function (foo) { return foo.slice(); }); - `), + `, }, { name: 'can implicitly allow global variables from language options', languageOptions: {globals: {foo: true}}, - code: stripIndent(` + code: outdent` makeSynchronous(function () { return foo.slice(); }); - `), + `, }, { name: 'allow global variables separate from language options', languageOptions: {globals: {abc: true}}, options: [{globals: {foo: true}}], - code: stripIndent(` + code: outdent` makeSynchronous(function () { return foo.slice(); }); - `), + `, }, ], }); From 03ba049e16eba6690af9c85d8dfc2ac8b6d239b9 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:28:36 +0000 Subject: [PATCH 19/29] stricter comment parsing --- docs/rules/isolated-functions.md | 2 +- rules/isolated-functions.js | 11 ++++++---- test/isolated-functions.js | 36 +++++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 8394559f64..2a81681e32 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -106,7 +106,7 @@ Array of [ESLint selectors](https://eslint.org/docs/developer-guide/selectors) t Type: `string[]`\ Default: `['@isolated']` -Array of comment strings that mark functions as isolated. Functions with JSDoc comments containing these strings will be considered isolated. +Array of comment strings that mark functions as isolated. Functions with inline, block, or JSDoc comments tagged with these strings will be considered isolated. (Definition of "tagged": either the comment consists solely of the tag, or starts with it, and has an explanation following a hyphen, like `// @isolated - this function will be stringified`). ```js { diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index c677cfde34..62065f91ff 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -19,7 +19,7 @@ const parseEsquerySelector = selector => { const defaultOptions = { functions: ['makeSynchronous'], selectors: [], - comments: ['isolated'], + comments: ['@isolated'], }; /** @param {import('eslint').Rule.RuleContext} context */ @@ -86,10 +86,13 @@ const create = context => { } if (previousToken?.type === 'Block' || previousToken?.type === 'Line') { - const previousComment = previousToken.value.trim().toLowerCase(); - const match = options.comments.find(comment => previousComment.includes(comment)); + const previousComment = previousToken.value + .replace(/(\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated` + .trim() + .toLowerCase(); + const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `)); if (match) { - return `follows comment containing ${JSON.stringify(match)}`; + return `follows comment ${JSON.stringify(match)}`; } } } diff --git a/test/isolated-functions.js b/test/isolated-functions.js index 721f8dc3c8..ea06b8421b 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -74,7 +74,7 @@ test({ return foo.slice(); } `, - errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], }, { name: '@isolated comment on arrow function', @@ -83,7 +83,37 @@ test({ /** @isolated */ const abc = () => foo.slice(); `, - errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], + }, + { + name: '@isolated comments with explanations', + code: outdent` + const foo = 'hi'; + // @isolated - explanation + const abc1 = () => foo.slice(); + `, + errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], + }, + { + name: '@isolated block comments', + code: outdent` + const foo = 'hi'; + /* @isolated */ + const abc1 = () => foo.slice(); + + /** @isolated */ + const abc2 = () => foo.slice(); + + /** + * @isolated + */ + const abc3 = () => foo.slice(); + `, + errors: [ + error({name: 'foo', reason: 'follows comment "@isolated"'}), + error({name: 'foo', reason: 'follows comment "@isolated"'}), + error({name: 'foo', reason: 'follows comment "@isolated"'}), + ], }, { name: '@isolated inline comment', @@ -92,7 +122,7 @@ test({ // @isolated const abc = () => foo.slice(); `, - errors: [error({name: 'foo', reason: 'follows comment containing "isolated"'})], + errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], }, { name: 'all global variables can be explicitly disallowed', From 0bd80a2e3b0584a3db55f0dbff16818796bfa207 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:52:40 +0000 Subject: [PATCH 20/29] use context.on --- rules/isolated-functions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 62065f91ff..004b35e0ee 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -116,10 +116,10 @@ const create = context => { } }; - return Object.fromEntries(functionTypes.map(type => [ - `${type}:exit`, + context.on( + functionTypes.map(type => `${type}:exit`), checkForExternallyScopedVariables, - ])); + ); }; /** @type {import('json-schema').JSONSchema7[]} */ From f303cb26ce4b4987fed2f3afee96641ccdf1f102 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:12:34 +0000 Subject: [PATCH 21/29] exports --- rules/isolated-functions.js | 43 ++++++++++++++++++++++++++----------- test/isolated-functions.js | 16 ++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 004b35e0ee..18ed8283d7 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -71,22 +71,39 @@ const create = context => { } }; - /** @param {import('estree').Node & {parent?: import('estree').Node}} node */ + /** + Find a comment on this node or its parent, in cases where the node passed is part of a variable or export declaration. + @param {import('estree').Node} node + */ + const findComment = node => { + let previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); + let commentableNode = node; + while ( + (previousToken?.type !== 'Block' && previousToken?.type !== 'Line') + && (commentableNode.parent.type === 'VariableDeclarator' + || commentableNode.parent.type === 'VariableDeclaration' + || commentableNode.parent.type === 'ExportNamedDeclaration' + || commentableNode.parent.type === 'ExportDefaultDeclaration') + ) { + commentableNode = commentableNode.parent; + previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true}); + } + + if (previousToken?.type === 'Block' || previousToken?.type === 'Line') { + return previousToken.value; + } + }; + + /** + Find the string "reason" that a function (node) should be considered isolated. For passing in to `context.report(...)` when out-of-scope variables are found. Returns undefined if the function should not be considered isolated. + @param {import('estree').Node & {parent?: import('estree').Node}} node + */ const reasonForBeingIsolatedFunction = node => { if (options.comments.length > 0) { - let previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); - let commentableNode = node; - while ( - (previousToken?.type !== 'Block' && previousToken?.type !== 'Line') - && (commentableNode.parent.type === 'VariableDeclarator' || commentableNode.parent.type === 'VariableDeclaration') - ) { - // Search up to find jsdoc comments on the parent declaration `/** @isolated */ const foo = () => abc` - commentableNode = commentableNode.parent; - previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true}); - } + let previousComment = findComment(node); - if (previousToken?.type === 'Block' || previousToken?.type === 'Line') { - const previousComment = previousToken.value + if (previousComment) { + previousComment = previousComment .replace(/(\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated` .trim() .toLowerCase(); diff --git a/test/isolated-functions.js b/test/isolated-functions.js index ea06b8421b..f1e8e4e13a 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -124,6 +124,22 @@ test({ `, errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], }, + { + name: '@isolated comment on exports', + code: outdent` + const foo = 'hi'; + + // @isolated + export const abc = () => foo.slice(); + + // @isolated + export default () => foo.slice(); + `, + errors: [ + error({name: 'foo', reason: 'follows comment "@isolated"'}), + error({name: 'foo', reason: 'follows comment "@isolated"'}), + ], + }, { name: 'all global variables can be explicitly disallowed', languageOptions: {globals: {foo: true}}, From 9be0de202230d074f14ba0dfa23e3e38dc2ebec6 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:36:46 +0000 Subject: [PATCH 22/29] feedback --- rules/isolated-functions.js | 8 +++++--- test/isolated-functions.js | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 18ed8283d7..dabb8b21c8 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -71,6 +71,8 @@ const create = context => { } }; + const isComment = token => token?.type === 'Block' || token?.type === 'Line'; + /** Find a comment on this node or its parent, in cases where the node passed is part of a variable or export declaration. @param {import('estree').Node} node @@ -79,7 +81,7 @@ const create = context => { let previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); let commentableNode = node; while ( - (previousToken?.type !== 'Block' && previousToken?.type !== 'Line') + !isComment(previousToken) && (commentableNode.parent.type === 'VariableDeclarator' || commentableNode.parent.type === 'VariableDeclaration' || commentableNode.parent.type === 'ExportNamedDeclaration' @@ -89,7 +91,7 @@ const create = context => { previousToken = sourceCode.getTokenBefore(commentableNode, {includeComments: true}); } - if (previousToken?.type === 'Block' || previousToken?.type === 'Line') { + if (isComment(previousToken)) { return previousToken.value; } }; @@ -107,7 +109,7 @@ const create = context => { .replace(/(\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated` .trim() .toLowerCase(); - const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `)); + const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `) || previousComment.startsWith(`${comment} -- `)); if (match) { return `follows comment ${JSON.stringify(match)}`; } diff --git a/test/isolated-functions.js b/test/isolated-functions.js index f1e8e4e13a..c7ea8cf1d7 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -91,8 +91,14 @@ test({ const foo = 'hi'; // @isolated - explanation const abc1 = () => foo.slice(); + + // @isolated -- explanation + const abc2 = () => foo.slice(); `, - errors: [error({name: 'foo', reason: 'follows comment "@isolated"'})], + errors: [ + error({name: 'foo', reason: 'follows comment "@isolated"'}), + error({name: 'foo', reason: 'follows comment "@isolated"'}), + ], }, { name: '@isolated block comments', From a8467d7fda72446c1f79e24c53dab80b75914e3a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:23:39 +0000 Subject: [PATCH 23/29] test globals with no languageOptions/options set --- test/isolated-functions.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/isolated-functions.js b/test/isolated-functions.js index c7ea8cf1d7..97ed63bbde 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -247,6 +247,10 @@ test({ }); `, }, + { + name: 'default global variables come from language options', + code: 'makeSynchronous(() => process.env.MAP ? new Map() : {})', + }, { name: 'can implicitly allow global variables from language options', languageOptions: {globals: {foo: true}}, From 88406ed1a7189e3f17a85875a2d93d35294310e6 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:28:28 +0000 Subject: [PATCH 24/29] arbitrary change --- test/isolated-functions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/isolated-functions.js b/test/isolated-functions.js index 97ed63bbde..012035db26 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -249,7 +249,7 @@ test({ }, { name: 'default global variables come from language options', - code: 'makeSynchronous(() => process.env.MAP ? new Map() : {})', + code: 'makeSynchronous(() => process.env.MAP ? new Map() : new URL("https://example.com"))', }, { name: 'can implicitly allow global variables from language options', From 5b70ca404e8ebf5a99dbefb5d550a26a158f2299 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:00:00 +0000 Subject: [PATCH 25/29] fisker globals --- rules/isolated-functions.js | 6 +++++- test/isolated-functions.js | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index dabb8b21c8..076d61002d 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -1,3 +1,4 @@ +import globals from 'globals'; import esquery from 'esquery'; import functionTypes from './ast/function-types.js'; @@ -33,7 +34,10 @@ const create = context => { options.comments = options.comments.map(comment => comment.toLowerCase()); - const allowedGlobals = options.globals ?? context.languageOptions.globals; + const allowedGlobals = { + ...(globals[`es${context.languageOptions.ecmaVersion}`] ?? globals.builtins), + ...(options.globals ?? context.languageOptions.globals), + }; /** @param {import('estree').Node} node */ const checkForExternallyScopedVariables = node => { diff --git a/test/isolated-functions.js b/test/isolated-functions.js index 012035db26..a2583951ec 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -208,6 +208,12 @@ test({ error({name: 'foo', reason: 'matches selector "FunctionDeclaration[id.name=/lambdaHandler.*/]"'}), ], }, + { + name: 'can explicitly turn off ecmascript globals', + code: 'makeSynchronous(() => new Array())', + options: [{globals: {Array: 'off'}}], + errors: [error({name: 'Array', reason: 'callee of function named "makeSynchronous"'})], + }, ], /** @type {import('eslint').RuleTester.ValidTestCase[]} */ valid: [ @@ -251,6 +257,15 @@ test({ name: 'default global variables come from language options', code: 'makeSynchronous(() => process.env.MAP ? new Map() : new URL("https://example.com"))', }, + { + name: 'global Array', + code: 'makeSynchronous(() => new Array())', + }, + { + name: 'global Array w globals: {} still works', + code: 'makeSynchronous(() => new Array())', + options: [{globals: {}}], + }, { name: 'can implicitly allow global variables from language options', languageOptions: {globals: {foo: true}}, From 412715303d3e9d7aaf8de188b592f68c5e20f676 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:00:22 +0000 Subject: [PATCH 26/29] valid first --- test/isolated-functions.js | 142 ++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/test/isolated-functions.js b/test/isolated-functions.js index a2583951ec..a8b54f27c8 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -7,6 +7,77 @@ const error = data => ({messageId: 'externally-scoped-variable', data}); const fooInMakeSynchronousError = error({name: 'foo', reason: 'callee of function named "makeSynchronous"'}); test({ + /** @type {import('eslint').RuleTester.ValidTestCase[]} */ + valid: [ + { + name: 'variable defined in scope of isolated function', + code: outdent` + makeSynchronous(() => { + const foo = 'hi'; + return foo.slice(); + }); + `, + }, + { + name: 'variable defined as parameter of isolated function', + code: outdent` + makeSynchronous(foo => { + return foo.slice(); + }); + `, + }, + { + name: 'inner function can access outer function parameters', + code: outdent` + /** @isolated */ + function abc () { + const foo = 'hi'; + const slice = () => foo.slice(); + return slice(); + } + `, + }, + { + name: 'variable defined as parameter of isolated function (async)', + code: outdent` + makeSynchronous(async function (foo) { + return foo.slice(); + }); + `, + }, + { + name: 'default global variables come from language options', + code: 'makeSynchronous(() => process.env.MAP ? new Map() : new URL("https://example.com"))', + }, + { + name: 'global Array', + code: 'makeSynchronous(() => new Array())', + }, + { + name: 'global Array w globals: {} still works', + code: 'makeSynchronous(() => new Array())', + options: [{globals: {}}], + }, + { + name: 'can implicitly allow global variables from language options', + languageOptions: {globals: {foo: true}}, + code: outdent` + makeSynchronous(function () { + return foo.slice(); + }); + `, + }, + { + name: 'allow global variables separate from language options', + languageOptions: {globals: {abc: true}}, + options: [{globals: {foo: true}}], + code: outdent` + makeSynchronous(function () { + return foo.slice(); + }); + `, + }, + ], /** @type {import('eslint').RuleTester.InvalidTestCase[]} */ invalid: [ { @@ -215,75 +286,4 @@ test({ errors: [error({name: 'Array', reason: 'callee of function named "makeSynchronous"'})], }, ], - /** @type {import('eslint').RuleTester.ValidTestCase[]} */ - valid: [ - { - name: 'variable defined in scope of isolated function', - code: outdent` - makeSynchronous(() => { - const foo = 'hi'; - return foo.slice(); - }); - `, - }, - { - name: 'variable defined as parameter of isolated function', - code: outdent` - makeSynchronous(foo => { - return foo.slice(); - }); - `, - }, - { - name: 'inner function can access outer function parameters', - code: outdent` - /** @isolated */ - function abc () { - const foo = 'hi'; - const slice = () => foo.slice(); - return slice(); - } - `, - }, - { - name: 'variable defined as parameter of isolated function (async)', - code: outdent` - makeSynchronous(async function (foo) { - return foo.slice(); - }); - `, - }, - { - name: 'default global variables come from language options', - code: 'makeSynchronous(() => process.env.MAP ? new Map() : new URL("https://example.com"))', - }, - { - name: 'global Array', - code: 'makeSynchronous(() => new Array())', - }, - { - name: 'global Array w globals: {} still works', - code: 'makeSynchronous(() => new Array())', - options: [{globals: {}}], - }, - { - name: 'can implicitly allow global variables from language options', - languageOptions: {globals: {foo: true}}, - code: outdent` - makeSynchronous(function () { - return foo.slice(); - }); - `, - }, - { - name: 'allow global variables separate from language options', - languageOptions: {globals: {abc: true}}, - options: [{globals: {foo: true}}], - code: outdent` - makeSynchronous(function () { - return foo.slice(); - }); - `, - }, - ], }); From 8eb68b2f5d49490e3c0087bbda806458779afae8 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:27:40 +0000 Subject: [PATCH 27/29] feedback --- docs/rules/isolated-functions.md | 42 ++++++++++++++------------------ rules/isolated-functions.js | 4 +-- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 2a81681e32..440c3bbd59 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -21,37 +21,31 @@ By default, this rule uses ESLint's language options globals and allows global v ```js import makeSynchronous from 'make-synchronous'; -export const fetchSync = () => { - const url = 'https://example.com'; +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 response = await fetch(url); // ❌ 'url' is not defined in isolated function scope + return response.text(); +}); - console.log(getText()); -}; +console.log(getText()); // βœ… Define all variables within isolated function's scope -export const fetchSync = () => { - const getText = makeSynchronous(async () => { - const url = 'https://example.com'; // Variable defined within function scope - const response = await fetch(url); - return response.text(); - }); +const getText = makeSynchronous(async () => { + const url = 'https://example.com'; // Variable defined within function scope + const response = await fetch(url); + return response.text(); +}); - console.log(getText()); -}; +console.log(getText()); // βœ… Alternative: Pass as parameter -export const fetchSync = () => { - const getText = makeSynchronous(async (url) => { // Variable passed as parameter - const response = await fetch(url); - return response.text(); - }); +const getText = makeSynchronous(async (url) => { // Variable passed as parameter + const response = await fetch(url); + return response.text(); +}); - console.log(getText('https://example.com')); -}; +console.log(getText('https://example.com')); ``` ```js @@ -217,7 +211,7 @@ makeSynchronous(async () => { 'unicorn/isolated-functions': [ 'error', { - globals: {} // Empty object disallows all globals + globals: {} // Empty object disallows all globals except language globals } ] } diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 076d61002d..673667cdbe 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -1,6 +1,6 @@ import globals from 'globals'; import esquery from 'esquery'; -import functionTypes from './ast/function-types.js'; +import {functionTypes} from './ast/index.js'; const MESSAGE_ID_EXTERNALLY_SCOPED_VARIABLE = 'externally-scoped-variable'; const messages = { @@ -110,7 +110,7 @@ const create = context => { if (previousComment) { previousComment = previousComment - .replace(/(\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated` + .replace(/(?:\*\s*)*/, '') // JSDoc comments like `/** @isolated */` are parsed into `* @isolated`. And `/**\n * @isolated */` is parsed into `*\n * @isolated` .trim() .toLowerCase(); const match = options.comments.find(comment => previousComment === comment || previousComment.startsWith(`${comment} - `) || previousComment.startsWith(`${comment} -- `)); From 1fa750e64d3bc33e55e02df59eb797c2517e0b13 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:31:17 +0000 Subject: [PATCH 28/29] rm console.log --- docs/rules/isolated-functions.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index 440c3bbd59..bbb55f174a 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -28,8 +28,6 @@ const getText = makeSynchronous(async () => { return response.text(); }); -console.log(getText()); - // βœ… Define all variables within isolated function's scope const getText = makeSynchronous(async () => { const url = 'https://example.com'; // Variable defined within function scope @@ -37,8 +35,6 @@ const getText = makeSynchronous(async () => { return response.text(); }); -console.log(getText()); - // βœ… Alternative: Pass as parameter const getText = makeSynchronous(async (url) => { // Variable passed as parameter const response = await fetch(url); From d9836a08ddfcadf56348b3efbccbd5c0cda347d6 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:35:05 +0000 Subject: [PATCH 29/29] globals -> overrideGlobals --- docs/rules/isolated-functions.md | 28 +++------------------------- rules/isolated-functions.js | 8 +++++--- test/isolated-functions.js | 21 +++++---------------- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/docs/rules/isolated-functions.md b/docs/rules/isolated-functions.md index bbb55f174a..642b8af6c3 100644 --- a/docs/rules/isolated-functions.md +++ b/docs/rules/isolated-functions.md @@ -112,7 +112,7 @@ Array of comment strings that mark functions as isolated. Functions with inline, } ``` -### globals +### overrideGlobals Type: `object`\ Default: `undefined` (uses ESLint's language options globals) @@ -128,7 +128,7 @@ Controls how global variables are handled. When not specified, uses ESLint's lan 'unicorn/isolated-functions': [ 'error', { - globals: { + overrideGlobals: { console: 'writable', // Allowed and writable fetch: 'readonly', // Allowed but readonly process: 'off' // Not allowed @@ -200,28 +200,6 @@ makeSynchronous(async () => { }); ``` -### Disallowing all globals - -```js -{ - 'unicorn/isolated-functions': [ - 'error', - { - globals: {} // Empty object disallows all globals except language globals - } - ] -} -``` - -```js -// ❌ All globals are disallowed -makeSynchronous(async () => { - console.log('Starting...'); // ❌ 'console' is not allowed - const response = await fetch('https://api.example.com'); // ❌ 'fetch' is not allowed - return response.text(); -}); -``` - ### Allowing specific globals ```js @@ -229,7 +207,7 @@ makeSynchronous(async () => { 'unicorn/isolated-functions': [ 'error', { - globals: { + overrideGlobals: { console: 'writable', // Allowed and writable fetch: 'readonly', // Allowed but readonly URL: 'readonly' // Allowed but readonly diff --git a/rules/isolated-functions.js b/rules/isolated-functions.js index 673667cdbe..4f38c53e8c 100644 --- a/rules/isolated-functions.js +++ b/rules/isolated-functions.js @@ -16,11 +16,12 @@ const parseEsquerySelector = selector => { return parsedEsquerySelectors.get(selector); }; -/** @type {{functions: string[], selectors: string[], comments: string[], globals?: import('eslint').Linter.Globals}} */ +/** @type {{functions: string[], selectors: string[], comments: string[], overrideGlobals?: import('eslint').Linter.Globals}} */ const defaultOptions = { functions: ['makeSynchronous'], selectors: [], comments: ['@isolated'], + overrideGlobals: {}, }; /** @param {import('eslint').Rule.RuleContext} context */ @@ -36,7 +37,8 @@ const create = context => { const allowedGlobals = { ...(globals[`es${context.languageOptions.ecmaVersion}`] ?? globals.builtins), - ...(options.globals ?? context.languageOptions.globals), + ...context.languageOptions.globals, + ...options.overrideGlobals, }; /** @param {import('estree').Node} node */ @@ -151,7 +153,7 @@ const schema = [ type: 'object', additionalProperties: false, properties: { - globals: { + overrideGlobals: { additionalProperties: { anyOf: [{type: 'boolean'}, {type: 'string', enum: ['readonly', 'writable', 'writeable', 'off']}], }, diff --git a/test/isolated-functions.js b/test/isolated-functions.js index a8b54f27c8..a493caf65c 100644 --- a/test/isolated-functions.js +++ b/test/isolated-functions.js @@ -54,9 +54,9 @@ test({ code: 'makeSynchronous(() => new Array())', }, { - name: 'global Array w globals: {} still works', + name: 'global Array w overrideGlobals: {} still works', code: 'makeSynchronous(() => new Array())', - options: [{globals: {}}], + options: [{overrideGlobals: {}}], }, { name: 'can implicitly allow global variables from language options', @@ -70,7 +70,7 @@ test({ { name: 'allow global variables separate from language options', languageOptions: {globals: {abc: true}}, - options: [{globals: {foo: true}}], + options: [{overrideGlobals: {foo: true}}], code: outdent` makeSynchronous(function () { return foo.slice(); @@ -217,20 +217,9 @@ test({ error({name: 'foo', reason: 'follows comment "@isolated"'}), ], }, - { - name: 'all global variables can be explicitly disallowed', - languageOptions: {globals: {foo: true}}, - options: [{globals: {}}], - code: outdent` - makeSynchronous(function () { - return foo.slice(); - }); - `, - errors: [fooInMakeSynchronousError], - }, { name: 'individual global variables can be explicitly disallowed', - options: [{globals: {URLSearchParams: 'readonly', URL: 'off'}}], + options: [{overrideGlobals: {URLSearchParams: 'readonly', URL: 'off'}}], code: outdent` makeSynchronous(function () { return new URL('https://example.com?') + new URLSearchParams({a: 'b'}).toString(); @@ -282,7 +271,7 @@ test({ { name: 'can explicitly turn off ecmascript globals', code: 'makeSynchronous(() => new Array())', - options: [{globals: {Array: 'off'}}], + options: [{overrideGlobals: {Array: 'off'}}], errors: [error({name: 'Array', reason: 'callee of function named "makeSynchronous"'})], }, ],