Skip to content

Commit 5b7f1c0

Browse files
committed
feat(inheritDoc): Add support for copying item’s documentation by copying it from another API item
1 parent 46371bf commit 5b7f1c0

File tree

8 files changed

+684
-51
lines changed

8 files changed

+684
-51
lines changed

src/lib/converter/factories/comment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ export function parseComment(
209209
if (
210210
tagName === "param" ||
211211
tagName === "typeparam" ||
212-
tagName === "template"
212+
tagName === "template" ||
213+
tagName === "inheritdoc"
213214
) {
214215
line = consumeTypeData(line);
215216
const param = /[^\s]+/.exec(line);

src/lib/converter/plugins/ImplementsPlugin.ts

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
Reflection,
32
ReflectionKind,
43
DeclarationReflection,
54
SignatureReflection,
@@ -8,8 +7,8 @@ import { Type, ReferenceType } from "../../models/types/index";
87
import { Component, ConverterComponent } from "../components";
98
import { Converter } from "../converter";
109
import { Context } from "../context";
11-
import { Comment } from "../../models/comments/comment";
1210
import { zip } from "../../utils/array";
11+
import { copyComment } from "../utils/reflections";
1312

1413
/**
1514
* A plugin that detects interface implementations of functions and
@@ -82,7 +81,7 @@ export class ImplementsPlugin extends ConverterComponent {
8281
interfaceMember,
8382
context.project
8483
);
85-
this.copyComment(classMember, interfaceMember);
84+
copyComment(classMember, interfaceMember);
8685

8786
if (
8887
interfaceMember.kindOf(ReflectionKind.FunctionOrMethod) &&
@@ -105,7 +104,7 @@ export class ImplementsPlugin extends ConverterComponent {
105104
interfaceSignature,
106105
context.project
107106
);
108-
this.copyComment(
107+
copyComment(
109108
classSignature,
110109
interfaceSignature
111110
);
@@ -119,46 +118,6 @@ export class ImplementsPlugin extends ConverterComponent {
119118
);
120119
}
121120

122-
/**
123-
* Copy the comment of the source reflection to the target reflection.
124-
*
125-
* @param target
126-
* @param source
127-
*/
128-
private copyComment(target: Reflection, source: Reflection) {
129-
if (
130-
target.comment &&
131-
source.comment &&
132-
target.comment.hasTag("inheritdoc")
133-
) {
134-
target.comment.copyFrom(source.comment);
135-
136-
if (
137-
target instanceof SignatureReflection &&
138-
target.parameters &&
139-
source instanceof SignatureReflection &&
140-
source.parameters
141-
) {
142-
for (
143-
let index = 0, count = target.parameters.length;
144-
index < count;
145-
index++
146-
) {
147-
const sourceParameter = source.parameters[index];
148-
if (sourceParameter && sourceParameter.comment) {
149-
const targetParameter = target.parameters[index];
150-
if (!targetParameter.comment) {
151-
targetParameter.comment = new Comment();
152-
targetParameter.comment.copyFrom(
153-
sourceParameter.comment
154-
);
155-
}
156-
}
157-
}
158-
}
159-
}
160-
}
161-
162121
private analyzeInheritance(
163122
context: Context,
164123
reflection: DeclarationReflection
@@ -201,7 +160,7 @@ export class ImplementsPlugin extends ConverterComponent {
201160
parentMember,
202161
context.project
203162
);
204-
this.copyComment(child, parentMember);
163+
copyComment(child, parentMember);
205164
}
206165
}
207166
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
ContainerReflection,
3+
DeclarationReflection,
4+
ReflectionKind,
5+
SignatureReflection,
6+
Type,
7+
} from "../../models";
8+
import { Component, ConverterComponent } from "../components";
9+
import { Converter } from "../converter";
10+
import { Context } from "../context";
11+
import { copyComment } from "../utils/reflections";
12+
import {
13+
Reflection,
14+
TraverseCallback,
15+
} from "../../models/reflections/abstract";
16+
17+
/**
18+
* A plugin that handles `inheritDoc` by copying documentation from another API item.
19+
*
20+
* What gets copied:
21+
* - short text
22+
* - text
23+
* - `@remarks` block
24+
* - `@params` block
25+
* - `@typeParam` block
26+
* - `@return` block
27+
*/
28+
@Component({ name: "inheritDoc" })
29+
export class InheritDocPlugin extends ConverterComponent {
30+
/**
31+
* Create a new InheritDocPlugin instance.
32+
*/
33+
initialize() {
34+
this.listenTo(
35+
this.owner,
36+
{
37+
[Converter.EVENT_RESOLVE]: this.onResolve,
38+
},
39+
undefined,
40+
-200
41+
);
42+
}
43+
44+
/**
45+
* Triggered when the converter resolves a reflection.
46+
*
47+
* Traverse through reflection descendant to check for `inheritDoc` tag.
48+
* If encountered, the parameter of the tag iss used to determine a source reflection
49+
* that will provide actual comment.
50+
*
51+
* @param context The context object describing the current state the converter is in.
52+
* @param reflection The reflection that is currently resolved.
53+
*/
54+
private onResolve(_context: Context, reflection: DeclarationReflection) {
55+
if (reflection instanceof ContainerReflection) {
56+
const descendantsCallback: TraverseCallback = (item) => {
57+
item.traverse(descendantsCallback);
58+
const inheritDoc = item.comment?.getTag("inheritdoc")
59+
?.paramName;
60+
const source =
61+
inheritDoc && reflection.findReflectionByName(inheritDoc);
62+
let referencedReflection = source;
63+
if (
64+
source instanceof DeclarationReflection &&
65+
item instanceof SignatureReflection
66+
) {
67+
const isFunction = source?.kindOf(
68+
ReflectionKind.FunctionOrMethod
69+
);
70+
if (isFunction) {
71+
referencedReflection =
72+
source.signatures?.find((signature) => {
73+
return Type.isTypeListEqual(
74+
signature.getParameterTypes(),
75+
item.getParameterTypes()
76+
);
77+
}) ?? source.signatures?.[0];
78+
}
79+
}
80+
81+
if (referencedReflection instanceof Reflection) {
82+
copyComment(item, referencedReflection);
83+
}
84+
};
85+
reflection.traverse(descendantsCallback);
86+
}
87+
}
88+
}

