Skip to content

Commit 1245000

Browse files
authored
feat: support explicit resource management in core rules (#19828)
1 parent e855717 commit 1245000

18 files changed

+347
-5
lines changed

docs/src/rules/no-await-in-loop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ async function foo(things) {
111111
}
112112
return results;
113113
}
114+
115+
async function bar(things) {
116+
for (const thing of things) {
117+
await using resource = getAsyncResource(thing);
118+
}
119+
}
114120
```
115121

116122
:::

docs/src/rules/no-unused-vars.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ By default this rule is enabled with `all` option for caught errors and variable
142142
"args": "after-used",
143143
"caughtErrors": "all",
144144
"ignoreRestSiblings": false,
145+
"ignoreUsingDeclarations": false,
145146
"reportUsedIgnorePattern": false
146147
}]
147148
}
@@ -461,6 +462,34 @@ class Foo {
461462

462463
:::
463464

465+
### ignoreUsingDeclarations
466+
467+
The `ignoreUsingDeclarations` option is a boolean (default: `false`). Explicit resource management allows automatic teardown of disposables by calling `Symbol.dispose` or `Symbol.asyncDispose` method implicitly at the end of the variable's scope. When this option is set to `true`, this rule ignores variables declared with `using` or `await using`.
468+
469+
Examples of **incorrect** code for the `{ "ignoreUsingDeclarations": true }` option:
470+
471+
::: incorrect
472+
473+
```js
474+
/*eslint no-unused-vars: ["error", { "ignoreUsingDeclarations": true }]*/
475+
const resource = getResource();
476+
```
477+
478+
:::
479+
480+
Examples of **correct** code for the `{ "ignoreUsingDeclarations": true }` option:
481+
482+
::: correct
483+
484+
```js
485+
/*eslint no-unused-vars: ["error", { "ignoreUsingDeclarations": true }]*/
486+
487+
using syncResource = getSyncResource();
488+
await using asyncResource = getAsyncResource();
489+
```
490+
491+
:::
492+
464493
### reportUsedIgnorePattern
465494

466495
The `reportUsedIgnorePattern` option is a boolean (default: `false`).

docs/src/rules/require-await.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ bar(() => {
7171
doSomething();
7272
});
7373

74+
async function resourceManagement() {
75+
await using resource = getAsyncResource();
76+
resource.use();
77+
}
78+
7479
// Allow empty functions.
7580
async function noop() {}
7681
```

