Skip to content

Commit 26582f7

Browse files
authored
Merge pull request #123 from Xvezda/feature/prefer-union
Add `preferUnionType` option to matching type rule
2 parents 7d173df + ea47bb5 commit 26582f7

File tree

3 files changed

+261
-61
lines changed

3 files changed

+261
-61
lines changed

docs/rules/check-throws-tag-type.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/check-throws-tag-type.js

Lines changed: 122 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
getNodeID,
99
getFirst,
1010
getLast,
11+
getNodeIndent,
1112
isInHandledContext,
1213
isInAsyncHandledContext,
1314
isNodeReturned,
@@ -128,15 +129,21 @@ module.exports = createRule({
128129
properties: {
129130
useBaseTypeOfLiteral: {
130131
type: 'boolean',
131-
default: false,
132+
},
133+
preferUnionType: {
134+
type: 'boolean',
132135
},
133136
},
134137
additionalProperties: false,
135138
},
136139
],
137140
},
138141
defaultOptions: [
139-
{ useBaseTypeOfLiteral: false },
142+
/** @type {{ useBaseTypeOfLiteral?: boolean; preferUnionType?: boolean }} */
143+
({
144+
useBaseTypeOfLiteral: false,
145+
preferUnionType: true
146+
}),
140147
],
141148

