Skip to content

Commit 6ff0aed

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 fcb48dd commit 6ff0aed

File tree

12 files changed

+537
-37
lines changed

12 files changed

+537
-37
lines changed

src/compiler/parser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6380,6 +6380,9 @@ namespace ts {
63806380
case "constructor":
63816381
tag = parseClassTag(atToken, tagName);
63826382
break;
6383+
case "inheritDoc":
6384+
tag = parseInheritDocTag(atToken, tagName);
6385+
break;
63836386
case "arg":
63846387
case "argument":
63856388
case "param":
@@ -6611,6 +6614,13 @@ namespace ts {
66116614
return finishNode(result);
66126615
}
66136616

6617+
function parseInheritDocTag(atToken: AtToken, tagName: Identifier): JSDocInheritDocTag {
6618+
const tag = <JSDocInheritDocTag>createNode(SyntaxKind.JSDocInheritDocTag, atToken.pos);
6619+
tag.atToken = atToken;
6620+
tag.tagName = tagName;
6621+
return finishNode(tag);
6622+
}
6623+
66146624
function parseAugmentsTag(atToken: AtToken, tagName: Identifier): JSDocAugmentsTag {
66156625
const result = <JSDocAugmentsTag>createNode(SyntaxKind.JSDocAugmentsTag, atToken.pos);
66166626
result.atToken = atToken;

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 {
@@ -2172,6 +2173,10 @@ namespace ts {
21722173
kind: SyntaxKind.JSDocClassTag;
21732174
}
21742175

2176+
export interface JSDocInheritDocTag extends JSDocTag {
2177+
kind: SyntaxKind.JSDocInheritDocTag;
2178+
}
2179+
21752180
export interface JSDocTemplateTag extends JSDocTag {
21762181
kind: SyntaxKind.JSDocTemplateTag;
21772182
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;
@@ -1370,7 +1441,7 @@ namespace ts {
13701441
kindModifiers: ScriptElementKindModifier.none,
13711442
textSpan: createTextSpan(node.getStart(), node.getWidth()),
13721443
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1373-
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
1444+
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
13741445
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
13751446
};
13761447
}

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
@@ -431,7 +431,7 @@ namespace ts.SymbolDisplay {
431431
}
432432

433433
if (!documentation) {
434-
documentation = symbol.getDocumentationComment();
434+
documentation = symbol.getDocumentationComment(typeChecker);
435435
tags = symbol.getJsDocTags();
436436
if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) {
437437
// For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo`
@@ -448,7 +448,7 @@ namespace ts.SymbolDisplay {
448448
continue;
449449
}
450450

451-
documentation = rhsSymbol.getDocumentationComment();
451+
documentation = rhsSymbol.getDocumentationComment(typeChecker);
452452
tags = rhsSymbol.getJsDocTags();
453453
if (documentation.length > 0) {
454454
break;
@@ -515,7 +515,7 @@ namespace ts.SymbolDisplay {
515515
displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads"));
516516
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
517517
}
518-
documentation = signature.getDocumentationComment();
518+
documentation = signature.getDocumentationComment(typeChecker);
519519
tags = signature.getJsDocTags();
520520
}
521521

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

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,14 @@ declare namespace ts {
346346
JSDocTypedefTag = 283,
347347
JSDocPropertyTag = 284,
348348
JSDocTypeLiteral = 285,
349-
SyntaxList = 286,
350-
NotEmittedStatement = 287,
351-
PartiallyEmittedExpression = 288,
352-
CommaListExpression = 289,
353-
MergeDeclarationMarker = 290,
354-
EndOfDeclarationMarker = 291,
355-
Count = 292,
349+
JSDocInheritDocTag = 286,
350+
SyntaxList = 287,
351+
NotEmittedStatement = 288,
352+
PartiallyEmittedExpression = 289,
353+
CommaListExpression = 290,
354+
MergeDeclarationMarker = 291,
355+
EndOfDeclarationMarker = 292,
356+
Count = 293,
356357
FirstAssignment = 58,
357358
LastAssignment = 70,
358359
FirstCompoundAssignment = 59,
@@ -379,9 +380,9 @@ declare namespace ts {
379380
LastBinaryOperator = 70,
380381
FirstNode = 143,
381382
FirstJSDocNode = 267,
382-
LastJSDocNode = 285,
383+
LastJSDocNode = 286,
383384
FirstJSDocTagNode = 276,
384-
LastJSDocTagNode = 285,
385+
LastJSDocTagNode = 286,
385386
}
386387
enum NodeFlags {
387388
None = 0,
@@ -1455,6 +1456,9 @@ declare namespace ts {
14551456
interface JSDocClassTag extends JSDocTag {
14561457
kind: SyntaxKind.JSDocClassTag;
14571458
}
1459+
interface JSDocInheritDocTag extends JSDocTag {
1460+
kind: SyntaxKind.JSDocInheritDocTag;
1461+
}
14581462
interface JSDocTemplateTag extends JSDocTag {
14591463
kind: SyntaxKind.JSDocTemplateTag;
14601464
typeParameters: NodeArray<TypeParameterDeclaration>;
@@ -3797,7 +3801,7 @@ declare namespace ts {
37973801
getEscapedName(): __String;
37983802
getName(): string;
37993803
getDeclarations(): Declaration[] | undefined;
3800-
getDocumentationComment(): SymbolDisplayPart[];
3804+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
38013805
getJsDocTags(): JSDocTagInfo[];
38023806
}
38033807
interface Type {
@@ -3818,7 +3822,7 @@ declare namespace ts {
38183822
getTypeParameters(): TypeParameter[] | undefined;
38193823
getParameters(): Symbol[];
38203824
getReturnType(): Type;
3821-
getDocumentationComment(): SymbolDisplayPart[];
3825+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
38223826
getJsDocTags(): JSDocTagInfo[];
38233827
}
38243828
interface SourceFile {

tests/baselines/reference/api/typescript.d.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,14 @@ declare namespace ts {
346346
JSDocTypedefTag = 283,
347347
JSDocPropertyTag = 284,
348348
JSDocTypeLiteral = 285,
349-
SyntaxList = 286,
350-
NotEmittedStatement = 287,
351-
PartiallyEmittedExpression = 288,
352-
CommaListExpression = 289,
353-
MergeDeclarationMarker = 290,
354-
EndOfDeclarationMarker = 291,
355-
Count = 292,
349+
JSDocInheritDocTag = 286,
350+
SyntaxList = 287,
351+
NotEmittedStatement = 288,
352+
PartiallyEmittedExpression = 289,
353+
CommaListExpression = 290,
354+
MergeDeclarationMarker = 291,
355+
EndOfDeclarationMarker = 292,
356+
Count = 293,
356357
FirstAssignment = 58,
357358
LastAssignment = 70,
358359
FirstCompoundAssignment = 59,
@@ -379,9 +380,9 @@ declare namespace ts {
379380
LastBinaryOperator = 70,
380381
FirstNode = 143,
381382
FirstJSDocNode = 267,
382-
LastJSDocNode = 285,
383+
LastJSDocNode = 286,
383384
FirstJSDocTagNode = 276,
384-
LastJSDocTagNode = 285,
385+
LastJSDocTagNode = 286,
385386
}
386387
enum NodeFlags {
387388
None = 0,
@@ -1455,6 +1456,9 @@ declare namespace ts {
14551456
interface JSDocClassTag extends JSDocTag {
14561457
kind: SyntaxKind.JSDocClassTag;
14571458
}
1459+
interface JSDocInheritDocTag extends JSDocTag {
1460+
kind: SyntaxKind.JSDocInheritDocTag;
1461+
}
14581462
interface JSDocTemplateTag extends JSDocTag {
14591463
kind: SyntaxKind.JSDocTemplateTag;
14601464
typeParameters: NodeArray<TypeParameterDeclaration>;
@@ -3797,7 +3801,7 @@ declare namespace ts {
37973801
getEscapedName(): __String;
37983802
getName(): string;
37993803
getDeclarations(): Declaration[] | undefined;
3800-
getDocumentationComment(): SymbolDisplayPart[];
3804+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
38013805
getJsDocTags(): JSDocTagInfo[];
38023806
}
38033807
interface Type {
@@ -3818,7 +3822,7 @@ declare namespace ts {
38183822
getTypeParameters(): TypeParameter[] | undefined;
38193823
getParameters(): Symbol[];
38203824
getReturnType(): Type;
3821-
getDocumentationComment(): SymbolDisplayPart[];
3825+
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
38223826
getJsDocTags(): JSDocTagInfo[];
38233827
}
38243828
interface SourceFile {

0 commit comments

Comments
 (0)