Skip to content

Commit 9a1e347

Browse files
authored
Merge pull request #28 from Xvezda/feature/edgecases
feature/edgecases
2 parents 5e612a6 + a25ee78 commit 9a1e347

File tree

2 files changed

+161
-27
lines changed

2 files changed

+161
-27
lines changed

src/rules/no-undocumented-throws.js

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
findNodeToComment,
2020
findIdentifierDeclaration,
2121
createInsertJSDocBeforeFixer,
22+
toFlattenedTypeArray,
2223
} = require('../utils');
2324

2425

@@ -79,16 +80,19 @@ module.exports = createRule({
7980
if (!throwStatementNodes) return;
8081

8182
/** @type {import('typescript').Type[]} */
82-
const throwTypes = throwStatementNodes
83-
.map(n => {
84-
const type = services.getTypeAtLocation(n.argument);
85-
const tsNode = services.esTreeNodeToTSNodeMap.get(n.argument);
86-
87-
return options.useBaseTypeOfLiteral && ts.isLiteralTypeLiteral(tsNode)
88-
? checker.getBaseTypeOfLiteralType(type)
89-
: type;
90-
})
91-
.flatMap(t => t.isUnion() ? t.types : t);
83+
const throwTypes =
84+
toFlattenedTypeArray(
85+
throwStatementNodes
86+
.map(n => {
87+
const type = services.getTypeAtLocation(n.argument);
88+
const tsNode = services.esTreeNodeToTSNodeMap.get(n.argument);
89+
90+
return options.useBaseTypeOfLiteral && ts.isLiteralTypeLiteral(tsNode)
91+
? checker.getBaseTypeOfLiteralType(type)
92+
: type;
93+
})
94+
)
95+
.map(t => checker.getAwaitedType(t) ?? t);
9296

9397
if (hasJSDocThrowsTag(sourceCode, nodeToComment)) {
9498
if (!services.esTreeNodeToTSNodeMap.has(nodeToComment)) return;
@@ -103,8 +107,7 @@ module.exports = createRule({
103107
if (!throwsTagTypeNodes.length) return;
104108

105109
const throwsTagTypes = getJSDocThrowsTagTypes(checker, functionDeclarationTSNode)
106-
.map(t => node.async ? checker.getAwaitedType(t) : t)
107-
.filter(t => !!t);
110+
.map(t => checker.getAwaitedType(t) ?? t);
108111

109112
const typeGroups = groupTypesByCompatibility(
110113
services.program,
@@ -188,12 +191,7 @@ module.exports = createRule({
188191
if (!utils.isPromiseConstructorLike(services.program, calleeType)) {
189192
return;
190193
}
191-
192-
const nodeToComment = findNodeToComment(functionDeclaration);
193-
if (!nodeToComment) return;
194194

195-
if (hasJSDocThrowsTag(sourceCode, nodeToComment)) return;
196-
197195
if (!node.arguments.length) return;
198196

199197
const firstArg = getFirst(node.arguments);
@@ -236,7 +234,9 @@ module.exports = createRule({
236234
const callbackScope = sourceCode.getScope(callbackNode)
237235
if (!callbackScope) return;
238236

239-
const rejectCallbackRefs = callbackScope.set.get(rejectCallbackNode.name)?.references;
237+
const rejectCallbackRefs =
238+
callbackScope.set.get(rejectCallbackNode.name)?.references;
239+
240240
if (!rejectCallbackRefs) return;
241241

242242
const callRefs = rejectCallbackRefs
@@ -250,29 +250,25 @@ module.exports = createRule({
250250
const argumentTypes = callRefs
251251
.map(ref => services.getTypeAtLocation(ref.arguments[0]));
252252

253-
rejectTypes.push(
254-
...argumentTypes.flatMap(t => t.isUnion() ? t.types : t)
255-
);
253+
rejectTypes.push(...toFlattenedTypeArray(argumentTypes));
256254
}
257255

258256
if (throwStatements.has(getNodeID(callbackNode))) {
259257
const throwStatementTypes = throwStatements.get(getNodeID(callbackNode))
260258
?.map(n => services.getTypeAtLocation(n.argument));
261259

262260
if (throwStatementTypes) {
263-
rejectTypes.push(
264-
...throwStatementTypes.flatMap(t => t.isUnion() ? t.types : t)
265-
);
261+
rejectTypes.push(...toFlattenedTypeArray(throwStatementTypes));
266262
}
267263
}
268264

269-
const throwsTagTypes = getJSDocThrowsTagTypes(
265+
const callbackThrowsTagTypes = getJSDocThrowsTagTypes(
270266
checker,
271267
services.esTreeNodeToTSNodeMap.get(callbackNode)
272268
);
273269

274-
if (throwsTagTypes.length) {
275-
rejectTypes.push(...throwsTagTypes);
270+
if (callbackThrowsTagTypes.length) {
271+
rejectTypes.push(...callbackThrowsTagTypes);
276272
}
277273

278274
if (!rejectTypes.length) return;
@@ -291,6 +287,53 @@ module.exports = createRule({
291287

292288
if (isRejectHandled) return;
293289

290+
const nodeToComment = findNodeToComment(functionDeclaration);
291+
if (!nodeToComment) return;
292+
293+
if (hasJSDocThrowsTag(sourceCode, nodeToComment)) {
294+
if (!services.esTreeNodeToTSNodeMap.has(nodeToComment)) return;
295+
296+
const functionDeclarationTSNode = services.esTreeNodeToTSNodeMap.get(functionDeclaration);
297+
298+
const throwsTags = getJSDocThrowsTags(functionDeclarationTSNode);
299+
const throwsTagTypeNodes = throwsTags
300+
.map(tag => tag.typeExpression?.type)
301+
.filter(tag => !!tag);
302+
303+
if (!throwsTagTypeNodes.length) return;
304+
305+
// Throws tag with `Promise<...>` considered as a reject tag
306+
const rejectTagTypes = toFlattenedTypeArray(
307+
getJSDocThrowsTagTypes(checker, functionDeclarationTSNode)
308+
.filter(t =>
309+
utils.isPromiseLike(services.program, t) &&
310+
t.symbol.getName() === 'Promise'
311+
)
312+
.map(t => checker.getAwaitedType(t) ?? t)
313+
);
314+
315+
const typeGroups = groupTypesByCompatibility(
316+
services.program,
317+
rejectTypes,
318+
rejectTagTypes,
319+
);
320+
if (!typeGroups.incompatible) return;
321+
322+
const lastTagtypeNode = getLast(throwsTagTypeNodes);
323+
if (!lastTagtypeNode) return;
324+
325+
context.report({
326+
node,
327+
messageId: 'throwTypeMismatch',
328+
fix(fixer) {
329+
return fixer.replaceTextRange(
330+
[lastTagtypeNode.pos, lastTagtypeNode.end],
331+
`Promise<${typesToUnionString(checker, rejectTypes)}>`,
332+
);
333+
},
334+
});
335+
return;
336+
}
294337
context.report({
295338
node,
296339
messageId: 'missingThrowsTag',

src/rules/no-undocumented-throws.test.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,39 @@ ruleTester.run(
252252
}
253253
`,
254254
},
255+
{
256+
code: `
257+
/**
258+
* @throws {Promise<TypeError | RangeError>}
259+
*/
260+
function foo() {
261+
return new Promise((resolve, reject) => {
262+
if (Math.random() > 0.5) {
263+
reject(new TypeError());
264+
} else {
265+
reject(new RangeError());
266+
}
267+
});
268+
}
269+
`,
270+
},
271+
{
272+
code: `
273+
/**
274+
* @throws {Promise<TypeError>}
275+
* @throws {Promise<RangeError>}
276+
*/
277+
function foo() {
278+
return new Promise((resolve, reject) => {
279+
if (Math.random() > 0.5) {
280+
reject(new TypeError());
281+
} else {
282+
reject(new RangeError());
283+
}
284+
});
285+
}
286+
`,
287+
},
255288
],
256289
invalid: [
257290
{
@@ -989,6 +1022,64 @@ ruleTester.run(
9891022
{ messageId: 'missingThrowsTag' },
9901023
],
9911024
},
1025+
{
1026+
code: `
1027+
/**
1028+
* @throws {Error}
1029+
*/
1030+
function foo() {
1031+
return new Promise((resolve, reject) => {
1032+
reject(new Error());
1033+
});
1034+
}
1035+
`,
1036+
output: `
1037+
/**
1038+
* @throws {Promise<Error>}
1039+
*/
1040+
function foo() {
1041+
return new Promise((resolve, reject) => {
1042+
reject(new Error());
1043+
});
1044+
}
1045+
`,
1046+
errors: [
1047+
{ messageId: 'throwTypeMismatch' },
1048+
],
1049+
},
1050+
{
1051+
code: `
1052+
/**
1053+
* @throws {Promise<TypeError>}
1054+
*/
1055+
function foo() {
1056+
return new Promise((resolve, reject) => {
1057+
if (Math.random() > 0.5) {
1058+
reject(new TypeError());
1059+
} else {
1060+
reject(new RangeError());
1061+
}
1062+
});
1063+
}
1064+
`,
1065+
output: `
1066+
/**
1067+
* @throws {Promise<TypeError | RangeError>}
1068+
*/
1069+
function foo() {
1070+
return new Promise((resolve, reject) => {
1071+
if (Math.random() > 0.5) {
1072+
reject(new TypeError());
1073+
} else {
1074+
reject(new RangeError());
1075+
}
1076+
});
1077+
}
1078+
`,
1079+
errors: [
1080+
{ messageId: 'throwTypeMismatch' },
1081+
],
1082+
},
9921083
],
9931084
},
9941085
);

0 commit comments

Comments
 (0)