142149
create(context) {
@@ -146,6 +153,7 @@ module.exports = createRule({
146153

147154
const {
148155
useBaseTypeOfLiteral = false,
156+
preferUnionType = true,
149157
} = context.options[0] ?? {};
150158

151159
/** @type {Set<string>} */
@@ -304,20 +312,31 @@ module.exports = createRule({
304312
}
305313

306314
const throwableTypes =
307-
toFlattenedTypeArray(
308-
/** @type {import('typescript').Type[]} */(
309-
throwTypes.get(node)
310-
?.map(t => checker.getAwaitedType(t) ?? t)
311-
)
312-
);
315+
node.async
316+
? []
317+
: toFlattenedTypeArray(
318+
/** @type {import('typescript').Type[]} */(
319+
throwTypes.get(node)
320+
?.map(t => checker.getAwaitedType(t) ?? t)
321+
)
322+
);
313323

314324
const rejectableTypes =
315-
toFlattenedTypeArray(
316-
/** @type {import('typescript').Type[]} */(
317-
rejectTypes.get(node)
318-
?.map(t => checker.getAwaitedType(t) ?? t)
325+
node.async
326+
? toFlattenedTypeArray(
327+
[
328+
...throwTypes.get(node)
329+
?.map(t => checker.getAwaitedType(t) ?? t),
330+
...rejectTypes.get(node)
331+
?.map(t => checker.getAwaitedType(t) ?? t)
332+
]
319333
)
320-
);
334+
: toFlattenedTypeArray(
335+
/** @type {import('typescript').Type[]} */(
336+
rejectTypes.get(node)
337+
?.map(t => checker.getAwaitedType(t) ?? t)
338+
)
339+
);
321340

322341
if (
323342
!throwableTypes.length &&
@@ -366,40 +385,58 @@ module.exports = createRule({
366385
const lastThrowsTypeNode = getLast(documentedThrowsTypeNodes);
367386
if (!lastThrowsTypeNode) return;
368387

369-
// Thrown types inside async function should be wrapped into Promise
388+
const indent = getNodeIndent(sourceCode, node);
389+
390+
// If all callee thrown types are compatible with caller's throws tags,
391+
// we don't need to report anything
370392
if (
371-
node.async &&
372-
!getJSDocThrowsTagTypes(checker, callerDeclarationTSNode)
373-
.every(type => isPromiseType(services, type))
374-
) {
393+
!throwTypeGroups.source.incompatible &&
394+
!rejectTypeGroups.source.incompatible
395+
) return;
396+
397+
// Thrown types inside async function should be wrapped into Promise
398+
if (node.async) {
399+
if (preferUnionType) {
400+
context.report({
401+
node,
402+
messageId: 'throwTypeMismatch',
403+
fix(fixer) {
404+
return fixer.replaceTextRange(
405+
[lastThrowsTypeNode.getStart(), lastThrowsTypeNode.getEnd()],
406+
`Promise<${
407+
typesToUnionString(
408+
checker,
409+
toSortedByMetadata([
410+
...throwableTypes,
411+
...rejectableTypes,
412+
]),
413+
{ useBaseTypeOfLiteral }
414+
)
415+
}>`);
416+
},
417+
});
418+
return;
419+
}
420+
375421
context.report({
376422
node,
377423
messageId: 'throwTypeMismatch',
378424
fix(fixer) {
379425
return fixer.replaceTextRange(
380-
[lastThrowsTypeNode.pos, lastThrowsTypeNode.end],
381-
`Promise<${
382-
typesToUnionString(
383-
checker,
384-
toSortedByMetadata([
385-
...throwableTypes,
386-
...rejectableTypes,
387-
]),
388-
{ useBaseTypeOfLiteral }
426+
[lastThrowsTypeNode.getStart(), lastThrowsTypeNode.getEnd()],
427+
toSortedByMetadata([ ...throwableTypes, ...rejectableTypes ])
428+
.map(t =>
429+
`Promise<${
430+
getQualifiedTypeName(checker, t, { useBaseTypeOfLiteral })
431+
}>`
389432
)
390-
}>`);
433+
.join(`}\n${indent} * @throws {`)
434+
);
391435
},
392436
});
393437
return;
394438
}
395439

396-
// If all callee thrown types are compatible with caller's throws tags,
397-
// we don't need to report anything
398-
if (
399-
!throwTypeGroups.source.incompatible &&
400-
!rejectTypeGroups.source.incompatible
401-
) return;
402-
403440
const lastThrowsTag = getLast(documentedThrowsTags);
404441
if (!lastThrowsTag) return;
405442

@@ -426,37 +463,61 @@ module.exports = createRule({
426463
return;
427464
}
428465

466+
if (preferUnionType) {
467+
context.report({
468+
node,
469+
messageId: 'throwTypeMismatch',
470+
fix(fixer) {
471+
return fixer.replaceTextRange(
472+
[lastThrowsTypeNode.getStart(), lastThrowsTypeNode.getEnd()],
473+
node.async
474+
? `Promise<${
475+
typesToUnionString(
476+
checker,
477+
toSortedByMetadata([...throwableTypes, ...rejectableTypes]),
478+
{ useBaseTypeOfLiteral }
479+
)
480+
}>`
481+
: typeStringsToUnionString([
482+
throwableTypes.length
483+
? typesToUnionString(
484+
checker, toSortedByMetadata(throwableTypes),
485+
{ useBaseTypeOfLiteral }
486+
)
487+
: '',
488+
rejectableTypes.length
489+
? `Promise<${
490+
typesToUnionString(
491+
checker,
492+
toSortedByMetadata(rejectableTypes),
493+
{ useBaseTypeOfLiteral }
494+
)}>`
495+
: '',
496+
].filter(t => !!t))
497+
);
498+
},
499+
});
500+
return;
501+
}
502+
429503
context.report({
430504
node,
431505
messageId: 'throwTypeMismatch',
432506
fix(fixer) {
433-
// If there is only one throws tag, make it as a union type
434507
return fixer.replaceTextRange(
435-
[lastThrowsTypeNode.pos, lastThrowsTypeNode.end],
436-
node.async
437-
? `Promise<${
438-
typesToUnionString(
439-
checker,
440-
toSortedByMetadata([...throwableTypes, ...rejectableTypes]),
441-
{ useBaseTypeOfLiteral }
442-
)
443-
}>`
444-
: typeStringsToUnionString([
445-
throwableTypes.length
446-
? typesToUnionString(
447-
checker, toSortedByMetadata(throwableTypes),
448-
{ useBaseTypeOfLiteral }
449-
)
450-
: '',
451-
rejectableTypes.length
452-
? `Promise<${
453-
typesToUnionString(
454-
checker,
455-
toSortedByMetadata(rejectableTypes),
456-
{ useBaseTypeOfLiteral }
457-
)}>`
458-
: '',
459-
].filter(t => !!t))
508+
[lastThrowsTypeNode.getStart(), lastThrowsTypeNode.getEnd()],
509+
toSortedByMetadata(throwableTypes)
510+
.map(t =>
511+
getQualifiedTypeName(checker, t, { useBaseTypeOfLiteral })
512+
)
513+
.join(`}\n${indent} * @throws {`) +
514+
toSortedByMetadata(rejectableTypes)
515+
.map(t =>
516+
`Promise<${
517+
getQualifiedTypeName(checker, t, { useBaseTypeOfLiteral })
518+
}>`
519+
)
520+
.join(`}\n${indent} * @throws {`)
460521
);
461522
},
462523
});

0 commit comments

Comments
 (0)