Skip to content

Commit a060420

Browse files
committed
Support cascading modifier tags
Resolves #2056
1 parent 94b19bb commit a060420

File tree

8 files changed

+128
-0
lines changed

8 files changed

+128
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
- Added support for a `packageOptions` object which specifies options that should be applied to each entry point when running with `--entryPointStrategy packages`, #2523.
4242
- `--hostedBaseUrl` will now be used to generate a `<link rel="canonical">` element in the project root page, #2550.
4343
- New option, `--customFooterHtml` to add custom HTML to the generated page footer, #2559.
44+
- TypeDoc will now copy modifier tags to children if specified in the `--cascadedModifierTags` option, #2056.
45+
- TypeDoc will now warn if mutually exclusive modifier tags are specified for a comment (e.g. both `@alpha` and `@beta`), #2056.
4446
- Added three new sort strategies `documents-first`, `documents-last`, and `alphabetical-ignoring-documents` to order markdown documents.
4547
- Added new `--alwaysCreateEntryPointModule` option. When set, TypeDoc will always create a `Module` for entry points, even if only one is provided.
4648
If `--projectDocuments` is used to add documents, this option defaults to `true`, otherwise, defaults to `false`.

src/lib/converter/plugins/CommentPlugin.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
removeIf,
2525
} from "../../utils";
2626
import { CategoryPlugin } from "./CategoryPlugin";
27+
import { setIntersection } from "../../utils/set";
2728

2829
/**
2930
* These tags are not useful to display in the generated documentation.
@@ -47,6 +48,18 @@ const NEVER_RENDERED = [
4748
"@typedef",
4849
] as const;
4950

51+
// We might make this user configurable at some point, but for now,
52+
// this set is configured here.
53+
const MUTUALLY_EXCLUSIVE_MODIFIERS = [
54+
new Set<`@${string}`>([
55+
"@alpha",
56+
"@beta",
57+
"@experimental",
58+
"@internal",
59+
"@public",
60+
]),
61+
] as const;
62+
5063
/**
5164
* Handles most behavior triggered by comments. `@group` and `@category` are handled by their respective plugins, but everything else is here.
5265
*
@@ -108,6 +121,9 @@ export class CommentPlugin extends ConverterComponent {
108121
@Option("excludeTags")
109122
accessor excludeTags!: `@${string}`[];
110123

124+
@Option("cascadedModifierTags")
125+
accessor cascadedModifierTags!: `@${string}`[];
126+
111127
@Option("excludeInternal")
112128
accessor excludeInternal!: boolean;
113129

@@ -262,6 +278,8 @@ export class CommentPlugin extends ConverterComponent {
262278
* @param node The node that is currently processed if available.
263279
*/
264280
private onDeclaration(_context: Context, reflection: Reflection) {
281+
this.cascadeModifiers(reflection);
282+
265283
const comment = reflection.comment;
266284
if (!comment) return;
267285

@@ -356,6 +374,23 @@ export class CommentPlugin extends ConverterComponent {
356374
);
357375
}
358376

377+
for (const group of MUTUALLY_EXCLUSIVE_MODIFIERS) {
378+
const intersect = setIntersection(
379+
group,
380+
reflection.comment.modifierTags,
381+
);
382+
if (intersect.size > 1) {
383+
const [a, b] = intersect;
384+
context.logger.warn(
385+
context.i18n.modifier_tag_0_is_mutually_exclusive_with_1_in_comment_for_2(
386+
a,
387+
b,
388+
reflection.getFriendlyFullName(),
389+
),
390+
);
391+
}
392+
}
393+
359394
mergeSeeTags(reflection.comment);
360395
movePropertyTags(reflection.comment, reflection);
361396
}
@@ -381,6 +416,14 @@ export class CommentPlugin extends ConverterComponent {
381416
reflection.comment.removeTags("@returns");
382417
}
383418
}
419+
420+
// Any cascaded tags will show up twice, once on this and once on our signatures
421+
// This is completely redundant, so remove them from the wrapping function.
422+
if (sigs.length) {
423+
for (const mod of this.cascadedModifierTags) {
424+
reflection.comment.modifierTags.delete(mod);
425+
}
426+
}
384427
}
385428

386429
if (reflection instanceof SignatureReflection) {
@@ -448,6 +491,29 @@ export class CommentPlugin extends ConverterComponent {
448491
}
449492
}
450493

