Skip to content

Commit b8cda6c

Browse files
committed
Fix reject types mismatch
1 parent 04747e0 commit b8cda6c

File tree

2 files changed

+144
-10
lines changed

2 files changed

+144
-10
lines changed

src/rules/no-undocumented-throws.js

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,7 @@ module.exports = createRule({
188188
if (!utils.isPromiseConstructorLike(services.program, calleeType)) {
189189
return;
190190
}
191-
192-
const nodeToComment = findNodeToComment(functionDeclaration);
193-
if (!nodeToComment) return;
194191

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

199194
const firstArg = getFirst(node.arguments);
@@ -224,7 +219,7 @@ module.exports = createRule({
224219
if (!callbackNode) return;
225220

226221
/** @type {import('typescript').Type[]} */
227-
const rejectTypes = [];
222+
let rejectTypes = [];
228223

229224
const isRejectCallbackNameDeclared =
230225
callbackNode.params.length >= 2;
@@ -236,7 +231,9 @@ module.exports = createRule({
236231
const callbackScope = sourceCode.getScope(callbackNode)
237232
if (!callbackScope) return;
238233

239-
const rejectCallbackRefs = callbackScope.set.get(rejectCallbackNode.name)?.references;
234+
const rejectCallbackRefs =
235+
callbackScope.set.get(rejectCallbackNode.name)?.references;
236+
240237
if (!rejectCallbackRefs) return;
241238

242239
const callRefs = rejectCallbackRefs
@@ -266,13 +263,13 @@ module.exports = createRule({
266263
}
267264
}
268265

269-
const throwsTagTypes = getJSDocThrowsTagTypes(
266+
const callbackThrowsTagTypes = getJSDocThrowsTagTypes(
270267
checker,
271268
services.esTreeNodeToTSNodeMap.get(callbackNode)
272269
);
273270

274-
if (throwsTagTypes.length) {
275-
rejectTypes.push(...throwsTagTypes);
271+
if (callbackThrowsTagTypes.length) {
272+
rejectTypes.push(...callbackThrowsTagTypes);
276273
}
277274

278275
if (!rejectTypes.length) return;
@@ -291,6 +288,52 @@ module.exports = createRule({
291288

292289
if (isRejectHandled) return;
293290

291+
const nodeToComment = findNodeToComment(functionDeclaration);
292+
if (!nodeToComment) return;
293+
294+
if (hasJSDocThrowsTag(sourceCode, nodeToComment)) {
295+
if (!services.esTreeNodeToTSNodeMap.has(nodeToComment)) return;
296+
297+
const functionDeclarationTSNode = services.esTreeNodeToTSNodeMap.get(functionDeclaration);
298+
299+
const throwsTags = getJSDocThrowsTags(functionDeclarationTSNode);
300+
const throwsTagTypeNodes = throwsTags
301+
.map(tag => tag.typeExpression?.type)
302+
.filter(tag => !!tag);
303+
304+
if (!throwsTagTypeNodes.length) return;
305+
306+
// Throws tag with `Promise<...>` considered as a reject tag
307+
const rejectTagTypes = 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+
.flatMap(t => t.isUnion() ? t.types : t);
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)