Skip to content

Commit df96aba

Browse files
committed
Use documentation comments from inherited properties when @inheritdoc is present
The JSDoc `@ineheritDoc` [tag](http://usejsdoc.org/tags-inheritdoc.html) "indicates that a symbol should inherit its documentation from its parent class". In the case of a TypeScript file, this also includes implemented interfaces and parent interfaces. With this change, a class method or property (or an interface property) with the `@inheritDoc` tag in its JSDoc comment will automatically use the comments from its nearest ancestor that has no `@inheritDoc` tag. To prevent breaking backwards compatibility, `Symbol.getDocumentationComment` now accepts an optional `TypeChecker` instance to support this feature. fixes #8912
1 parent d407f14 commit df96aba

File tree

10 files changed

+507
-15
lines changed

10 files changed

+507
-15
lines changed

src/compiler/parser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6372,6 +6372,9 @@ namespace ts {
63726372
case "constructor":
63736373
tag = parseClassTag(atToken, tagName);
63746374
break;
6375+
case "inheritDoc":
6376+
tag = parseInheritDocTag(atToken, tagName);
6377+
break;
63756378
case "arg":
63766379
case "argument":
63776380
case "param":
@@ -6603,6 +6606,13 @@ namespace ts {
66036606
return finishNode(result);
66046607
}
66056608

6609+
function parseInheritDocTag(atToken: AtToken, tagName: Identifier): JSDocInheritDocTag {
6610+
const tag = <JSDocInheritDocTag>createNode(SyntaxKind.JSDocInheritDocTag, atToken.pos);
6611+
tag.atToken = atToken;
6612+
tag.tagName = tagName;
6613+
return finishNode(tag);
6614+
}
6615+
66066616
function parseAugmentsTag(atToken: AtToken, tagName: Identifier): JSDocAugmentsTag {
66076617
const typeExpression = parseJSDocTypeExpression(/*requireBraces*/ true);
66086618

src/compiler/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ namespace ts {
372372
JSDocTypedefTag,
373373
JSDocPropertyTag,
374374
JSDocTypeLiteral,
375+
JSDocInheritDocTag,
375376

376377
// Synthesized list
377378
SyntaxList,
@@ -413,9 +414,9 @@ namespace ts {
413414
LastBinaryOperator = CaretEqualsToken,
414415
FirstNode = QualifiedName,
415416
FirstJSDocNode = JSDocTypeExpression,
416-
LastJSDocNode = JSDocTypeLiteral,
417+
LastJSDocNode = JSDocInheritDocTag,
417418
FirstJSDocTagNode = JSDocTag,
418-
LastJSDocTagNode = JSDocTypeLiteral
419+
LastJSDocTagNode = JSDocInheritDocTag
419420
}
420421

421422
export const enum NodeFlags {
@@ -2168,6 +2169,10 @@ namespace ts {
21682169
kind: SyntaxKind.JSDocClassTag;
21692170
}
21702171

2172+
export interface JSDocInheritDocTag extends JSDocTag {
2173+
kind: SyntaxKind.JSDocInheritDocTag;
2174+
}
2175+
21712176
export interface JSDocTemplateTag extends JSDocTag {
21722177
kind: SyntaxKind.JSDocTemplateTag;
21732178
typeParameters: NodeArray<TypeParameterDeclaration>;

src/services/jsDoc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace ts.JsDoc {
1919
"fileOverview",
2020
"function",
2121
"ignore",
22+
"inheritDoc",
2223
"inner",
2324
"lends",
2425
"link",
@@ -70,7 +71,7 @@ namespace ts.JsDoc {
7071
const tags: JSDocTagInfo[] = [];
7172
forEachUnique(declarations, declaration => {
7273
for (const tag of getJSDocTags(declaration)) {
73-
if (tag.kind === SyntaxKind.JSDocTag) {
74+
if (tag.kind === SyntaxKind.JSDocTag || tag.kind === SyntaxKind.JSDocInheritDocTag) {
7475
tags.push({ name: tag.tagName.text, text: tag.comment });
7576
}
7677
}

src/services/services.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,19 @@ namespace ts {
342342
return this.declarations;
343343
}
344344

345-
getDocumentationComment(): SymbolDisplayPart[] {
345+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[] {
346346
if (this.documentationComment === undefined) {
347347
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations);
348+
349+
if (this.documentationComment.length === 0 && hasJSDocInheritDocTag(this) && typeChecker) {
350+
for (const declaration of this.getDeclarations()) {
351+
const inheritedDocs = findInheritedJSDocComments(declaration, this.getName(), typeChecker);
352+
if (inheritedDocs.length > 0) {
353+
this.documentationComment = inheritedDocs;
354+
break;
355+
}
356+
}
357+
}
348358
}
349359

350360
return this.documentationComment;
@@ -471,9 +481,13 @@ namespace ts {
471481
return this.checker.getReturnTypeOfSignature(this);
472482
}
473483

474-
getDocumentationComment(): SymbolDisplayPart[] {
484+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[] {
475485
if (this.documentationComment === undefined) {
476486
this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations([this.declaration]) : [];
487+
488+
if (this.declaration && this.documentationComment.length === 0 && hasJSDocInheritDocTag(this) && typeChecker) {
489+
this.documentationComment = findInheritedJSDocComments(this.declaration, this.declaration.symbol.getName(), typeChecker);
490+
}
477491
}
478492

479493
return this.documentationComment;
@@ -488,6 +502,63 @@ namespace ts {
488502
}
489503
}
490504

505+
/**
506+
* Returns whether or not the given symbol or signature has a JSDoc "inheritDoc" tag on it.
507+
* @param symbol the Symbol or Signature in question.
508+
* @returns `true` if `symbol` has a JSDoc "inheritDoc" tag on it, otherwise `false`.
509+
*/
510+
function hasJSDocInheritDocTag(symbol: Signature | Symbol) {
511+
return !!find(symbol.getJsDocTags(), tag => tag.name === "inheritDoc");
512+
}
513+
514+
/**
515+
* Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses
516+
* implemented interfaces until a symbol is found with the same name and with documentation.
517+
* @param declaration The possibly-inherited declaration to find comments for.
518+
* @param propertyName The name of the possibly-inherited property.
519+
* @param typeChecker A TypeChecker, used to find inherited properties.
520+
* @returns A filled array of documentation comments if any were found, otherwise an empty array.
521+
*/
522+
function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] {
523+
let documentationComment: SymbolDisplayPart[] = [];
524+
525+
if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) {
526+
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent;
527+
const baseTypeNode = getClassExtendsHeritageClauseElement(container);
528+
529+
if (baseTypeNode) {
530+
const baseType = typeChecker.getTypeAtLocation(baseTypeNode);
531+
532+
// First check superclasses for a property of the same name
533+
let baseProperty = typeChecker.getPropertyOfType(baseType, propertyName);
534+
let baseDocs = baseProperty ? baseProperty.getDocumentationComment(typeChecker) : [];
535+
if (baseDocs.length > 0) {
536+
documentationComment = baseDocs;
537+
}
538+
539+
// If there's nothing in the superclass, walk through implemented interfaces left-to-right
540+
if (documentationComment.length === 0) {
541+
const implementedInterfaces = map(
542+
getClassImplementsHeritageClauseElements(container as ClassLikeDeclaration),
543+
interfaceNode => typeChecker.getTypeAtLocation(interfaceNode)
544+
);
545+
546+
for (const implementedInterface of implementedInterfaces) {
547+
// Use the docs from the first implemented interface to have this property and documentation
548+
baseProperty = typeChecker.getPropertyOfType(implementedInterface, propertyName);
549+
baseDocs = baseProperty ? baseProperty.getDocumentationComment(typeChecker) : [];
550+
if (baseDocs.length > 0) {
551+
documentationComment = baseDocs;
552+
break;
553+
}
554+
}
555+
}
556+
}
557+
}
558+
559+
return documentationComment;
560+
}
561+
491562
class SourceFileObject extends NodeObject implements SourceFile {
492563
public kind: SyntaxKind.SourceFile;
493564
public _declarationBrand: any;
@@ -1423,7 +1494,7 @@ namespace ts {
14231494
kindModifiers: ScriptElementKindModifier.none,
14241495
textSpan: createTextSpan(node.getStart(), node.getWidth()),
14251496
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1426-
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
1497+
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
14271498
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
14281499
};
14291500
}

src/services/signatureHelp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ namespace ts.SignatureHelp {
400400
suffixDisplayParts,
401401
separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()],
402402
parameters: signatureHelpParameters,
403-
documentation: candidateSignature.getDocumentationComment(),
403+
documentation: candidateSignature.getDocumentationComment(typeChecker),
404404
tags: candidateSignature.getJsDocTags()
405405
};
406406
});
@@ -420,7 +420,7 @@ namespace ts.SignatureHelp {
420420

421421
return {
422422
name: parameter.name,
423-
documentation: parameter.getDocumentationComment(),
423+
documentation: parameter.getDocumentationComment(typeChecker),
424424
displayParts,
425425
isOptional: typeChecker.isOptionalParameter(<ParameterDeclaration>parameter.valueDeclaration)
426426
};
@@ -438,4 +438,4 @@ namespace ts.SignatureHelp {
438438
};
439439
}
440440
}
441-
}
441+
}

src/services/symbolDisplay.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ namespace ts.SymbolDisplay {
429429
}
430430

431431
if (!documentation) {
432-
documentation = symbol.getDocumentationComment();
432+
documentation = symbol.getDocumentationComment(typeChecker);
433433
tags = symbol.getJsDocTags();
434434
if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) {
435435
// For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo`
@@ -446,7 +446,7 @@ namespace ts.SymbolDisplay {
446446
continue;
447447
}
448448

449-
documentation = rhsSymbol.getDocumentationComment();
449+
documentation = rhsSymbol.getDocumentationComment(typeChecker);
450450
tags = rhsSymbol.getJsDocTags();
451451
if (documentation.length > 0) {
452452
break;
@@ -513,7 +513,7 @@ namespace ts.SymbolDisplay {
513513
displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads"));
514514
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
515515
}
516-
documentation = signature.getDocumentationComment();
516+
documentation = signature.getDocumentationComment(typeChecker);
517517
tags = signature.getJsDocTags();
518518
}
519519

src/services/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace ts {
3232
getEscapedName(): __String;
3333
getName(): string;
3434
getDeclarations(): Declaration[] | undefined;
35-
getDocumentationComment(): SymbolDisplayPart[];
35+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
3636
getJsDocTags(): JSDocTagInfo[];
3737
}
3838

@@ -55,7 +55,7 @@ namespace ts {
5555
getTypeParameters(): TypeParameter[] | undefined;
5656
getParameters(): Symbol[];
5757
getReturnType(): Type;
58-
getDocumentationComment(): SymbolDisplayPart[];
58+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
5959
getJsDocTags(): JSDocTagInfo[];
6060
}
6161

0 commit comments

Comments
 (0)