Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5b2ebcd

Browse files
feat(eslint-plugin): [await-thenable] report unnecessaryawait using statements (typescript-eslint#10209)
* [await-thenable] report unnecessary await using statements* continue, not return* await using without init* disable lint error* exempt code ticks from upper-case heading testCo-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>---------Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
1 parent22f7f25 commit5b2ebcd

File tree

8 files changed

+375
-45
lines changed

8 files changed

+375
-45
lines changed

‎packages/eslint-plugin/docs/rules/await-thenable.mdx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ This rule also inspects [`for await...of` statements](https://developer.mozilla.
4949
While`for await...of` can be used with synchronous iterables, and it will await each promise produced by the iterable, it is inadvisable to do so.
5050
There are some tiny nuances that you may want to consider.
5151

52-
The biggest difference between using`for await...of` and using`for...of` (plus awaiting each result yourself) is error handling.
52+
The biggest difference between using`for await...of` and using`for...of` (apart from awaiting each result yourself) is error handling.
5353
When an error occurs within the loop body,`for await...of` does_not_ close the original sync iterable, while`for...of` does.
5454
For detailed examples of this, see the[MDN documentation on using`for await...of` with sync-iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_sync_iterables_and_generators).
5555

@@ -120,6 +120,62 @@ async function validUseOfForAwaitOnAsyncIterable() {
120120
</TabItem>
121121
</Tabs>
122122

123+
##Explicit Resource Management (`await using` Statements)
124+
125+
This rule also inspects[`await using` statements](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management).
126+
If the disposable being used is not async-disposable, an`await using` statement is unnecessary.
127+
128+
###Examples
129+
130+
<Tabs>
131+
<TabItemvalue="❌ Incorrect">
132+
133+
```ts
134+
function makeSyncDisposable():Disposable {
135+
return {
136+
[Symbol.dispose]():void {
137+
// Dispose of the resource
138+
},
139+
};
140+
}
141+
142+
asyncfunction shouldNotAwait() {
143+
awaitusingresource=makeSyncDisposable();
144+
}
145+
```
146+
147+
</TabItem>
148+
<TabItemvalue="✅ Correct">
149+
150+
```ts
151+
function makeSyncDisposable():Disposable {
152+
return {
153+
[Symbol.dispose]():void {
154+
// Dispose of the resource
155+
},
156+
};
157+
}
158+
159+
asyncfunction shouldNotAwait() {
160+
usingresource=makeSyncDisposable();
161+
}
162+
163+
function makeAsyncDisposable():AsyncDisposable {
164+
return {
165+
async [Symbol.asyncDispose]():Promise<void> {
166+
// Dispose of the resource asynchronously
167+
},
168+
};
169+
}
170+
171+
asyncfunction shouldAwait() {
172+
awaitusingresource=makeAsyncDisposable();
173+
}
174+
```
175+
176+
</TabItem>
177+
</Tabs>
178+
123179
##When Not To Use It
124180

125181
If you want to allow code to`await` non-Promise values.

‎packages/eslint-plugin/src/rules/await-thenable.ts

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as tsutils from 'ts-api-utils';
44

55
import{
66
createRule,
7+
getFixOrSuggest,
78
getParserServices,
89
isAwaitKeyword,
910
isTypeAnyType,
@@ -15,8 +16,9 @@ import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc';
1516

1617
typeMessageId=
1718
|'await'
19+
|'awaitUsingOfNonAsyncDisposable'
1820
|'convertToOrdinaryFor'
19-
|'forAwaitOfNonThenable'
21+
|'forAwaitOfNonAsyncIterable'
2022
|'removeAwait';
2123

2224
exportdefaultcreateRule<[],MessageId>({
@@ -31,8 +33,10 @@ export default createRule<[], MessageId>({
3133
hasSuggestions:true,
3234
messages:{
3335
await:'Unexpected `await` of a non-Promise (non-"Thenable") value.',
36+
awaitUsingOfNonAsyncDisposable:
37+
'Unexpected `await using` of a value that is not async disposable.',
3438
convertToOrdinaryFor:'Convert to an ordinary `for...of` loop.',
35-
forAwaitOfNonThenable:
39+
forAwaitOfNonAsyncIterable:
3640
'Unexpected `for await...of` of a value that is not async iterable.',
3741
removeAwait:'Remove unnecessary `await`.',
3842
},
@@ -80,21 +84,21 @@ export default createRule<[], MessageId>({
8084
return;
8185
}
8286

83-
constasyncIteratorSymbol=tsutils
87+
consthasAsyncIteratorSymbol=tsutils
8488
.unionTypeParts(type)
85-
.map(t=>
86-
tsutils.getWellKnownSymbolPropertyOfType(
87-
t,
88-
'asyncIterator',
89-
checker,
90-
),
91-
)
92-
.find(symbol=>symbol!=null);
93-
94-
if(asyncIteratorSymbol==null){
89+
.some(
90+
typePart=>
91+
tsutils.getWellKnownSymbolPropertyOfType(
92+
typePart,
93+
'asyncIterator',
94+
checker,
95+
)!=null,
96+
);
97+
98+
if(!hasAsyncIteratorSymbol){
9599
context.report({
96100
loc:getForStatementHeadLoc(context.sourceCode,node),
97-
messageId:'forAwaitOfNonThenable',
101+
messageId:'forAwaitOfNonAsyncIterable',
98102
suggest:[
99103
// Note that this suggestion causes broken code for sync iterables
100104
// of promises, since the loop variable is not awaited.
@@ -112,6 +116,57 @@ export default createRule<[], MessageId>({
112116
});
113117
}
114118
},
119+
120+
'VariableDeclaration[kind="await using"]'(
121+
node:TSESTree.VariableDeclaration,
122+
):void{
123+
for(constdeclaratorofnode.declarations){
124+
constinit=declarator.init;
125+
if(init==null){
126+
continue;
127+
}
128+
consttype=services.getTypeAtLocation(init);
129+
if(isTypeAnyType(type)){
130+
continue;
131+
}
132+
133+
consthasAsyncDisposeSymbol=tsutils
134+
.unionTypeParts(type)
135+
.some(
136+
typePart=>
137+
tsutils.getWellKnownSymbolPropertyOfType(
138+
typePart,
139+
'asyncDispose',
140+
checker,
141+
)!=null,
142+
);
143+
144+
if(!hasAsyncDisposeSymbol){
145+
context.report({
146+
node:init,
147+
messageId:'awaitUsingOfNonAsyncDisposable',
148+
// let the user figure out what to do if there's
149+
// await using a = b, c = d, e = f;
150+
// it's rare and not worth the complexity to handle.
151+
...getFixOrSuggest({
152+
fixOrSuggest:
153+
node.declarations.length===1 ?'suggest' :'none',
154+
155+
suggestion:{
156+
messageId:'removeAwait',
157+
fix(fixer):TSESLint.RuleFix{
158+
constawaitToken=nullThrows(
159+
context.sourceCode.getFirstToken(node,isAwaitKeyword),
160+
NullThrowsReasons.MissingToken('await','await using'),
161+
);
162+
returnfixer.remove(awaitToken);
163+
},
164+
},
165+
}),
166+
});
167+
}
168+
}
169+
},
115170
};
116171
},
117172
});

‎packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,11 +413,11 @@ function getReportDescriptor(
413413
},
414414
messageId:'preferOptionalChain',
415415
...getFixOrSuggest({
416+
fixOrSuggest:useSuggestionFixer ?'suggest' :'fix',
416417
suggestion:{
417418
fix,
418419
messageId:'optionalChainSuggest',
419420
},
420-
useFix:!useSuggestionFixer,
421421
}),
422422
};
423423

‎packages/eslint-plugin/src/rules/return-await.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as ts from 'typescript';
66

77
import{
88
createRule,
9+
getFixOrSuggest,
910
getParserServices,
1011
isAwaitExpression,
1112
isAwaitKeyword,
@@ -44,6 +45,7 @@ export default createRule({
4445
requiresTypeChecking:true,
4546
},
4647
fixable:'code',
48+
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- suggestions are exposed through a helper.
4749
hasSuggestions:true,
4850
messages:{
4951
disallowedPromiseAwait:
@@ -340,14 +342,17 @@ export default createRule({
340342
context.report({
341343
node,
342344
messageId:'requiredPromiseAwait',
343-
...fixOrSuggest(useAutoFix,{
344-
messageId:'requiredPromiseAwaitSuggestion',
345-
fix:fixer=>
346-
insertAwait(
347-
fixer,
348-
node,
349-
isHigherPrecedenceThanAwait(expression),
350-
),
345+
...getFixOrSuggest({
346+
fixOrSuggest:useAutoFix ?'fix' :'suggest',
347+
suggestion:{
348+
messageId:'requiredPromiseAwaitSuggestion',
349+
fix:fixer=>
350+
insertAwait(
351+
fixer,
352+
node,
353+
isHigherPrecedenceThanAwait(expression),
354+
),
355+
},
351356
}),
352357
});
353358
}
@@ -359,9 +364,12 @@ export default createRule({
359364
context.report({
360365
node,
361366
messageId:'disallowedPromiseAwait',
362-
...fixOrSuggest(useAutoFix,{
363-
messageId:'disallowedPromiseAwaitSuggestion',
364-
fix:fixer=>removeAwait(fixer,node),
367+
...getFixOrSuggest({
368+
fixOrSuggest:useAutoFix ?'fix' :'suggest',
369+
suggestion:{
370+
messageId:'disallowedPromiseAwaitSuggestion',
371+
fix:fixer=>removeAwait(fixer,node),
372+
},
365373
}),
366374
});
367375
}
@@ -446,12 +454,3 @@ function getConfiguration(option: Option): RuleConfiguration {
446454
};
447455
}
448456
}
449-
450-
functionfixOrSuggest<MessageIdextendsstring>(
451-
useFix:boolean,
452-
suggestion:TSESLint.SuggestionReportDescriptor<MessageId>,
453-
):
454-
|{fix:TSESLint.ReportFixFunction}
455-
|{suggest:TSESLint.SuggestionReportDescriptor<MessageId>[]}{
456-
returnuseFix ?{fix:suggestion.fix} :{suggest:[suggestion]};
457-
}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
importtype{TSESLint}from'@typescript-eslint/utils';
22

33
exportfunctiongetFixOrSuggest<MessageIdextendsstring>({
4+
fixOrSuggest,
45
suggestion,
5-
useFix,
66
}:{
7+
fixOrSuggest:'fix'|'none'|'suggest';
78
suggestion:TSESLint.SuggestionReportDescriptor<MessageId>;
8-
useFix:boolean;
99
}):
1010
|{fix:TSESLint.ReportFixFunction}
11-
|{suggest:TSESLint.SuggestionReportDescriptor<MessageId>[]}{
12-
returnuseFix ?{fix:suggestion.fix} :{suggest:[suggestion]};
11+
|{suggest:TSESLint.SuggestionReportDescriptor<MessageId>[]}
12+
|undefined{
13+
switch(fixOrSuggest){
14+
case'fix':
15+
return{fix:suggestion.fix};
16+
case'none':
17+
returnundefined;
18+
case'suggest':
19+
return{suggest:[suggestion]};
20+
}
1321
}

‎packages/eslint-plugin/tests/docs-eslint-output-snapshots/await-thenable.shot

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎packages/eslint-plugin/tests/docs.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,10 @@ describe('Validating rule docs', () => {
252252
// Get all H2 headings objects as the other levels are variable by design.
253253
constheadings=tokens.filter(tokenIsH2);
254254

255-
headings.forEach(heading=>
256-
expect(heading.text).toBe(titleCase(heading.text)),
257-
);
255+
headings.forEach(heading=>{
256+
constnonCodeText=heading.text.replace(/`[^`]*`/g,'');
257+
expect(nonCodeText).toBe(titleCase(nonCodeText));
258+
});
258259
});
259260

260261
constheadings=tokens.filter(tokenIsHeading);

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp