Skip to content

Commit 7d173df

Browse files
authored
Merge pull request #122 from Xvezda/feature/prefer-union
Add `preferUnionType` option
2 parents 08864bf + 0d5b3b3 commit 7d173df

File tree

3 files changed

+473
-39
lines changed

3 files changed

+473
-39
lines changed

docs/rules/no-undocumented-throws.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ Default: `false`
1414

1515
When a literal value is thrown, document its base type rather than the literal type.
1616
For example, for `throw "foo"`, insert `@throws {string}` instead of `@throws {"foo"}`.
17+
18+
### `preferUnionType`
19+
20+
Default: `true`
21+
22+
When more than one exception can be thrown, the types are added as a union type in the comment.
23+
If set to `false`, each exception is written on its own line.

src/rules/no-undocumented-throws.js

Lines changed: 120 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const {
2424
getCalleeDeclaration,
2525
getJSDocThrowsTags,
2626
getJSDocThrowsTagTypes,
27+
getQualifiedTypeName,
2728
findParent,
2829
findClosest,
2930
findClosestFunctionNode,
@@ -54,13 +55,21 @@ module.exports = createRule({
5455
type: 'boolean',
5556
default: false,
5657
},
58+
preferUnionType: {
59+
type: 'boolean',
60+
default: true,
61+
}
5762
},
5863
additionalProperties: false,
5964
},
6065
],
6166
},
6267
defaultOptions: [
63-
{ useBaseTypeOfLiteral: false },
68+
/** @type {{ useBaseTypeOfLiteral?: boolean; preferUnionType?: boolean }} */
69+
({
70+
useBaseTypeOfLiteral: false,
71+
preferUnionType: true,
72+
}),
6473
],
6574

6675
create(context) {
@@ -70,6 +79,7 @@ module.exports = createRule({
7079

7180
const {
7281
useBaseTypeOfLiteral = false,
82+
preferUnionType = true,
7383
} = context.options[0] ?? {};
7484

7585
/** @type {Set<string>} */
@@ -274,45 +284,92 @@ module.exports = createRule({
274284
return;
275285
}
276286

277-
context.report({
278-
node: nodeToComment,
279-
messageId: 'missingThrowsTag',
280-
fix(fixer) {
281-
const newType =
282-
node.async
283-
? `Promise<${
284-
typesToUnionString(
285-
checker,
286-
toSortedByMetadata([
287-
...throwableTypes,
288-
...rejectableTypes,
289-
]),
290-
{ useBaseTypeOfLiteral }
291-
)
292-
}>`
293-
: typeStringsToUnionString([
294-
...throwableTypes.length
295-
? [
296-
typesToUnionString(
297-
checker,
298-
toSortedByMetadata(throwableTypes),
299-
{ useBaseTypeOfLiteral }
300-
)
301-
]
302-
: [],
303-
...rejectableTypes.length
304-
? [
305-
`Promise<${
287+
if (preferUnionType) {
288+
context.report({
289+
node: nodeToComment,
290+
messageId: 'missingThrowsTag',
291+
fix(fixer) {
292+
const newType =
293+
node.async
294+
? `Promise<${
295+
typesToUnionString(
296+
checker,
297+
toSortedByMetadata([
298+
...throwableTypes,
299+
...rejectableTypes,
300+
]),
301+
{ useBaseTypeOfLiteral }
302+
)
303+
}>`
304+
: typeStringsToUnionString([
305+
...throwableTypes.length
306+
? [
306307
typesToUnionString(
307308
checker,
308-
toSortedByMetadata(rejectableTypes),
309+
toSortedByMetadata(throwableTypes),
309310
{ useBaseTypeOfLiteral }
310311
)
311-
}>`
312-
]
313-
: [],
314-
]);
312+
]
313+
: [],
314+
...rejectableTypes.length
315+
? [
316+
`Promise<${
317+
typesToUnionString(
318+
checker,
319+
toSortedByMetadata(rejectableTypes),
320+
{ useBaseTypeOfLiteral }
321+
)
322+
}>`
323+
]
324+
: [],
325+
]);
326+
327+
const indent = getNodeIndent(sourceCode, node);
328+
329+
if (hasJSDoc(sourceCode, nodeToComment)) {
330+
const comments = sourceCode.getCommentsBefore(nodeToComment);
331+
const comment = comments
332+
.find(({ value }) => value.startsWith('*'));
333+
334+
if (comment) {
335+
let newCommentText = sourceCode.getText(comment);
336+
if (!/^\/\*\*[ \t]*\n/.test(newCommentText)) {
337+
newCommentText = newCommentText
338+
.replace(/^\/\*\*\s*/, `/**\n${indent} * `)
339+
.replace(/\s*\*\/$/, `\n${indent} * @throws {${newType}}\n${indent} */`)
340+
} else {
341+
newCommentText = appendThrowsTags(
342+
newCommentText,
343+
[newType],
344+
);
345+
}
346+
return fixer.replaceTextRange(
347+
comment.range,
348+
newCommentText,
349+
);
350+
}
351+
}
352+
353+
return fixer
354+
.insertTextBefore(
355+
nodeToComment,
356+
`/**\n` +
357+
`${indent} * @throws {${newType}}\n` +
358+
`${indent} */\n` +
359+
`${indent}`
360+
);
361+
}
362+
});
363+
return;
364+
}
315365

