Skip to content

Commit be6363a

Browse files
committed
Fixes for JSDoc style @inheritDoc handling
Resolves #1927
1 parent 6eb93d2 commit be6363a

File tree

31 files changed

+335
-159
lines changed

31 files changed

+335
-159
lines changed

.eslintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
"@typescript-eslint/ban-types": 0,
4646
"@typescript-eslint/no-explicit-any": 0,
4747

48+
// Really annoying, doesn't provide any value.
49+
"@typescript-eslint/no-empty-function": 0,
50+
4851
// Declaration merging with a namespace is a necessary tool when working with enums.
4952
"@typescript-eslint/no-namespace": 0,
5053

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ These TODOs will be resolved before a full release. ([GitHub project](https://gi
5656
- Links which refer to members within a reference reflection will now correctly resolve to the referenced reflection's member, #1770.
5757
- Correctly detect optional parameters in JavaScript projects using JSDoc, #1804.
5858
- Fixed identical anchor links for reflections with the same name, #1845.
59+
- TypeDoc will now automatically inherit documentation from classes `implements` by other interfaces/classes.
60+
- Fixed `@inheritDoc` on accessors, #1927.
5961
- JS exports defined as `exports.foo = ...` will now be converted as variables rather than properties.
6062
- Corrected schema generation for https://typedoc.org/schema.json
6163

scripts/rebuild_specs.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ function rebuildConverterTests(dirs) {
7878
);
7979
const serialized = app.serializer.toObject(result);
8080

81-
const data = JSON.stringify(serialized, null, " ")
82-
.split(TypeDoc.normalizePath(base))
83-
.join("%BASE%");
81+
const data =
82+
JSON.stringify(serialized, null, " ")
83+
.split(TypeDoc.normalizePath(base))
84+
.join("%BASE%")
85+
.trim() + "\n";
8486
after();
8587
fs.writeFileSync(out, data);
8688
}

src/lib/converter/comments/parser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ function blockContent(
228228
break;
229229

230230
case TokenSyntaxKind.Tag:
231+
if (next.text === "@inheritdoc") {
232+
warning(
233+
"The @inheritDoc tag should be properly capitalized."
234+
);
235+
next.text = "@inheritDoc";
236+
}
231237
if (config.modifierTags.has(next.text)) {
232238
comment.modifierTags.add(next.text);
233239
break;

src/lib/converter/plugins/ImplementsPlugin.ts

Lines changed: 135 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ts from "typescript";
22
import {
3+
ContainerReflection,
34
DeclarationReflection,
45
Reflection,
56
ReflectionKind,
@@ -10,7 +11,6 @@ import { filterMap, zip } from "../../utils/array";
1011
import { Component, ConverterComponent } from "../components";
1112
import type { Context } from "../context";
1213
import { Converter } from "../converter";
13-
import { copyComment } from "../utils/reflections";
1414

1515
/**
1616
* A plugin that detects interface implementations of functions and
@@ -25,7 +25,11 @@ export class ImplementsPlugin extends ConverterComponent {
2525
* Create a new ImplementsPlugin instance.
2626
*/
2727
override initialize() {
28-
this.listenTo(this.owner, Converter.EVENT_RESOLVE, this.onResolve, -10);
28+
this.listenTo(
29+
this.owner,
30+
Converter.EVENT_RESOLVE_END,
31+
this.onResolveEnd
32+
);
2933
this.listenTo(
3034
this.owner,
3135
Converter.EVENT_CREATE_DECLARATION,
@@ -47,38 +51,21 @@ export class ImplementsPlugin extends ConverterComponent {
4751
* @param classReflection The reflection of the classReflection class.
4852
* @param interfaceReflection The reflection of the interfaceReflection interface.
4953
*/
50-
private analyzeClass(
54+
private analyzeImplements(
5155
context: Context,
5256
classReflection: DeclarationReflection,
5357
interfaceReflection: DeclarationReflection
5458
) {
59+
handleInheritedComments(classReflection, interfaceReflection);
5560
if (!interfaceReflection.children) {
5661
return;
5762
}
5863

5964
interfaceReflection.children.forEach((interfaceMember) => {
60-
let classMember: DeclarationReflection | undefined;
61-
62-
if (!classReflection.children) {
63-
return;
64-
}
65-
66-
for (
67-
let index = 0, count = classReflection.children.length;
68-
index < count;
69-
index++
70-
) {
71-
const child = classReflection.children[index];
72-
if (child.name !== interfaceMember.name) {
73-
continue;
74-
}
75-
if (child.flags.isStatic !== interfaceMember.flags.isStatic) {
76-
continue;
77-
}
78-
79-
classMember = child;
80-
break;
81-
}
65+
const classMember = findMatchingMember(
66+
interfaceMember,
67+
classReflection
68+
);
8269

8370
if (!classMember) {
8471
return;
@@ -92,23 +79,6 @@ export class ImplementsPlugin extends ConverterComponent {
9279
interfaceMember,
9380
context.project
9481
);
95-
copyComment(classMember, interfaceMember);
96-
97-
if (
98-
interfaceMember.kindOf(ReflectionKind.Property) &&
99-
classMember.kindOf(ReflectionKind.Accessor)
100-
) {
101-
if (classMember.getSignature) {
102-
copyComment(classMember.getSignature, interfaceMember);
103-
classMember.getSignature.implementationOf =
104-
classMember.implementationOf;
105-
}
106-
if (classMember.setSignature) {
107-
copyComment(classMember.setSignature, interfaceMember);
108-
classMember.setSignature.implementationOf =
109-
classMember.implementationOf;
110-
}
111-
}
11282

11383
if (
11484
interfaceMember.kindOf(ReflectionKind.FunctionOrMethod) &&
@@ -127,9 +97,10 @@ export class ImplementsPlugin extends ConverterComponent {
12797
context.project
12898
);
12999
}
130-
copyComment(clsSig, intSig);
131100
}
132101
}
102+
103+
handleInheritedComments(classMember, interfaceMember);
133104
});
134105
}
135106

@@ -150,12 +121,10 @@ export class ImplementsPlugin extends ConverterComponent {
150121
);
151122

152123
for (const parent of extendedTypes) {
124+
handleInheritedComments(reflection, parent.reflection);
125+
153126
for (const parentMember of parent.reflection.children ?? []) {
154-
const child = reflection.children?.find(
155-
(child) =>
156-
child.name == parentMember.name &&
157-
child.flags.isStatic === parentMember.flags.isStatic
158-
);
127+
const child = findMatchingMember(parentMember, reflection);
159128

160129
if (child) {
161130
const key = child.overwrites
@@ -171,28 +140,26 @@ export class ImplementsPlugin extends ConverterComponent {
171140
parentSig,
172141
context.project
173142
);
174-
copyComment(childSig, parentSig);
175143
}
176144

177145
child[key] = ReferenceType.createResolvedReference(
178146
`${parent.name}.${parentMember.name}`,
179147
parentMember,
180148
context.project
181149
);
182-
copyComment(child, parentMember);
150+
151+
handleInheritedComments(child, parentMember);
183152
}
184153
}
185154
}
186155
}
187156

188-
/**
189-
* Triggered when the converter resolves a reflection.
190-
*
191-
* @param context The context object describing the current state the converter is in.
192-
* @param reflection The reflection that is currently resolved.
193-
*/
194-
private onResolve(context: Context, reflection: DeclarationReflection) {
195-
this.tryResolve(context, reflection);
157+
private onResolveEnd(context: Context) {
158+
for (const reflection of Object.values(context.project.reflections)) {
159+
if (reflection instanceof DeclarationReflection) {
160+
this.tryResolve(context, reflection);
161+
}
162+
}
196163
}
197164

198165
private tryResolve(context: Context, reflection: DeclarationReflection) {
@@ -235,22 +202,19 @@ export class ImplementsPlugin extends ConverterComponent {
235202

236203
if (
237204
type.reflection &&
238-
type.reflection.kindOf(ReflectionKind.Interface)
205+
type.reflection.kindOf(ReflectionKind.ClassOrInterface)
239206
) {
240-
this.analyzeClass(
207+
this.analyzeImplements(
241208
context,
242209
reflection,
243-
<DeclarationReflection>type.reflection
210+
type.reflection as DeclarationReflection
244211
);
245212
}
246213
});
247214
}
248215

249216
if (
250-
reflection.kindOf([
251-
ReflectionKind.Class,
252-
ReflectionKind.Interface,
253-
]) &&
217+
reflection.kindOf(ReflectionKind.ClassOrInterface) &&
254218
reflection.extendedTypes
255219
) {
256220
this.analyzeInheritance(context, reflection);
@@ -460,3 +424,109 @@ function createLink(
460424
}
461425
}
462426
}
427+
428+
/**
429+
* Responsible for copying comments from "parent" reflections defined
430+
* in either a base class or implemented interface to the child class.
431+
*/
432+
function handleInheritedComments(
433+
child: DeclarationReflection,
434+
parent: DeclarationReflection
435+
) {
436+
copyComment(child, parent);
437+
438+
if (
439+
parent.kindOf(ReflectionKind.Property) &&
440+
child.kindOf(ReflectionKind.Accessor)
441+
) {
442+
if (child.getSignature) {
443+
copyComment(child.getSignature, parent);
444+
child.getSignature.implementationOf = child.implementationOf;
445+
}
446+
if (child.setSignature) {
447+
copyComment(child.setSignature, parent);
448+
child.setSignature.implementationOf = child.implementationOf;
449+
}
450+
}
451+
if (
452+
parent.kindOf(ReflectionKind.Accessor) &&
453+
child.kindOf(ReflectionKind.Accessor)
454+
) {
455+
if (parent.getSignature && child.getSignature) {
456+
copyComment(child.getSignature, parent.getSignature);
457+
}
458+
if (parent.setSignature && child.setSignature) {
459+
copyComment(child.setSignature, parent.setSignature);
460+
}
461+
}
462+
463+
if (
464+
parent.kindOf(ReflectionKind.FunctionOrMethod) &&
465+
parent.signatures &&
466+
child.signatures
467+
) {
468+
for (const [cs, ps] of zip(child.signatures, parent.signatures)) {
469+
copyComment(cs, ps);
470+
}
471+
}
472+
}
473+
474+
/**
475+
* Copy the comment of the source reflection to the target reflection with a JSDoc style copy
476+
* function. The TSDoc copy function is in the InheritDocPlugin.
477+
*/
478+
function copyComment(target: Reflection, source: Reflection) {
479+
if (target.comment) {
480+
// We might still want to copy, if the child has a JSDoc style inheritDoc tag.
481+
const tag = target.comment.getTag("@inheritDoc");
482+
if (!tag || tag.name) {
483+
return;
484+
}
485+
}
486+
487+
if (!source.comment) {
488+
return;
489+
}
490+
491+
target.comment = source.comment.clone();
492+
493+
if (
494+
target instanceof DeclarationReflection &&
495+
source instanceof DeclarationReflection
496+
) {
497+
for (const [tt, ts] of zip(
498+
target.typeParameters || [],
499+
source.typeParameters || []
500+
)) {
501+
copyComment(tt, ts);
502+
}
503+
}
504+
if (
505+
target instanceof SignatureReflection &&
506+
source instanceof SignatureReflection
507+
) {
508+
for (const [tt, ts] of zip(
509+
target.typeParameters || [],
510+
source.typeParameters || []
511+
)) {
512+
copyComment(tt, ts);
513+
}
514+
for (const [pt, ps] of zip(
515+
target.parameters || [],
516+
source.parameters || []
517+
)) {
518+
copyComment(pt, ps);
519+
}
520+
}
521+
}
522+
523+
function findMatchingMember(
524+
toMatch: Reflection,
525+
container: ContainerReflection
526+
) {
527+
return container.children?.find(
528+
(child) =>
529+
child.name == toMatch.name &&
530+
child.flags.isStatic === toMatch.flags.isStatic
531+
);
532+
}

src/lib/converter/plugins/InheritDocPlugin.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { DefaultMap } from "../../utils";
1111
import { zip } from "../../utils/array";
1212

1313
/**
14-
* A plugin that handles `inheritDoc` by copying documentation from another API item.
14+
* A plugin that handles `@inheritDoc` tags by copying documentation from another API item.
15+
* It is NOT responsible for handling bare JSDoc style `@inheritDoc` tags which do not specify
16+
* a target to inherit from. Those are handled by the ImplementsPlugin class.
1517
*
1618
* What gets copied:
1719
* - short text
@@ -32,13 +34,12 @@ export class InheritDocPlugin extends ConverterComponent {
3234
override initialize() {
3335
this.owner.on(
3436
Converter.EVENT_RESOLVE_END,
35-
this.processInheritDoc.bind(this)
37+
this.processInheritDoc,
38+
this
3639
);
3740
}
3841

3942
/**
40-
* Triggered when the converter resolves a reflection.
41-
*
4243
* Traverse through reflection descendant to check for `inheritDoc` tag.
4344
* If encountered, the parameter of the tag is used to determine a source reflection
4445
* that will provide actual comment.

0 commit comments

Comments
 (0)