Skip to content

Commit

Permalink
Support cascading modifier tags
Browse files Browse the repository at this point in the history
Resolves #2056
  • Loading branch information
Gerrit0 committed May 5, 2024
1 parent 94b19bb commit a060420
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
- Added support for a `packageOptions` object which specifies options that should be applied to each entry point when running with `--entryPointStrategy packages`, #2523.
- `--hostedBaseUrl` will now be used to generate a `<link rel="canonical">` element in the project root page, #2550.
- New option, `--customFooterHtml` to add custom HTML to the generated page footer, #2559.
- TypeDoc will now copy modifier tags to children if specified in the `--cascadedModifierTags` option, #2056.
- TypeDoc will now warn if mutually exclusive modifier tags are specified for a comment (e.g. both `@alpha` and `@beta`), #2056.
- Added three new sort strategies `documents-first`, `documents-last`, and `alphabetical-ignoring-documents` to order markdown documents.
- Added new `--alwaysCreateEntryPointModule` option. When set, TypeDoc will always create a `Module` for entry points, even if only one is provided.
If `--projectDocuments` is used to add documents, this option defaults to `true`, otherwise, defaults to `false`.
Expand Down
66 changes: 66 additions & 0 deletions src/lib/converter/plugins/CommentPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
removeIf,
} from "../../utils";
import { CategoryPlugin } from "./CategoryPlugin";
import { setIntersection } from "../../utils/set";

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

// We might make this user configurable at some point, but for now,
// this set is configured here.
const MUTUALLY_EXCLUSIVE_MODIFIERS = [
new Set<`@${string}`>([
"@alpha",
"@beta",
"@experimental",
"@internal",
"@public",
]),
] as const;

/**
* Handles most behavior triggered by comments. `@group` and `@category` are handled by their respective plugins, but everything else is here.
*
Expand Down Expand Up @@ -108,6 +121,9 @@ export class CommentPlugin extends ConverterComponent {
@Option("excludeTags")
accessor excludeTags!: `@${string}`[];

@Option("cascadedModifierTags")
accessor cascadedModifierTags!: `@${string}`[];

@Option("excludeInternal")
accessor excludeInternal!: boolean;

Expand Down Expand Up @@ -262,6 +278,8 @@ export class CommentPlugin extends ConverterComponent {
* @param node The node that is currently processed if available.
*/
private onDeclaration(_context: Context, reflection: Reflection) {
this.cascadeModifiers(reflection);

const comment = reflection.comment;
if (!comment) return;

Expand Down Expand Up @@ -356,6 +374,23 @@ export class CommentPlugin extends ConverterComponent {
);
}

for (const group of MUTUALLY_EXCLUSIVE_MODIFIERS) {
const intersect = setIntersection(
group,
reflection.comment.modifierTags,
);
if (intersect.size > 1) {
const [a, b] = intersect;
context.logger.warn(
context.i18n.modifier_tag_0_is_mutually_exclusive_with_1_in_comment_for_2(
a,
b,
reflection.getFriendlyFullName(),
),
);
}
}

mergeSeeTags(reflection.comment);
movePropertyTags(reflection.comment, reflection);
}
Expand All @@ -381,6 +416,14 @@ export class CommentPlugin extends ConverterComponent {
reflection.comment.removeTags("@returns");
}
}

// Any cascaded tags will show up twice, once on this and once on our signatures
// This is completely redundant, so remove them from the wrapping function.
if (sigs.length) {
for (const mod of this.cascadedModifierTags) {
reflection.comment.modifierTags.delete(mod);
}
}
}

if (reflection instanceof SignatureReflection) {
Expand Down Expand Up @@ -448,6 +491,29 @@ export class CommentPlugin extends ConverterComponent {
}
}

private cascadeModifiers(reflection: Reflection) {
const parentComment = reflection.parent?.comment;
if (!parentComment) return;

const childMods = reflection.comment?.modifierTags ?? new Set();

for (const mod of this.cascadedModifierTags) {
if (parentComment.hasModifier(mod)) {
const exclusiveSet = MUTUALLY_EXCLUSIVE_MODIFIERS.find((tags) =>
tags.has(mod),
);

if (
!exclusiveSet ||
Array.from(exclusiveSet).every((tag) => !childMods.has(tag))
) {
reflection.comment ||= new Comment();
reflection.comment.modifierTags.add(mod);
}
}
}
}

/**
* Determines whether or not a reflection has been hidden
*
Expand Down
1 change: 1 addition & 0 deletions src/lib/internationalization/translatable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const translatable = {
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.`,
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.`,
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.`,
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}.`,
signature_0_has_unused_param_with_name_1: `The signature {0} has an @param with name "{1}", which was not used.`,
declaration_reference_in_inheritdoc_for_0_not_fully_parsed: `Declaration reference in @inheritDoc for {0} was not fully parsed and may resolve incorrectly.`,
failed_to_find_0_to_inherit_comment_from_in_1: `Failed to find "{0}" to inherit the comment from in the comment for {1}`,
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/options/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export interface TypeDocOptionMap {
externalSymbolLinkMappings: ManuallyValidatedOption<
Record<string, Record<string, string>>
>;
cascadedModifierTags: `@${string}`[];

// Organization
categorizeByGroup: boolean;
Expand Down
15 changes: 15 additions & 0 deletions src/lib/utils/options/sources/typedoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,21 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
}
},
});
options.addDeclaration({
name: "cascadedModifierTags",
help: (i18n) => i18n.help_modifierTags(),
type: ParameterType.Array,
defaultValue: ["@alpha", "@beta", "@experimental"],
validate(value, i18n) {
if (!Validation.validate([Array, Validation.isTagString], value)) {
throw new Error(
i18n.option_0_values_must_be_array_of_tags(
"cascadedModifierTags",
),
);
}
},
});

///////////////////////////
// Organization Options ///
Expand Down
9 changes: 9 additions & 0 deletions src/lib/utils/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function setIntersection<T>(a: Set<T>, b: Set<T>): Set<T> {
const result = new Set<T>();
for (const elem of a) {
if (b.has(elem)) {
result.add(elem);
}
}
return result;
}
20 changes: 20 additions & 0 deletions src/test/behavior.c2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1205,4 +1205,24 @@ describe("Behavior Tests", () => {
const sig2 = querySig(project, "isNonNullish");
equal(sig2.type?.toString(), "x is NonNullable<T>");
});

it("Cascades specified modifier tags to child reflections, #2056", () => {
const project = convert("cascadedModifiers");

const mods = (s: string) => query(project, s).comment?.modifierTags;
const sigMods = (s: string) =>
querySig(project, s).comment?.modifierTags;

equal(mods("BetaStuff"), new Set(["@beta"]));
equal(mods("BetaStuff.AlsoBeta"), new Set(["@beta"]));
equal(mods("BetaStuff.AlsoBeta.betaFish"), new Set());
equal(mods("BetaStuff.AlsoBeta.alphaFish"), new Set());

equal(sigMods("BetaStuff.AlsoBeta.betaFish"), new Set(["@beta"]));
equal(sigMods("BetaStuff.AlsoBeta.alphaFish"), new Set(["@alpha"]));

logger.expectMessage(
"warn: The modifier tag @alpha is mutually exclusive with @beta in the comment for mutuallyExclusive.",
);
});
});
14 changes: 14 additions & 0 deletions src/test/converter2/behavior/cascadedModifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @beta
*/
export namespace BetaStuff {
export class AlsoBeta {
betaFish() {}

/** @alpha */
alphaFish() {}
}
}

/** @alpha @beta */
export const mutuallyExclusive = true;

0 comments on commit a060420

Please sign in to comment.