494+
private cascadeModifiers(reflection: Reflection) {
495+
const parentComment = reflection.parent?.comment;
496+
if (!parentComment) return;
497+
498+
const childMods = reflection.comment?.modifierTags ?? new Set();
499+
500+
for (const mod of this.cascadedModifierTags) {
501+
if (parentComment.hasModifier(mod)) {
502+
const exclusiveSet = MUTUALLY_EXCLUSIVE_MODIFIERS.find((tags) =>
503+
tags.has(mod),
504+
);
505+
506+
if (
507+
!exclusiveSet ||
508+
Array.from(exclusiveSet).every((tag) => !childMods.has(tag))
509+
) {
510+
reflection.comment ||= new Comment();
511+
reflection.comment.modifierTags.add(mod);
512+
}
513+
}
514+
}
515+
}
516+
451517
/**
452518
* Determines whether or not a reflection has been hidden
453519
*

src/lib/internationalization/translatable.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const translatable = {
9696
comment_for_0_includes_categoryDescription_for_1_but_no_child_in_group: `Comment for {0} includes @categoryDescription for "{1}", but no child is placed in that category.`,
9797
comment_for_0_includes_groupDescription_for_1_but_no_child_in_group: `Comment for {0} includes @groupDescription for "{1}", but no child is placed in that group.`,
9898
label_0_for_1_cannot_be_referenced: `The label "{0}" for {1} cannot be referenced with a declaration reference. Labels may only contain A-Z, 0-9, and _, and may not start with a number.`,
99+
modifier_tag_0_is_mutually_exclusive_with_1_in_comment_for_2: `The modifier tag {0} is mutually exclusive with {1} in the comment for {2}.`,
99100
signature_0_has_unused_param_with_name_1: `The signature {0} has an @param with name "{1}", which was not used.`,
100101
declaration_reference_in_inheritdoc_for_0_not_fully_parsed: `Declaration reference in @inheritDoc for {0} was not fully parsed and may resolve incorrectly.`,
101102
failed_to_find_0_to_inherit_comment_from_in_1: `Failed to find "{0}" to inherit the comment from in the comment for {1}`,

src/lib/utils/options/declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export interface TypeDocOptionMap {
202202
externalSymbolLinkMappings: ManuallyValidatedOption<
203203
Record<string, Record<string, string>>
204204
>;
205+
cascadedModifierTags: `@${string}`[];
205206

206207
// Organization
207208
categorizeByGroup: boolean;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,21 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
705705
}
706706
},
707707
});
708+
options.addDeclaration({
709+
name: "cascadedModifierTags",
710+
help: (i18n) => i18n.help_modifierTags(),
711+
type: ParameterType.Array,
712+
defaultValue: ["@alpha", "@beta", "@experimental"],
713+
validate(value, i18n) {
714+
if (!Validation.validate([Array, Validation.isTagString], value)) {
715+
throw new Error(
716+
i18n.option_0_values_must_be_array_of_tags(
717+
"cascadedModifierTags",
718+
),
719+
);
720+
}
721+
},
722+
});
708723

709724
///////////////////////////
710725
// Organization Options ///

src/lib/utils/set.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function setIntersection<T>(a: Set<T>, b: Set<T>): Set<T> {
2+
const result = new Set<T>();
3+
for (const elem of a) {
4+
if (b.has(elem)) {
5+
result.add(elem);
6+
}
7+
}
8+
return result;
9+
}

src/test/behavior.c2.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,4 +1205,24 @@ describe("Behavior Tests", () => {
12051205
const sig2 = querySig(project, "isNonNullish");
12061206
equal(sig2.type?.toString(), "x is NonNullable<T>");
12071207
});
1208+
1209+
it("Cascades specified modifier tags to child reflections, #2056", () => {
1210+
const project = convert("cascadedModifiers");
1211+
1212+
const mods = (s: string) => query(project, s).comment?.modifierTags;
1213+
const sigMods = (s: string) =>
1214+
querySig(project, s).comment?.modifierTags;
1215+
1216+
equal(mods("BetaStuff"), new Set(["@beta"]));
1217+
equal(mods("BetaStuff.AlsoBeta"), new Set(["@beta"]));
1218+
equal(mods("BetaStuff.AlsoBeta.betaFish"), new Set());
1219+
equal(mods("BetaStuff.AlsoBeta.alphaFish"), new Set());
1220+
1221+
equal(sigMods("BetaStuff.AlsoBeta.betaFish"), new Set(["@beta"]));
1222+
equal(sigMods("BetaStuff.AlsoBeta.alphaFish"), new Set(["@alpha"]));
1223+
1224+
logger.expectMessage(
1225+
"warn: The modifier tag @alpha is mutually exclusive with @beta in the comment for mutuallyExclusive.",
1226+
);
1227+
});
12081228
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @beta
3+
*/
4+
export namespace BetaStuff {
5+
export class AlsoBeta {
6+
betaFish() {}
7+
8+
/** @alpha */
9+
alphaFish() {}
10+
}
11+
}
12+
13+
/** @alpha @beta */
14+
export const mutuallyExclusive = true;

0 commit comments

Comments
 (0)