Skip to content

Commit

Permalink
Fixes for JSDoc style @inheritDoc handling
Browse files Browse the repository at this point in the history
Resolves #1927
  • Loading branch information
Gerrit0 committed May 15, 2022
1 parent 6eb93d2 commit be6363a
Show file tree
Hide file tree
Showing 31 changed files with 335 additions and 159 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Expand Up @@ -45,6 +45,9 @@
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": 0,

// Really annoying, doesn't provide any value.
"@typescript-eslint/no-empty-function": 0,

// Declaration merging with a namespace is a necessary tool when working with enums.
"@typescript-eslint/no-namespace": 0,

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

Expand Down
8 changes: 5 additions & 3 deletions scripts/rebuild_specs.js
Expand Up @@ -78,9 +78,11 @@ function rebuildConverterTests(dirs) {
);
const serialized = app.serializer.toObject(result);

const data = JSON.stringify(serialized, null, " ")
.split(TypeDoc.normalizePath(base))
.join("%BASE%");
const data =
JSON.stringify(serialized, null, " ")
.split(TypeDoc.normalizePath(base))
.join("%BASE%")
.trim() + "\n";
after();
fs.writeFileSync(out, data);
}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/converter/comments/parser.ts
Expand Up @@ -228,6 +228,12 @@ function blockContent(
break;

case TokenSyntaxKind.Tag:
if (next.text === "@inheritdoc") {
warning(
"The @inheritDoc tag should be properly capitalized."
);
next.text = "@inheritDoc";
}
if (config.modifierTags.has(next.text)) {
comment.modifierTags.add(next.text);
break;
Expand Down
200 changes: 135 additions & 65 deletions src/lib/converter/plugins/ImplementsPlugin.ts
@@ -1,5 +1,6 @@
import * as ts from "typescript";
import {
ContainerReflection,
DeclarationReflection,
Reflection,
ReflectionKind,
Expand All @@ -10,7 +11,6 @@ import { filterMap, zip } from "../../utils/array";
import { Component, ConverterComponent } from "../components";
import type { Context } from "../context";
import { Converter } from "../converter";
import { copyComment } from "../utils/reflections";

/**
* A plugin that detects interface implementations of functions and
Expand All @@ -25,7 +25,11 @@ export class ImplementsPlugin extends ConverterComponent {
* Create a new ImplementsPlugin instance.
*/
override initialize() {
this.listenTo(this.owner, Converter.EVENT_RESOLVE, this.onResolve, -10);
this.listenTo(
this.owner,
Converter.EVENT_RESOLVE_END,
this.onResolveEnd
);
this.listenTo(
this.owner,
Converter.EVENT_CREATE_DECLARATION,
Expand All @@ -47,38 +51,21 @@ export class ImplementsPlugin extends ConverterComponent {
* @param classReflection The reflection of the classReflection class.
* @param interfaceReflection The reflection of the interfaceReflection interface.
*/
private analyzeClass(
private analyzeImplements(
context: Context,
classReflection: DeclarationReflection,
interfaceReflection: DeclarationReflection
) {
handleInheritedComments(classReflection, interfaceReflection);
if (!interfaceReflection.children) {
return;
}

interfaceReflection.children.forEach((interfaceMember) => {
let classMember: DeclarationReflection | undefined;

if (!classReflection.children) {
return;
}

for (
let index = 0, count = classReflection.children.length;
index < count;
index++
) {
const child = classReflection.children[index];
if (child.name !== interfaceMember.name) {
continue;
}
if (child.flags.isStatic !== interfaceMember.flags.isStatic) {
continue;
}

classMember = child;
break;
}
const classMember = findMatchingMember(
interfaceMember,
classReflection
);

if (!classMember) {
return;
Expand All @@ -92,23 +79,6 @@ export class ImplementsPlugin extends ConverterComponent {
interfaceMember,
context.project
);
copyComment(classMember, interfaceMember);

if (
interfaceMember.kindOf(ReflectionKind.Property) &&
classMember.kindOf(ReflectionKind.Accessor)
) {
if (classMember.getSignature) {
copyComment(classMember.getSignature, interfaceMember);
classMember.getSignature.implementationOf =
classMember.implementationOf;
}
if (classMember.setSignature) {
copyComment(classMember.setSignature, interfaceMember);
classMember.setSignature.implementationOf =
classMember.implementationOf;
}
}

if (
interfaceMember.kindOf(ReflectionKind.FunctionOrMethod) &&
Expand All @@ -127,9 +97,10 @@ export class ImplementsPlugin extends ConverterComponent {
context.project
);
}
copyComment(clsSig, intSig);
}
}

handleInheritedComments(classMember, interfaceMember);
});
}

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

for (const parent of extendedTypes) {
handleInheritedComments(reflection, parent.reflection);

for (const parentMember of parent.reflection.children ?? []) {
const child = reflection.children?.find(
(child) =>
child.name == parentMember.name &&
child.flags.isStatic === parentMember.flags.isStatic
);
const child = findMatchingMember(parentMember, reflection);

if (child) {
const key = child.overwrites
Expand All @@ -171,28 +140,26 @@ export class ImplementsPlugin extends ConverterComponent {
parentSig,
context.project
);
copyComment(childSig, parentSig);
}

child[key] = ReferenceType.createResolvedReference(
`${parent.name}.${parentMember.name}`,
parentMember,
context.project
);
copyComment(child, parentMember);

handleInheritedComments(child, parentMember);
}
}
}
}

/**
* Triggered when the converter resolves a reflection.
*
* @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) {
this.tryResolve(context, reflection);
private onResolveEnd(context: Context) {
for (const reflection of Object.values(context.project.reflections)) {
if (reflection instanceof DeclarationReflection) {
this.tryResolve(context, reflection);
}
}
}

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

if (
type.reflection &&
type.reflection.kindOf(ReflectionKind.Interface)
type.reflection.kindOf(ReflectionKind.ClassOrInterface)
) {
this.analyzeClass(
this.analyzeImplements(
context,
reflection,
<DeclarationReflection>type.reflection
type.reflection as DeclarationReflection
);
}
});
}

if (
reflection.kindOf([
ReflectionKind.Class,
ReflectionKind.Interface,
]) &&
reflection.kindOf(ReflectionKind.ClassOrInterface) &&
reflection.extendedTypes
) {
this.analyzeInheritance(context, reflection);
Expand Down Expand Up @@ -460,3 +424,109 @@ function createLink(
}
}
}

/**
* Responsible for copying comments from "parent" reflections defined
* in either a base class or implemented interface to the child class.
*/
function handleInheritedComments(
child: DeclarationReflection,
parent: DeclarationReflection
) {
copyComment(child, parent);

if (
parent.kindOf(ReflectionKind.Property) &&
child.kindOf(ReflectionKind.Accessor)
) {
if (child.getSignature) {
copyComment(child.getSignature, parent);
child.getSignature.implementationOf = child.implementationOf;
}
if (child.setSignature) {
copyComment(child.setSignature, parent);
child.setSignature.implementationOf = child.implementationOf;
}
}
if (
parent.kindOf(ReflectionKind.Accessor) &&
child.kindOf(ReflectionKind.Accessor)
) {
if (parent.getSignature && child.getSignature) {
copyComment(child.getSignature, parent.getSignature);
}
if (parent.setSignature && child.setSignature) {
copyComment(child.setSignature, parent.setSignature);
}
}

if (
parent.kindOf(ReflectionKind.FunctionOrMethod) &&
parent.signatures &&
child.signatures
) {
for (const [cs, ps] of zip(child.signatures, parent.signatures)) {
copyComment(cs, ps);
}
}
}

/**
* Copy the comment of the source reflection to the target reflection with a JSDoc style copy
* function. The TSDoc copy function is in the InheritDocPlugin.
*/
function copyComment(target: Reflection, source: Reflection) {
if (target.comment) {
// We might still want to copy, if the child has a JSDoc style inheritDoc tag.
const tag = target.comment.getTag("@inheritDoc");
if (!tag || tag.name) {
return;
}
}

if (!source.comment) {
return;
}

target.comment = source.comment.clone();

if (
target instanceof DeclarationReflection &&
source instanceof DeclarationReflection
) {
for (const [tt, ts] of zip(
target.typeParameters || [],
source.typeParameters || []
)) {
copyComment(tt, ts);
}
}
if (
target instanceof SignatureReflection &&
source instanceof SignatureReflection
) {
for (const [tt, ts] of zip(
target.typeParameters || [],
source.typeParameters || []
)) {
copyComment(tt, ts);
}
for (const [pt, ps] of zip(
target.parameters || [],
source.parameters || []
)) {
copyComment(pt, ps);
}
}
}

function findMatchingMember(
toMatch: Reflection,
container: ContainerReflection
) {
return container.children?.find(
(child) =>
child.name == toMatch.name &&
child.flags.isStatic === toMatch.flags.isStatic
);
}
9 changes: 5 additions & 4 deletions src/lib/converter/plugins/InheritDocPlugin.ts
Expand Up @@ -11,7 +11,9 @@ import { DefaultMap } from "../../utils";
import { zip } from "../../utils/array";

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

/**
* Triggered when the converter resolves a reflection.
*
* Traverse through reflection descendant to check for `inheritDoc` tag.
* If encountered, the parameter of the tag is used to determine a source reflection
* that will provide actual comment.
Expand Down

0 comments on commit be6363a

Please sign in to comment.