Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6715b87
New rule: `isolated-functions`
mmkal Jul 2, 2025
2ce92bf
Merge remote-tracking branch 'origin/main' into isolated-functions
mmkal Jul 2, 2025
27ac79c
esm is cool
mmkal Jul 2, 2025
4fbb759
lint
mmkal Jul 2, 2025
55036f1
exts
mmkal Jul 3, 2025
ea09c9e
allow globals by default
mmkal Jul 3, 2025
26e70a5
set (updated 07:04)
mmkal Jul 7, 2025
a52c3ee
rm .reverse()
mmkal Jul 7, 2025
60f072d
Update isolated-functions.md
sindresorhus Jul 7, 2025
7786865
Update isolated-functions.js
sindresorhus Jul 7, 2025
b3649e9
docs: group fail/pass examples closer
mmkal Jul 7, 2025
9c92506
docs: block fn
mmkal Jul 7, 2025
30eaa9d
more better docs (updated 08:02)
mmkal Jul 7, 2025
f4b8b66
match builtin eslint globals behavior
mmkal Jul 7, 2025
543f751
Document how to use predefined global variables
mmkal Jul 7, 2025
c712120
fix markdown
mmkal Jul 9, 2025
8f21993
Update isolated-functions.md
sindresorhus Jul 16, 2025
9d9ecf0
PR comments
mmkal Sep 26, 2025
5374d20
Merge remote-tracking branch 'origin/main' into isolated-functions
mmkal Sep 26, 2025
dbf974f
outdent + lint
mmkal Sep 26, 2025
03ba049
stricter comment parsing
mmkal Oct 27, 2025
0bd80a2
use context.on
mmkal Oct 27, 2025
f303cb2
exports
mmkal Oct 27, 2025
9be0de2
feedback
mmkal Oct 27, 2025
a8467d7
test globals with no languageOptions/options set
mmkal Oct 27, 2025
88406ed
arbitrary change
mmkal Oct 27, 2025
5b70ca4
fisker globals
mmkal Oct 31, 2025
4127153
valid first
mmkal Oct 31, 2025
8eb68b2
feedback
mmkal Nov 4, 2025
1fa750e
rm console.log
mmkal Nov 4, 2025
d9836a0
globals -> overrideGlobals
mmkal Nov 6, 2025
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
303 changes: 303 additions & 0 deletions docs/rules/isolated-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# 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 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

```js
import makeSynchronous from 'make-synchronous';

export const fetchSync = () => {
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();
});

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

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

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](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: `object`\
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)
- `'off'`: Global variable is not allowed

```js
{
'unicorn/isolated-functions': [
'error',
{
globals: {
console: 'writable', // Allowed and writable
fetch: 'readonly', // Allowed but readonly
process: 'off' // Not allowed
}
}
]
}
```

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

### 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
globals: {} // Empty object disallows all globals
globals: {} // Empty object disallows all globals except language globals

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should mention that language globals are always enabled; they need to be disabled explicitly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think worth changing the option to

{
	esGlobals: boolean,
	eslintGlobals: boolean,
	overrideGlobals: {
		...
	}
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about I change globals to overrideGlobals and we can add the other ones if/when somebody asks for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I think the behaviour is less confusing now - before globals: {} could turn off everything in language options, and I don't think it was good for globals: undefined to be so dramatically different from globals: {}.

Now, overrideGlobals does just that - it overrides globals that otherwise are defined, so if you want to turn off globals you turn them off explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commit: d9836a0

}
]
}
```

```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
{
'unicorn/isolated-functions': [
'error',
{
globals: {
console: 'writable', // Allowed and writable
fetch: 'readonly', // Allowed but readonly
URL: 'readonly' // Allowed but readonly
}
}
]
}
```

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

// ❌ Attempting to write to readonly global
makeSynchronous(async () => {
fetch = null; // ❌ 'fetch' is readonly
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,
},
},
],
},
},
]
```
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