lib/rules/no-await-in-loop.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ function isLooped(node, parent) {
4141

4242
case "ForOfStatement":
4343
case "ForInStatement":
44-
return node === parent.body;
44+
return (
45+
node === parent.body ||
46+
(node === parent.left && node.kind === "await using")
47+
);
4548

4649
case "WhileStatement":
4750
case "DoWhileStatement":
@@ -76,6 +79,13 @@ module.exports = {
7679
* @returns {void}
7780
*/
7881
function validate(awaitNode) {
82+
if (
83+
awaitNode.type === "VariableDeclaration" &&
84+
awaitNode.kind !== "await using"
85+
) {
86+
return;
87+
}
88+
7989
if (awaitNode.type === "ForOfStatement" && !awaitNode.await) {
8090
return;
8191
}
@@ -99,6 +109,7 @@ module.exports = {
99109
return {
100110
AwaitExpression: validate,
101111
ForOfStatement: validate,
112+
VariableDeclaration: validate,
102113
};
103114
},
104115
};

lib/rules/no-unused-vars.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ module.exports = {
8888
ignoreClassWithStaticInitBlock: {
8989
type: "boolean",
9090
},
91+
ignoreUsingDeclarations: {
92+
type: "boolean",
93+
},
9194
reportUsedIgnorePattern: {
9295
type: "boolean",
9396
},
@@ -119,6 +122,7 @@ module.exports = {
119122
ignoreRestSiblings: false,
120123
caughtErrors: "all",
121124
ignoreClassWithStaticInitBlock: false,
125+
ignoreUsingDeclarations: false,
122126
reportUsedIgnorePattern: false,
123127
};
124128

@@ -137,6 +141,9 @@ module.exports = {
137141
config.ignoreClassWithStaticInitBlock =
138142
firstOption.ignoreClassWithStaticInitBlock ||
139143
config.ignoreClassWithStaticInitBlock;
144+
config.ignoreUsingDeclarations =
145+
firstOption.ignoreUsingDeclarations ||
146+
config.ignoreUsingDeclarations;
140147
config.reportUsedIgnorePattern =
141148
firstOption.reportUsedIgnorePattern ||
142149
config.reportUsedIgnorePattern;
@@ -357,6 +364,22 @@ module.exports = {
357364
return false;
358365
}
359366

367+
/**
368+
* Determines if a given variable uses the explicit resource management protocol.
369+
* @param {Variable} variable eslint-scope variable object.
370+
* @returns {boolean} True if the variable is declared with "using" or "await using"
371+
* @private
372+
*/
373+
function usesExplicitResourceManagement(variable) {
374+
const [definition] = variable.defs;
375+
376+
return (
377+
definition?.type === "Variable" &&
378+
(definition.parent.kind === "using" ||
379+
definition.parent.kind === "await using")
380+
);
381+
}
382+
360383
/**
361384
* Checks whether a node is a sibling of the rest property or not.
362385
* @param {ASTNode} node a node to check
@@ -922,6 +945,10 @@ module.exports = {
922945
if (
923946
!isUsedVariable(variable) &&
924947
!isExported(variable) &&
948+
!(
949+
config.ignoreUsingDeclarations &&
950+
usesExplicitResourceManagement(variable)
951+
) &&
925952
!hasRestSpreadSibling(variable)
926953
) {
927954
unusedVars.push(variable);

lib/rules/prefer-destructuring.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ module.exports = {
293293
return;
294294
}
295295

296+
// Variable declarations using explicit resource management cannot use destructuring (parse error)
297+
if (
298+
node.parent.kind === "using" ||
299+
node.parent.kind === "await using"
300+
) {
301+
return;
302+
}
303+
296304
// We only care about member expressions past this point
297305
if (node.init.type !== "MemberExpression") {
298306
return;

lib/rules/require-await.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ module.exports = {
170170
scopeInfo.hasAwait = true;
171171
}
172172
},
173+
VariableDeclaration(node) {
174+
if (!scopeInfo) {
175+
return;
176+
}
177+
178+
if (node.kind === "await using") {
179+
scopeInfo.hasAwait = true;
180+
}
181+
},
173182
};
174183
},
175184
};

lib/rules/utils/ast-utils.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ const STATEMENT_LIST_PARENTS = new Set([
4747
"StaticBlock",
4848
"SwitchCase",
4949
]);
50+
const LEXICAL_DECLARATION_KINDS = new Set([
51+
"let",
52+
"const",
53+
"using",
54+
"await using",
55+
]);
5056

5157
const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
5258

@@ -2589,16 +2595,14 @@ module.exports = {
25892595
*/
25902596
areBracesNecessary(node, sourceCode) {
25912597
/**
2592-
* Determines if the given node is a lexical declaration (let, const, function, or class)
2598+
* Determines if the given node is a lexical declaration (let, const, using, await using, function, or class)
25932599
* @param {ASTNode} nodeToCheck The node to check
25942600
* @returns {boolean} True if the node is a lexical declaration
25952601
* @private
25962602
*/
25972603
function isLexicalDeclaration(nodeToCheck) {
25982604
if (nodeToCheck.type === "VariableDeclaration") {
2599-
return (
2600-
nodeToCheck.kind === "const" || nodeToCheck.kind === "let"
2601-
);
2605+
return LEXICAL_DECLARATION_KINDS.has(nodeToCheck.kind);
26022606
}
26032607

26042608
return (

lib/types/rules.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4078,6 +4078,10 @@ export interface ESLintRules extends Linter.RulesRecord {
40784078
* @default false
40794079
*/
40804080
ignoreClassWithStaticInitBlock: boolean;
4081+
/**
4082+
* @default false
4083+
*/
4084+
ignoreUsingDeclarations: boolean;
40814085
/**
40824086
* @default false
40834087
*/

tests/lib/rules/curly.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,22 @@ ruleTester.run("curly", rule, {
174174
options: ["multi"],
175175
languageOptions: { ecmaVersion: 6 },
176176
},
177+
{
178+
code: "if (foo) { using bar = 'baz'; }",
179+
options: ["multi"],
180+
languageOptions: {
181+
sourceType: "module",
182+
ecmaVersion: 2026,
183+
},
184+
},
185+
{
186+
code: "if (foo) { await using bar = 'baz'; }",
187+
options: ["multi"],
188+
languageOptions: {
189+
sourceType: "module",
190+
ecmaVersion: 2026,
191+
},
192+
},
177193
{
178194
code: "while (foo) { let bar = 'baz'; }",
179195
options: ["multi"],

0 commit comments

Comments
 (0)