Skip to content

feat(inheritDoc): Add support for copying item’s documentation by copying it from another API item #1468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lib/converter/factories/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ export function parseComment(
if (
tagName === "param" ||
tagName === "typeparam" ||
tagName === "template"
tagName === "template" ||
tagName === "inheritdoc"
) {
line = consumeTypeData(line);
const param = /[^\s]+/.exec(line);
Expand Down
49 changes: 4 additions & 45 deletions src/lib/converter/plugins/ImplementsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Reflection,
ReflectionKind,
DeclarationReflection,
SignatureReflection,
Expand All @@ -8,8 +7,8 @@ import { Type, ReferenceType } from "../../models/types/index";
import { Component, ConverterComponent } from "../components";
import { Converter } from "../converter";
import { Context } from "../context";
import { Comment } from "../../models/comments/comment";
import { zip } from "../../utils/array";
import { copyComment } from "../utils/reflections";

/**
* A plugin that detects interface implementations of functions and
Expand Down Expand Up @@ -82,7 +81,7 @@ export class ImplementsPlugin extends ConverterComponent {
interfaceMember,
context.project
);
this.copyComment(classMember, interfaceMember);
copyComment(classMember, interfaceMember);

if (
interfaceMember.kindOf(ReflectionKind.FunctionOrMethod) &&
Expand All @@ -105,7 +104,7 @@ export class ImplementsPlugin extends ConverterComponent {
interfaceSignature,
context.project
);
this.copyComment(
copyComment(
classSignature,
interfaceSignature
);
Expand All @@ -119,46 +118,6 @@ export class ImplementsPlugin extends ConverterComponent {
);
}

/**
* Copy the comment of the source reflection to the target reflection.
*
* @param target
* @param source
*/
private copyComment(target: Reflection, source: Reflection) {
if (
target.comment &&
source.comment &&
target.comment.hasTag("inheritdoc")
) {
target.comment.copyFrom(source.comment);

if (
target instanceof SignatureReflection &&
target.parameters &&
source instanceof SignatureReflection &&
source.parameters
) {
for (
let index = 0, count = target.parameters.length;
index < count;
index++
) {
const sourceParameter = source.parameters[index];
if (sourceParameter && sourceParameter.comment) {
const targetParameter = target.parameters[index];
if (!targetParameter.comment) {
targetParameter.comment = new Comment();
targetParameter.comment.copyFrom(
sourceParameter.comment
);
}
}
}
}
}
}

private analyzeInheritance(
context: Context,
reflection: DeclarationReflection
Expand Down Expand Up @@ -201,7 +160,7 @@ export class ImplementsPlugin extends ConverterComponent {
parentMember,
context.project
);
this.copyComment(child, parentMember);
copyComment(child, parentMember);
}
}
}
Expand Down
88 changes: 88 additions & 0 deletions src/lib/converter/plugins/InheritDocPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
ContainerReflection,
DeclarationReflection,
ReflectionKind,
SignatureReflection,
Type,
} from "../../models";
import { Component, ConverterComponent } from "../components";
import { Converter } from "../converter";
import { Context } from "../context";
import { copyComment } from "../utils/reflections";
import {
Reflection,
TraverseCallback,
} from "../../models/reflections/abstract";

/**
* A plugin that handles `inheritDoc` by copying documentation from another API item.
*
* What gets copied:
* - short text
* - text
* - `@remarks` block
* - `@params` block
* - `@typeParam` block
* - `@return` block
*/
@Component({ name: "inheritDoc" })
export class InheritDocPlugin extends ConverterComponent {
/**
* Create a new InheritDocPlugin instance.
*/
initialize() {
this.listenTo(
this.owner,
{
[Converter.EVENT_RESOLVE]: this.onResolve,
},
undefined,
-200
);
}

/**
* Triggered when the converter resolves a reflection.
*
* Traverse through reflection descendant to check for `inheritDoc` tag.
* If encountered, the parameter of the tag iss used to determine a source reflection
* that will provide actual comment.
*
* @param context The context object describing the current state the converter is in.
* @param reflection The reflection that is currently resolved.
*/
private onResolve(_context: Context, reflection: DeclarationReflection) {
if (reflection instanceof ContainerReflection) {
const descendantsCallback: TraverseCallback = (item) => {
item.traverse(descendantsCallback);
const inheritDoc = item.comment?.getTag("inheritdoc")
?.paramName;
const source =
inheritDoc && reflection.findReflectionByName(inheritDoc);
let referencedReflection = source;
if (
source instanceof DeclarationReflection &&
item instanceof SignatureReflection
) {
const isFunction = source?.kindOf(
ReflectionKind.FunctionOrMethod
);
if (isFunction) {
referencedReflection =
source.signatures?.find((signature) => {
return Type.isTypeListEqual(
signature.getParameterTypes(),
item.getParameterTypes()
);
}) ?? source.signatures?.[0];
}
}

if (referencedReflection instanceof Reflection) {
copyComment(item, referencedReflection);
}
};
reflection.traverse(descendantsCallback);
}
}
}
1 change: 1 addition & 0 deletions src/lib/converter/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { ImplementsPlugin } from "./ImplementsPlugin";
export { PackagePlugin } from "./PackagePlugin";
export { SourcePlugin } from "./SourcePlugin";
export { TypePlugin } from "./TypePlugin";
export { InheritDocPlugin } from "./InheritDocPlugin";
80 changes: 79 additions & 1 deletion src/lib/converter/utils/reflections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { IntrinsicType, Type, UnionType } from "../../models";
import {
Comment,
DeclarationReflection,
IntrinsicType,
Reflection,
SignatureReflection,
Type,
UnionType,
} from "../../models";

