Skip to content

Commit 4956a6b

Browse files
mmkalsindresorhus
andauthored
Add isolated-functions rule (#2701)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent af42ccb commit 4956a6b

File tree

5 files changed

+750
-0
lines changed

5 files changed

+750
-0
lines changed

docs/rules/isolated-functions.md

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Prevent usage of variables from outside the scope of isolated functions
2+
3+
💼🚫 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).
4+
5+
<!-- end auto-generated rule header -->
6+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
7+
8+
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.
9+
10+
Common scenarios where functions must be isolated:
11+
12+
- Functions passed to `makeSynchronous()` (executed in worker)
13+
- Functions that will be serialized via `Function.prototype.toString()`
14+
- Server actions or other remote execution contexts
15+
- Functions with specific JSDoc annotations
16+
17+
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.
18+
19+
## Examples
20+
21+
```js
22+
import makeSynchronous from 'make-synchronous';
23+
24+
const url = 'https://example.com';
25+
26+
const getText = makeSynchronous(async () => {
27+
const response = await fetch(url); // ❌ 'url' is not defined in isolated function scope
28+
return response.text();
29+
});
30+
31+
// ✅ Define all variables within isolated function's scope
32+
const getText = makeSynchronous(async () => {
33+
const url = 'https://example.com'; // Variable defined within function scope
34+
const response = await fetch(url);
35+
return response.text();
36+
});
37+
38+
// ✅ Alternative: Pass as parameter
39+
const getText = makeSynchronous(async (url) => { // Variable passed as parameter
40+
const response = await fetch(url);
41+
return response.text();
42+
});
43+
44+
console.log(getText('https://example.com'));
45+
```
46+
47+
```js
48+
const foo = 'hi';
49+
50+
/** @isolated */
51+
function abc() {
52+
return foo.slice(); // ❌ 'foo' is not defined in isolated function scope
53+
}
54+
55+
//
56+
/** @isolated */
57+
function abc() {
58+
const foo = 'hi'; // Variable defined within function scope
59+
return foo.slice();
60+
}
61+
```
62+
63+
## Options
64+
65+
Type: `object`
66+
67+
### functions
68+
69+
Type: `string[]`\
70+
Default: `['makeSynchronous']`
71+
72+
Array of function names that create isolated execution contexts. Functions passed as arguments to these functions will be considered isolated.
73+
74+
### selectors
75+
76+
Type: `string[]`\
77+
Default: `[]`
78+
79+
Array of [ESLint selectors](https://eslint.org/docs/developer-guide/selectors) to identify isolated functions. Useful for custom naming conventions or framework-specific patterns.
80+
81+
```js
82+
{
83+
'unicorn/isolated-functions': [
84+
'error',
85+
{
86+
selectors: [
87+
'FunctionDeclaration[id.name=/lambdaHandler.*/]'
88+
]
89+
}
90+
]
91+
}
92+
```
93+
94+
### comments
95+
96+
Type: `string[]`\
97+
Default: `['@isolated']`
98+
99+
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`).
100+
101+
```js
102+
{
103+
'unicorn/isolated-functions': [
104+
'error',
105+
{
106+
comments: [
107+
'@isolated',
108+
'@remote'
109+
]
110+
}
111+
]
112+
}
113+
```
114+
115+
### overrideGlobals
116+
117+
Type: `object`\
118+
Default: `undefined` (uses ESLint's language options globals)
119+
120+
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:
121+
122+
- `'readonly'`: Global variable is allowed but cannot be written to
123+
- `'writable'`: Global variable is allowed and can be read/written
124+
- `'off'`: Global variable is not allowed
125+
126+
```js
127+
{
128+
'unicorn/isolated-functions': [
129+
'error',
130+
{
131+
overrideGlobals: {
132+
console: 'writable', // Allowed and writable
133+
fetch: 'readonly', // Allowed but readonly
134+
process: 'off' // Not allowed
135+
}
136+
}
137+
]
138+
}
139+
```
140+
141+
## Examples
142+
143+
### Custom function names
144+
145+
```js
146+
{
147+
'unicorn/isolated-functions': [
148+
'error',
149+
{
150+
functions: [
151+
'makeSynchronous',
152+
'createWorker',
153+
'serializeFunction'
154+
]
155+
}
156+
]
157+
}
158+
```
159+
160+
### Lambda function naming convention
161+
162+
```js
163+
{
164+
'unicorn/isolated-functions': [
165+
'error',
166+
{
167+
selectors: [
168+
'FunctionDeclaration[id.name=/lambdaHandler.*/]'
169+
]
170+
}
171+
]
172+
}
173+
```
174+
175+
```js
176+
const foo = 'hi';
177+
178+
function lambdaHandlerFoo() { // ❌ Will be flagged as isolated
179+
return foo.slice();
180+
}
181+
182+
function someOtherFunction() { // ✅ Not flagged
183+
return foo.slice();
184+
}
185+
186+
createLambda({
187+
name: 'fooLambda',
188+
code: lambdaHandlerFoo.toString(), // Function will be serialized
189+
});
190+
```
191+
192+
### Default behavior (using ESLint's language options)
193+
194+
```js
195+
// Uses ESLint's language options globals by default
196+
makeSynchronous(async () => {
197+
console.log('Starting...'); // ✅ Allowed if console is in language options
198+
const response = await fetch('https://api.example.com'); // ✅ Allowed if fetch is in language options
199+
return response.text();
200+
});
201+
```
202+
203+
### Allowing specific globals
204+
205+
```js
206+
{
207+
'unicorn/isolated-functions': [
208+
'error',
209+
{
210+
overrideGlobals: {
211+
console: 'writable', // Allowed and writable
212+
fetch: 'readonly', // Allowed but readonly
213+
URL: 'readonly' // Allowed but readonly
214+
}
215+
}
216+
]
217+
}
218+
```
219+
220+
```js
221+
// ✅ All globals used are explicitly allowed
222+
makeSynchronous(async () => {
223+
console.log('Starting...'); // ✅ Allowed global
224+
const response = await fetch('https://api.example.com'); // ✅ Allowed global
225+
const url = new URL(response.url); // ✅ Allowed global
226+
return response.text();
227+
});
228+
229+
makeSynchronous(async () => {
230+
const response = await fetch('https://api.example.com', {
231+
headers: {
232+
'Authorization': `Bearer ${process.env.API_TOKEN}` // ❌ 'process' is not in allowed globals
233+
}
234+
});
235+
236+
const url = new URL(response.url);
237+
238+
return response.text();
239+
});
240+
241+
// ❌ Attempting to write to readonly global
242+
makeSynchronous(async () => {
243+
fetch = null; // ❌ 'fetch' is readonly
244+
console.log('Starting...');
245+
});
246+
```
247+
248+
### Predefined global variables
249+
250+
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)):
251+
252+
```js
253+
import globals from 'globals'
254+
255+
export default [
256+
{
257+
rules: {
258+
'unicorn/isolated-functions': [
259+
'error',
260+
{
261+
globals: {
262+
...globals.builtin,
263+
...globals.applescript,
264+
...globals.greasemonkey,
265+
},
266+
},
267+
],
268+
},
269+
},
270+
]
271+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default [
7373
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. || 🔧 | 💡 |
7474
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames. || | |
7575
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ ☑️ | | |
76+
| [isolated-functions](docs/rules/isolated-functions.md) | Prevent usage of variables from outside the scope of isolated functions. || | |
7677
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ ☑️ | 🔧 | 💡 |
7778
| [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) | Enforce specifying rules to disable in `eslint-disable` comments. | ✅ ☑️ | | |
7879
| [no-accessor-recursion](docs/rules/no-accessor-recursion.md) | Disallow recursive access to `this` within getters and setters. | ✅ ☑️ | | |

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js';
1616
export {default as 'explicit-length-check'} from './explicit-length-check.js';
1717
export {default as 'filename-case'} from './filename-case.js';
1818
export {default as 'import-style'} from './import-style.js';
19+
export {default as 'isolated-functions'} from './isolated-functions.js';
1920
export {default as 'new-for-builtins'} from './new-for-builtins.js';
2021
export {default as 'no-abusive-eslint-disable'} from './no-abusive-eslint-disable.js';
2122
export {default as 'no-accessor-recursion'} from './no-accessor-recursion.js';

0 commit comments

Comments
 (0)