Skip to content

Commit 5acbce0

Browse files
authored
Merge pull request #128 from Xvezda/feature/handle-async-generator
Handle async generator
2 parents 7c7722b + 874030e commit 5acbce0

File tree

5 files changed

+1011
-23
lines changed

5 files changed

+1011
-23
lines changed

src/rules/check-throws-tag-type.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ const {
99
getFirst,
1010
getLast,
1111
getNodeIndent,
12+
getCallee,
1213
isInHandledContext,
1314
isInAsyncHandledContext,
1415
isNodeReturned,
16+
isGeneratorLike,
1517
isPromiseType,
1618
isPromiseConstructorCallbackNode,
1719
isThenableCallbackNode,
@@ -248,6 +250,62 @@ module.exports = createRule({
248250
}
249251
};
250252

253+
/**
254+
* Visit iterable node and collect types.
255+
*
256+
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
257+
*/
258+
const visitIterableNode = (node) => {
259+
const iterableType = services.getTypeAtLocation(node);
260+
if (!isGeneratorLike(iterableType)) return;
261+
262+
const callerDeclaration = findClosestFunctionNode(node);
263+
if (!callerDeclaration) return;
264+
265+
const calleeNode = getCallee(node);
266+
if (!calleeNode) return;
267+
268+
// TODO: Extract duplicated logic of extracting narrowed type declaration
269+
const calleeDeclaration =
270+
(calleeNode.type === AST_NODE_TYPES.CallExpression ||
271+
calleeNode.type === AST_NODE_TYPES.NewExpression)
272+
? getCallSignatureDeclaration(services, calleeNode)
273+
: calleeNode.parent?.type === AST_NODE_TYPES.CallExpression
274+
? getCallSignatureDeclaration(services, calleeNode.parent)
275+
: getCalleeDeclaration(
276+
services,
277+
/** @type {import('@typescript-eslint/utils').TSESTree.Expression} */
278+
(calleeNode)
279+
);
280+
281+
if (!calleeDeclaration) return;
282+
283+
const calleeThrowsTypes = getJSDocThrowsTagTypes(checker, calleeDeclaration);
284+
if (!calleeThrowsTypes.length) return;
285+
286+
for (const type of calleeThrowsTypes) {
287+
if (isPromiseType(services, type)) {
288+
if (isInAsyncHandledContext(sourceCode, node)) continue;
289+
290+
const flattened =
291+
toFlattenedTypeArray([checker.getAwaitedType(type) ?? type]);
292+
293+
rejectTypes.add(callerDeclaration, flattened);
294+
295+
flattened
296+
.forEach(t => metadata.set(t, { pos: node.range[0] }));
297+
} else {
298+
if (isInHandledContext(node)) continue;
299+
const flattened = toFlattenedTypeArray([type]);
300+
301+
throwTypes.add(callerDeclaration, flattened);
302+
303+
flattened
304+
.forEach(t => metadata.set(t, { pos: node.range[0] }));
305+
}
306+
}
307+
};
308+
251309
/** @param {import('@typescript-eslint/utils').TSESTree.FunctionLike} node */
252310
const visitFunctionOnExit = (node) => {
253311
if (visitedFunctionNodes.has(getNodeID(node))) return;
@@ -759,6 +817,99 @@ module.exports = createRule({
759817
'CallExpression[callee.property.name=/^(then|finally)$/] > MemberExpression:first-child:exit':
760818
visitPromiseCallbackOnExit,
761819

820+
/**
821+
* Collect throwable types of generators
822+
*/
823+
/**
824+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of MDN}
825+
* @example
826+
* ```
827+
* for (const item of iterable) { ... }
828+
* // ^ this
829+
* ```
830+
*/
831+
'ForOfStatement'(node) {
832+
visitIterableNode(node.right);
833+
},
834+
/**
835+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax MDN}
836+
* @example
837+
* ```
838+
* [...iterable]
839+
* // ^ this
840+
* ```
841+
*/
842+
'SpreadElement'(node) {
843+
visitIterableNode(node.argument);
844+
},
845+
/**
846+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from MDN}
847+
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
848+
* @example
849+
* ```
850+
* Array.from(iterable)
851+
* // ^ this
852+
* ```
853+
*/
854+
'CallExpression:has(> MemberExpression[object.name="Array"][property.name="from"])'(node) {
855+
if (node.arguments.length < 1) return;
856+
857+
const [firstArgumentNode] = node.arguments;
858+
859+
visitIterableNode(firstArgumentNode);
860+
},
861+
/**
862+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync MDN}
863+
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
864+
* @example
865+
* ```
866+
* await Array.fromAsync(iterable)
867+
* // ^ this
868+
* ```
869+
*/
870+
'CallExpression:has(> MemberExpression[object.name="Array"][property.name="fromAsync"])'(node) {
871+
if (node.arguments.length < 1) return;
872+
873+
const [firstArgumentNode] = node.arguments;
874+
875+
visitIterableNode(firstArgumentNode);
876+
},
877+
/**
878+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield* MDN}
879+
* @param {import('@typescript-eslint/utils').TSESTree.YieldExpression} node
880+
* @example
881+
* ```
882+
* function* gen() {
883+
* yield* iterable;
884+
* // ^ this
885+
* }
886+
* // or
887+
* async function* asyncGen() {
888+
* yield* iterable;
889+
* // ^ this
890+
* }
891+
* ```
892+
*/
893+
'YieldExpression[delegate=true]'(node) {
894+
if (!node.argument) return;
895+
896+
visitIterableNode(node.argument);
897+
},
898+
/**
899+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next MDN}
900+
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
901+
* @example
902+
* ```
903+
* iterable.next();
904+
* // ^ this
905+
* ```
906+
*/
907+
'CallExpression[callee.property.name="next"]'(node) {
908+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return;
909+
910+
visitIterableNode(node.callee.object);
911+
},
912+
762913
/**
763914
* Process collected types when each function node exits
764915
*/

src/rules/no-undocumented-throws.js

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ module.exports = createRule({
163163
/** @type {import('typescript').JSDocThrowsTag[]} */
164164
const comments = [];
165165
comments.push(...getJSDocThrowsTags(calleeDeclaration));
166+
throwsComments.set(getNodeID(callerDeclaration), comments);
166167

167168
const calleeThrowsTypes = getJSDocThrowsTagTypes(checker, calleeDeclaration);
168169
for (const type of calleeThrowsTypes) {
@@ -194,8 +195,7 @@ module.exports = createRule({
194195
flattened
195196
.forEach(t => metadata.set(t, { pos: node.range[0] }));
196197
}
197-
};
198-
throwsComments.set(getNodeID(callerDeclaration), comments);
198+
}
199199
};
200200

201201
/**
@@ -204,7 +204,8 @@ module.exports = createRule({
204204
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
205205
*/
206206
const visitIterableNode = (node) => {
207-
if (isInHandledContext(node)) return;
207+
const iterableType = services.getTypeAtLocation(node);
208+
if (!isGeneratorLike(iterableType)) return;
208209

209210
const callerDeclaration = findClosestFunctionNode(node);
210211
if (!callerDeclaration) return;
@@ -227,16 +228,34 @@ module.exports = createRule({
227228

228229
if (!calleeDeclaration) return;
229230

231+
/** @type {import('typescript').JSDocThrowsTag[]} */
232+
const comments = [];
233+
comments.push(...getJSDocThrowsTags(calleeDeclaration));
234+
throwsComments.set(getNodeID(callerDeclaration), comments);
235+
230236
const calleeThrowsTypes = getJSDocThrowsTagTypes(checker, calleeDeclaration);
231237
if (!calleeThrowsTypes.length) return;
232238

233239
for (const type of calleeThrowsTypes) {
234-
const flattened = toFlattenedTypeArray([type]);
240+
if (isPromiseType(services, type)) {
241+
if (isInAsyncHandledContext(sourceCode, node)) continue;
235242

236-
throwTypes.add(callerDeclaration, flattened);
243+
const flattened =
244+
toFlattenedTypeArray([checker.getAwaitedType(type) ?? type]);
237245

238-
flattened
239-
.forEach(t => metadata.set(t, { pos: node.range[0] }));
246+
rejectTypes.add(callerDeclaration, flattened);
247+
248+
flattened
249+
.forEach(t => metadata.set(t, { pos: node.range[0] }));
250+
} else {
251+
if (isInHandledContext(node)) continue;
252+
const flattened = toFlattenedTypeArray([type]);
253+
254+
throwTypes.add(callerDeclaration, flattened);
255+
256+
flattened
257+
.forEach(t => metadata.set(t, { pos: node.range[0] }));
258+
}
240259
}
241260
};
242261

@@ -721,57 +740,91 @@ module.exports = createRule({
721740
*/
722741
/**
723742
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of MDN}
743+
* @example
744+
* ```
745+
* for (const item of iterable) { ... }
746+
* // ^ this
747+
* ```
724748
*/
725749
'ForOfStatement'(node) {
726-
const iterableType = services.getTypeAtLocation(node.right);
727-
if (!isGeneratorLike(iterableType)) return;
728-
729750
visitIterableNode(node.right);
730751
},
731752
/**
732753
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax MDN}
754+
* @example
755+
* ```
756+
* [...iterable]
757+
* // ^ this
758+
* ```
733759
*/
734760
'SpreadElement'(node) {
735-
const iterableType = services.getTypeAtLocation(node.argument);
736-
if (!isGeneratorLike(iterableType)) return;
737-
738761
visitIterableNode(node.argument);
739762
},
740763
/**
741764
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from MDN}
742765
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
766+
* @example
767+
* ```
768+
* Array.from(iterable)
769+
* // ^ this
770+
* ```
743771
*/
744772
'CallExpression:has(> MemberExpression[object.name="Array"][property.name="from"])'(node) {
745773
if (node.arguments.length < 1) return;
746774

747775
const [firstArgumentNode] = node.arguments;
748-
const iterableType = services.getTypeAtLocation(firstArgumentNode);
749-
if (!isGeneratorLike(iterableType)) return;
776+
777+
visitIterableNode(firstArgumentNode);
778+
},
779+
/**
780+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync MDN}
781+
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
782+
* @example
783+
* ```
784+
* await Array.fromAsync(iterable)
785+
* // ^ this
786+
* ```
787+
*/
788+
'CallExpression:has(> MemberExpression[object.name="Array"][property.name="fromAsync"])'(node) {
789+
if (node.arguments.length < 1) return;
790+
791+
const [firstArgumentNode] = node.arguments;
750792

751793
visitIterableNode(firstArgumentNode);
752794
},
753795
/**
754796
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield* MDN}
755797
* @param {import('@typescript-eslint/utils').TSESTree.YieldExpression} node
798+
* @example
799+
* ```
800+
* function* gen() {
801+
* yield* iterable;
802+
* // ^ this
803+
* }
804+
* // or
805+
* async function* asyncGen() {
806+
* yield* iterable;
807+
* // ^ this
808+
* }
809+
* ```
756810
*/
757811
'YieldExpression[delegate=true]'(node) {
758812
if (!node.argument) return;
759813

760-
const iterableType = services.getTypeAtLocation(node.argument);
761-
if (!isGeneratorLike(iterableType)) return;
762-
763814
visitIterableNode(node.argument);
764815
},
765816
/**
766817
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next MDN}
767818
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} node
819+
* @example
820+
* ```
821+
* iterable.next();
822+
* // ^ this
823+
* ```
768824
*/
769825
'CallExpression[callee.property.name="next"]'(node) {
770826
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return;
771827

772-
const iterableType = services.getTypeAtLocation(node.callee.object);
773-
if (!isGeneratorLike(iterableType)) return;
774-
775828
visitIterableNode(node.callee.object);
776829
},
777830

src/utils.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,23 @@ const isCatchMethodCalled = (node) => {
940940
*/
941941
const isAwaitCatchPattern = (node) => {
942942
return (
943-
node.type === AST_NODE_TYPES.AwaitExpression &&
943+
(
944+
node.type === AST_NODE_TYPES.AwaitExpression ||
945+
// yield*
946+
findParent(node, (parent) =>
947+
parent.type === AST_NODE_TYPES.YieldExpression &&
948+
parent.delegate
949+
) ||
950+
// for await
951+
isParentOrAncestor(
952+
node,
953+
/** @type {import('@typescript-eslint/utils').TSESTree.ForOfStatement} */
954+
(findParent(node, (parent) =>
955+
parent.type === AST_NODE_TYPES.ForOfStatement &&
956+
parent.await
957+
))?.right
958+
)
959+
) &&
944960
isParentOrAncestor(
945961
node,
946962
/** @type {import('@typescript-eslint/utils').TSESTree.TryStatement} */

0 commit comments

Comments
 (0)