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 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions docs/rules/isolated-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# 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).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

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

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

```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`

### 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: `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

```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();
});
```
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | βœ… | | |
Expand Down
1 change: 1 addition & 0 deletions rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading
Loading