Skip to content

Commit

Permalink
feat(inheritDoc): Add support for copying item’s documentation by cop…
Browse files Browse the repository at this point in the history
…ying it from another API item
  • Loading branch information
Dergash authored and Gerrit0 committed Jan 23, 2021
1 parent dd03036 commit 84db49a
Show file tree
Hide file tree
Showing 8 changed files with 684 additions and 51 deletions.
3 changes: 2 additions & 1 deletion src/lib/converter/factories/comment.ts
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
@@ -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
@@ -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
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
@@ -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
@@ -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];
}
}

0 comments on commit 84db49a

Please sign in to comment.