366+
context.report({
367+
node: nodeToComment,
368+
messageId: 'missingThrowsTag',
369+
fix(fixer) {
370+
const sortedThrowableTypes = toSortedByMetadata(throwableTypes);
371+
const sortedRejectableTypes = toSortedByMetadata(rejectableTypes);
372+
316373
const indent = getNodeIndent(sourceCode, node);
317374

318375
if (hasJSDoc(sourceCode, nodeToComment)) {
@@ -322,14 +379,33 @@ module.exports = createRule({
322379

323380
if (comment) {
324381
let newCommentText = sourceCode.getText(comment);
325-
if (!/^\/\*\*[ \t]*\n/.test(newCommentText)) {
382+
const isOneLiner = !/^\/\*\*[ \t]*\n/.test(newCommentText);
383+
if (isOneLiner) {
326384
newCommentText = newCommentText
327385
.replace(/^\/\*\*\s*/, `/**\n${indent} * `)
328-
.replace(/\s*\*\/$/, `\n${indent} * @throws {${newType}}\n${indent} */`)
386+
.replace(
387+
/\s*\*\/$/,
388+
sortedThrowableTypes.map((t) =>
389+
`\n${indent} * @throws {${getQualifiedTypeName(checker, t)}}`
390+
).join('') +
391+
'\n' +
392+
sortedRejectableTypes.map((t) =>
393+
`${indent} * @throws {Promise<${getQualifiedTypeName(checker, t)}>}`
394+
).join('\n') +
395+
'\n' +
396+
`${indent} */`
397+
);
329398
} else {
330399
newCommentText = appendThrowsTags(
331400
newCommentText,
332-
[newType],
401+
[
402+
...sortedThrowableTypes.map((t) =>
403+
getQualifiedTypeName(checker, t)
404+
),
405+
...sortedRejectableTypes.map((t) =>
406+
`Promise<${getQualifiedTypeName(checker, t)}>`
407+
),
408+
],
333409
);
334410
}
335411
return fixer.replaceTextRange(
@@ -343,7 +419,12 @@ module.exports = createRule({
343419
.insertTextBefore(
344420
nodeToComment,
345421
`/**\n` +
346-
`${indent} * @throws {${newType}}\n` +
422+
sortedThrowableTypes.map((t) =>
423+
`${indent} * @throws {${getQualifiedTypeName(checker, t)}}\n`
424+
).join('') +
425+
sortedRejectableTypes.map((t) =>
426+
`${indent} * @throws {Promise<${getQualifiedTypeName(checker, t)}>}\n`
427+
).join('') +
347428
`${indent} */\n` +
348429
`${indent}`
349430
);

0 commit comments

Comments
 (0)