Skip to content

Add isolated-functions rule #2701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Conversation

mmkal
Copy link
Contributor

@mmkal mmkal commented Jul 2, 2025

Fixes #2214

From docs:


Prevent usage of variables from outside the scope of isolated functions

💼 This rule is enabled in the ✅ recommended config.

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 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 worker)
  • Functions that will be serialized via Function.prototype.toString()
  • 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.

Examples

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());
};

// ✅ 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 res = await fetch(url);
		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);
		return res.text();
	});

	console.log(getText('https://example.com'));
};

```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
	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 to identify isolated functions. Useful for custom naming conventions or framework-specific patterns.

{
	'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.

{
	'unicorn/isolated-functions': [
		'error',
		{
			comments: [
				'@isolated',
				'@remote'
			]
		}
	]
}

globals

Type: boolean | string[]
Default: true

Controls how global variables are handled:

  • 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
{
	'unicorn/isolated-functions': [
		'error',
		{
			globals: ['console', 'fetch'] // Only allow these globals
		}
	]
}

Examples

Custom function names

{
	'unicorn/isolated-functions': [
		'error',
		{
			functions: [
				'makeSynchronous',
				'createWorker',
				'serializeFunction'
			]
		}
	]
}

Lambda function naming convention

{
	'unicorn/isolated-functions': [
		'error',
		{
			selectors: [
				'FunctionDeclaration[id.name=/lambdaHandler.*/]'
			]
		}
	]
}
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

{
	'unicorn/isolated-functions': [
		'error',
		{
			globals: [
				'console',
				'fetch',
				'URL'
			]
		}
	]
}
// ✅ 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();
});

Note: I didn't go as far as suggested in that issue #2214 (comment) - that is, to track imports from specific modules like import makeSync from 'make-synchronous', to allow for arbitrary aliasing from that package. Instead I went for fairly specific naming conventions. But I thought the rule still had value while relying on naming convention, so opening a pull request now since I don't know when I'll get some more time to spend on it. I think the module-import-tracking thing could go in later as a new feature, if there's enough interest in it.

@fisker fisker changed the title New rule: isolated-functions Add isolated-functions rule Jul 7, 2025
@mmkal mmkal force-pushed the isolated-functions branch from f926ae0 to 26e70a5 Compare July 7, 2025 11:04
@mmkal mmkal force-pushed the isolated-functions branch from 64b3917 to 30eaa9d Compare July 7, 2025 12:02
@mmkal
Copy link
Contributor Author

mmkal commented Jul 7, 2025

Actually maybe I should re-think how globals works. It seems wrong that it works differently from the built-in eslint languageOptions.globals. Maybe it should be more like this:

{
	'unicorn/isolated-functions': [
		'error',
		{
			globals: [
				'node', // globals.node from `globals` package
				'browser', // globals.browser from `globals` package
				{foobar: true}, // also allow this global
				{abc: 'off'}, // disallow this global
				'xyz', // error, this doesn't exist in the `globals` package
		}
	]
}

The above also allows for import globals from 'globals' and spreading the values in directly too, but the string option just adds a shortcut so end-users don't have to manually install globals

@fisker @sindresorhus what do you think?

@fisker
Copy link
Collaborator

fisker commented Jul 7, 2025

so end-users don't have to manually install globals

Users should always have globals installed, I don't think we should worry about it, we can accept the languageOptions.globals style, anyway, I think language builtins should always allowed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rule proposal: isolated-functions
3 participants