|
9 | 9 | getFirst, |
10 | 10 | getLast, |
11 | 11 | getNodeIndent, |
| 12 | + getCallee, |
12 | 13 | isInHandledContext, |
13 | 14 | isInAsyncHandledContext, |
14 | 15 | isNodeReturned, |
| 16 | + isGeneratorLike, |
15 | 17 | isPromiseType, |
16 | 18 | isPromiseConstructorCallbackNode, |
17 | 19 | isThenableCallbackNode, |
@@ -248,6 +250,62 @@ module.exports = createRule({ |
248 | 250 | } |
249 | 251 | }; |
250 | 252 |
|
| 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 | + |
251 | 309 | /** @param {import('@typescript-eslint/utils').TSESTree.FunctionLike} node */ |
252 | 310 | const visitFunctionOnExit = (node) => { |
253 | 311 | if (visitedFunctionNodes.has(getNodeID(node))) return; |
@@ -759,6 +817,99 @@ module.exports = createRule({ |
759 | 817 | 'CallExpression[callee.property.name=/^(then|finally)$/] > MemberExpression:first-child:exit': |
760 | 818 | visitPromiseCallbackOnExit, |
761 | 819 |
|
| 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 | + |
762 | 913 | /** |
763 | 914 | * Process collected types when each function node exits |
764 | 915 | */ |
|
0 commit comments