export function removeUndefined(type: Type) {
if (type instanceof UnionType) {
Expand All @@ -13,3 +21,73 @@ export function removeUndefined(type: Type) {
}
return type;
}

/**
* Copy the comment of the source reflection to the target reflection.
*
* @param target - Reflection with comment containing `inheritdoc` tag
* @param source - Referenced reflection
*/
export function copyComment(target: Reflection, source: Reflection) {
if (
target.comment &&
source.comment &&
target.comment.hasTag("inheritdoc")
) {
if (
target instanceof DeclarationReflection &&
source instanceof DeclarationReflection
) {
target.typeParameters = source.typeParameters;
}
if (
target instanceof SignatureReflection &&
source instanceof SignatureReflection
) {
target.typeParameters = source.typeParameters;
/**
* TSDoc overrides existing parameters entirely with inherited ones, while
* existing implementation merges them.
* To avoid breaking things, `inheritDoc` tag is additionally checked for the parameter,
* so the previous behaviour will continue to work.
*
* TODO: When breaking change becomes acceptable remove legacy implementation
*/
if (target.comment.getTag("inheritdoc")?.paramName) {
target.parameters = source.parameters;
} else {
legacyCopyImplementation(target, source);
}
}
target.comment.removeTags("inheritdoc");
target.comment.copyFrom(source.comment);
}
}

/**
* Copy comments from source reflection to target reflection, parameters are merged.
*
* @param target - Reflection with comment containing `inheritdoc` tag
* @param source - Parent reflection
*/
function legacyCopyImplementation(
target: SignatureReflection,
source: SignatureReflection
) {
if (target.parameters && source.parameters) {
for (
let index = 0, count = target.parameters.length;
index < count;
index++
) {
const sourceParameter = source.parameters[index];
if (sourceParameter && sourceParameter.comment) {
const targetParameter = target.parameters[index];
if (!targetParameter.comment) {
targetParameter.comment = new Comment();
targetParameter.comment.copyFrom(sourceParameter.comment);
}
}
}
}
}
23 changes: 19 additions & 4 deletions src/lib/models/comments/comment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { removeIf } from "../../utils";
import { CommentTag } from "./tag";

const COPIED_TAGS = ["remarks"];

/**
* A model that represents a comment.
*
Expand Down Expand Up @@ -85,14 +87,27 @@ export class Comment {
/**
* Copy the data of the given comment into this comment.
*
* @param comment
* `shortText`, `text`, `returns` and tags from `COPIED_TAGS` are copied;
* other instance tags left unchanged.
*
* @param comment - Source comment to copy from
*/
copyFrom(comment: Comment) {
this.shortText = comment.shortText;
this.text = comment.text;
this.returns = comment.returns;
this.tags = comment.tags.map(
(tag) => new CommentTag(tag.tagName, tag.paramName, tag.text)
);
const overrideTags: CommentTag[] = comment.tags
.filter((tag) => COPIED_TAGS.includes(tag.tagName))
.map((tag) => new CommentTag(tag.tagName, tag.paramName, tag.text));
this.tags.forEach((tag, index) => {
const matchingTag = overrideTags.find(
(matchingOverride) => matchingOverride?.tagName === tag.tagName
);
if (matchingTag) {
this.tags[index] = matchingTag;
overrideTags.splice(overrideTags.indexOf(matchingTag), 1);
}
});
this.tags = [...this.tags, ...overrideTags];
}
}
Loading