src/lib/converter/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { ImplementsPlugin } from "./ImplementsPlugin";
88
export { PackagePlugin } from "./PackagePlugin";
99
export { SourcePlugin } from "./SourcePlugin";
1010
export { TypePlugin } from "./TypePlugin";
11+
export { InheritDocPlugin } from "./InheritDocPlugin";

src/lib/converter/utils/reflections.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { IntrinsicType, Type, UnionType } from "../../models";
1+
import {
2+
Comment,
3+
DeclarationReflection,
4+
IntrinsicType,
5+
Reflection,
6+
SignatureReflection,
7+
Type,
8+
UnionType,
9+
} from "../../models";
210

311
export function removeUndefined(type: Type) {
412
if (type instanceof UnionType) {
@@ -13,3 +21,73 @@ export function removeUndefined(type: Type) {
1321
}
1422
return type;
1523
}
24+
25+
/**
26+
* Copy the comment of the source reflection to the target reflection.
27+
*
28+
* @param target - Reflection with comment containing `inheritdoc` tag
29+
* @param source - Referenced reflection
30+
*/
31+
export function copyComment(target: Reflection, source: Reflection) {
32+
if (
33+
target.comment &&
34+
source.comment &&
35+
target.comment.hasTag("inheritdoc")
36+
) {
37+
if (
38+
target instanceof DeclarationReflection &&
39+
source instanceof DeclarationReflection
40+
) {
41+
target.typeParameters = source.typeParameters;
42+
}
43+
if (
44+
target instanceof SignatureReflection &&
45+
source instanceof SignatureReflection
46+
) {
47+
target.typeParameters = source.typeParameters;
48+
/**
49+
* TSDoc overrides existing parameters entirely with inherited ones, while
50+
* existing implementation merges them.
51+
* To avoid breaking things, `inheritDoc` tag is additionally checked for the parameter,
52+
* so the previous behaviour will continue to work.
53+
*
54+
* TODO: When breaking change becomes acceptable remove legacy implementation
55+
*/
56+
if (target.comment.getTag("inheritdoc")?.paramName) {
57+
target.parameters = source.parameters;
58+
} else {
59+
legacyCopyImplementation(target, source);
60+
}
61+
}
62+
target.comment.removeTags("inheritdoc");
63+
target.comment.copyFrom(source.comment);
64+
}
65+
}
66+
67+
/**
68+
* Copy comments from source reflection to target reflection, parameters are merged.
69+
*
70+
* @param target - Reflection with comment containing `inheritdoc` tag
71+
* @param source - Parent reflection
72+
*/
73+
function legacyCopyImplementation(
74+
target: SignatureReflection,
75+
source: SignatureReflection
76+
) {
77+
if (target.parameters && source.parameters) {
78+
for (
79+
let index = 0, count = target.parameters.length;
80+
index < count;
81+
index++
82+
) {
83+
const sourceParameter = source.parameters[index];
84+
if (sourceParameter && sourceParameter.comment) {
85+
const targetParameter = target.parameters[index];
86+
if (!targetParameter.comment) {
87+
targetParameter.comment = new Comment();
88+
targetParameter.comment.copyFrom(sourceParameter.comment);
89+
}
90+
}
91+
}
92+
}
93+
}

src/lib/models/comments/comment.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { removeIf } from "../../utils";
22
import { CommentTag } from "./tag";
33

4+
const COPIED_TAGS = ["remarks"];
5+
46
/**
57
* A model that represents a comment.
68
*
@@ -85,14 +87,27 @@ export class Comment {
8587
/**
8688
* Copy the data of the given comment into this comment.
8789
*
88-
* @param comment
90+
* `shortText`, `text`, `returns` and tags from `COPIED_TAGS` are copied;
91+
* other instance tags left unchanged.
92+
*
93+
* @param comment - Source comment to copy from
8994
*/
9095
copyFrom(comment: Comment) {
9196
this.shortText = comment.shortText;
9297
this.text = comment.text;
9398
this.returns = comment.returns;
94-
this.tags = comment.tags.map(
95-
(tag) => new CommentTag(tag.tagName, tag.paramName, tag.text)
96-
);
99+
const overrideTags: CommentTag[] = comment.tags
100+
.filter((tag) => COPIED_TAGS.includes(tag.tagName))
101+
.map((tag) => new CommentTag(tag.tagName, tag.paramName, tag.text));
102+
this.tags.forEach((tag, index) => {
103+
const matchingTag = overrideTags.find(
104+
(matchingOverride) => matchingOverride?.tagName === tag.tagName
105+
);
106+
if (matchingTag) {
107+
this.tags[index] = matchingTag;
108+
overrideTags.splice(overrideTags.indexOf(matchingTag), 1);
109+
}
110+
});
111+
this.tags = [...this.tags, ...overrideTags];
97112
}
98113
}

0 commit comments

Comments
 (0)