Skip to content

Commit a46d270

Browse files
sjbaragAndy
authored and
Andy
committed
Use documentation comments from inherited properties when @inheritdoc is present (#18804)
* 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 * Use ts.getJSDocTags as per @Andy-MS 's recommendation * Convert @inheritdoc tests to verify.quickInfoAt * Concatenate inherited and local docs when @inheritdoc is present * Make typeChecker param explicitly `TypeChecker | undefined` * Re-accept baseline after switch to explicit `| undefined` * Update APISample_jsodc.ts to match new getDocumentationComment signature * Re-accept baselines after rebasing
1 parent 28ed9b3 commit a46d270

File tree

11 files changed

+170
-24
lines changed

11 files changed

+170
-24
lines changed

src/services/jsDoc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace ts.JsDoc {
2020
"fileOverview",
2121
"function",
2222
"ignore",
23+
"inheritDoc",
2324
"inner",
2425
"lends",
2526
"link",

src/services/services.ts

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,29 @@ namespace ts {
346346
return this.declarations;
347347
}
348348

349-
getDocumentationComment(): SymbolDisplayPart[] {
349+
getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] {
350350
if (this.documentationComment === undefined) {
351-
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations);
351+
if (this.declarations) {
352+
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations);
353+
354+
if (this.documentationComment.length === 0 || this.declarations.some(hasJSDocInheritDocTag)) {
355+
if (checker) {
356+
for (const declaration of this.declarations) {
357+
const inheritedDocs = findInheritedJSDocComments(declaration, this.getName(), checker);
358+
if (inheritedDocs.length > 0) {
359+
if (this.documentationComment.length > 0) {
360+
inheritedDocs.push(ts.lineBreakPart());
361+
}
362+
this.documentationComment = concatenate(inheritedDocs, this.documentationComment);
363+
break;
364+
}
365+
}
366+
}
367+
}
368+
}
369+
else {
370+
this.documentationComment = [];
371+
}
352372
}
353373

354374
return this.documentationComment;
@@ -477,7 +497,23 @@ namespace ts {
477497

478498
getDocumentationComment(): SymbolDisplayPart[] {
479499
if (this.documentationComment === undefined) {
480-
this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations([this.declaration]) : [];
500+
if (this.declaration) {
501+
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations([this.declaration]);
502+
503+
if (this.documentationComment.length === 0 || hasJSDocInheritDocTag(this.declaration)) {
504+
const inheritedDocs = findInheritedJSDocComments(this.declaration, this.declaration.symbol.getName(), this.checker);
505+
if (this.documentationComment.length > 0) {
506+
inheritedDocs.push(ts.lineBreakPart());
507+
}
508+
this.documentationComment = concatenate(
509+
inheritedDocs,
510+
this.documentationComment
511+
);
512+
}
513+
}
514+
else {
515+
this.documentationComment = [];
516+
}
481517
}
482518

483519
return this.documentationComment;
@@ -492,6 +528,58 @@ namespace ts {
492528
}
493529
}
494530

531+
/**
532+
* Returns whether or not the given node has a JSDoc "inheritDoc" tag on it.
533+
* @param node the Node in question.
534+
* @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`.
535+
*/
536+
function hasJSDocInheritDocTag(node: Node) {
537+
return ts.getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc");
538+
}
539+
540+
/**
541+
* Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses
542+
* implemented interfaces until a symbol is found with the same name and with documentation.
543+
* @param declaration The possibly-inherited declaration to find comments for.
544+
* @param propertyName The name of the possibly-inherited property.
545+
* @param typeChecker A TypeChecker, used to find inherited properties.
546+
* @returns A filled array of documentation comments if any were found, otherwise an empty array.
547+
*/
548+
function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] {
549+
let foundDocs = false;
550+
return flatMap(getAllSuperTypeNodes(declaration), superTypeNode => {
551+
if (foundDocs) {
552+
return emptyArray;
553+
}
554+
const superType = typeChecker.getTypeAtLocation(superTypeNode);
555+
if (!superType) {
556+
return emptyArray;
557+
}
558+
const baseProperty = typeChecker.getPropertyOfType(superType, propertyName);
559+
if (!baseProperty) {
560+
return emptyArray;
561+
}
562+
const inheritedDocs = baseProperty.getDocumentationComment(typeChecker);
563+
foundDocs = inheritedDocs.length > 0;
564+
return inheritedDocs;
565+
});
566+
}
567+
568+
/**
569+
* Finds and returns the `TypeNode` for all super classes and implemented interfaces given a declaration.
570+
* @param declaration The possibly-inherited declaration.
571+
* @returns A filled array of `TypeNode`s containing all super classes and implemented interfaces if any exist, otherwise an empty array.
572+
*/
573+
function getAllSuperTypeNodes(declaration: Declaration): ReadonlyArray<TypeNode> {
574+
const container = declaration.parent;
575+
if (!container || (!isClassDeclaration(container) && !isInterfaceDeclaration(container))) {
576+
return emptyArray;
577+
}
578+
const extended = getClassExtendsHeritageClauseElement(container);
579+
const types = extended ? [extended] : emptyArray;
580+
return isClassLike(container) ? concatenate(types, getClassImplementsHeritageClauseElements(container)) : types;
581+
}
582+
495583
class SourceFileObject extends NodeObject implements SourceFile {
496584
public kind: SyntaxKind.SourceFile;
497585
public _declarationBrand: any;
@@ -1399,7 +1487,7 @@ namespace ts {
13991487
kindModifiers: ScriptElementKindModifier.none,
14001488
textSpan: createTextSpan(node.getStart(), node.getWidth()),
14011489
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1402-
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
1490+
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
14031491
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
14041492
};
14051493
}

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
@@ -438,7 +438,7 @@ namespace ts.SymbolDisplay {
438438
}
439439

440440
if (!documentation) {
441-
documentation = symbol.getDocumentationComment();
441+
documentation = symbol.getDocumentationComment(typeChecker);
442442
tags = symbol.getJsDocTags();
443443
if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) {
444444
// For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo`
@@ -455,7 +455,7 @@ namespace ts.SymbolDisplay {
455455
continue;
456456
}
457457

458-
documentation = rhsSymbol.getDocumentationComment();
458+
documentation = rhsSymbol.getDocumentationComment(typeChecker);
459459
tags = rhsSymbol.getJsDocTags();
460460
if (documentation.length > 0) {
461461
break;
@@ -524,7 +524,7 @@ namespace ts.SymbolDisplay {
524524
displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads"));
525525
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
526526
}
527-
documentation = signature.getDocumentationComment();
527+
documentation = signature.getDocumentationComment(typeChecker);
528528
tags = signature.getJsDocTags();
529529
}
530530

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 | undefined): 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 | undefined): SymbolDisplayPart[];
5959
getJsDocTags(): JSDocTagInfo[];
6060
}
6161

tests/baselines/reference/APISample_jsdoc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function parseCommentsIntoDefinition(this: any,
2121
}
2222

2323
// the comments for a symbol
24-
let comments = symbol.getDocumentationComment();
24+
let comments = symbol.getDocumentationComment(undefined);
2525

2626
if (comments.length) {
2727
definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join("");
@@ -131,7 +131,7 @@ function parseCommentsIntoDefinition(symbol, definition, otherAnnotations) {
131131
return;
132132
}
133133
// the comments for a symbol
134-
var comments = symbol.getDocumentationComment();
134+
var comments = symbol.getDocumentationComment(undefined);
135135
if (comments.length) {
136136
definition.description = comments.map(function (comment) { return comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n"); }).join("");
137137
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3813,7 +3813,7 @@ declare namespace ts {
38133813
getEscapedName(): __String;
38143814
getName(): string;
38153815
getDeclarations(): Declaration[] | undefined;
3816-
getDocumentationComment(): SymbolDisplayPart[];
3816+
getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[];
38173817
getJsDocTags(): JSDocTagInfo[];
38183818
}
38193819
interface Type {
@@ -3834,7 +3834,7 @@ declare namespace ts {
38343834
getTypeParameters(): TypeParameter[] | undefined;
38353835
getParameters(): Symbol[];
38363836
getReturnType(): Type;
3837-
getDocumentationComment(): SymbolDisplayPart[];
3837+
getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[];
38383838
getJsDocTags(): JSDocTagInfo[];
38393839
}
38403840
interface SourceFile {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3813,7 +3813,7 @@ declare namespace ts {
38133813
getEscapedName(): __String;
38143814
getName(): string;
38153815
getDeclarations(): Declaration[] | undefined;
3816-
getDocumentationComment(): SymbolDisplayPart[];
3816+
getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[];
38173817
getJsDocTags(): JSDocTagInfo[];
38183818
}
38193819
interface Type {
@@ -3834,7 +3834,7 @@ declare namespace ts {
38343834
getTypeParameters(): TypeParameter[] | undefined;
38353835
getParameters(): Symbol[];
38363836
getReturnType(): Type;
3837-
getDocumentationComment(): SymbolDisplayPart[];
3837+
getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[];
38383838
getJsDocTags(): JSDocTagInfo[];
38393839
}
38403840
interface SourceFile {

tests/cases/compiler/APISample_jsdoc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function parseCommentsIntoDefinition(this: any,
2525
}
2626

2727
// the comments for a symbol
28-
let comments = symbol.getDocumentationComment();
28+
let comments = symbol.getDocumentationComment(undefined);
2929

3030
if (comments.length) {
3131
definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join("");

tests/cases/fourslash/commentsInheritance.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ verify.quickInfos({
263263
});
264264

265265
goTo.marker('6');
266-
verify.completionListContains("i1_p1", "(property) c1.i1_p1: number", "");
267-
verify.completionListContains("i1_f1", "(method) c1.i1_f1(): void", "");
266+
verify.completionListContains("i1_p1", "(property) c1.i1_p1: number", "i1_p1");
267+
verify.completionListContains("i1_f1", "(method) c1.i1_f1(): void", "i1_f1");
268268
verify.completionListContains("i1_l1", "(property) c1.i1_l1: () => void", "");
269269
verify.completionListContains("i1_nc_p1", "(property) c1.i1_nc_p1: number", "");
270270
verify.completionListContains("i1_nc_f1", "(method) c1.i1_nc_f1(): void", "");
@@ -276,7 +276,7 @@ verify.completionListContains("nc_p1", "(property) c1.nc_p1: number", "c1_nc_p1"
276276
verify.completionListContains("nc_f1", "(method) c1.nc_f1(): void", "c1_nc_f1");
277277
verify.completionListContains("nc_l1", "(property) c1.nc_l1: () => void", "");
278278
goTo.marker('7');
279-
verify.currentSignatureHelpDocCommentIs("");
279+
verify.currentSignatureHelpDocCommentIs("i1_f1");
280280
goTo.marker('8');
281281
verify.currentSignatureHelpDocCommentIs("");
282282
goTo.marker('9');
@@ -294,7 +294,7 @@ verify.currentSignatureHelpDocCommentIs("");
294294

295295
verify.quickInfos({
296296
"6iq": "var c1_i: c1",
297-
"7q": "(method) c1.i1_f1(): void",
297+
"7q": ["(method) c1.i1_f1(): void", "i1_f1"],
298298
"8q": "(method) c1.i1_nc_f1(): void",
299299
"9q": ["(method) c1.f1(): void", "c1_f1"],
300300
"10q": ["(method) c1.nc_f1(): void", "c1_nc_f1"],
@@ -515,7 +515,7 @@ verify.quickInfos({
515515
"39q": ["(method) i2.f1(): void", "i2 f1"],
516516
"40q": "(method) i2.nc_f1(): void",
517517
"l37q": "(property) i2.i2_l1: () => void",
518-
"l38q": "(property) i2.i2_nc_l1: () => void",
518+
"l38q": "(property) i2.i2_nc_l1: () => void",
519519
"l39q": "(property) i2.l1: () => void",
520520
"l40q": "(property) i2.nc_l1: () => void",
521521
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
///<reference path="fourslash.ts" />
2+
// @Filename: inheritDoc.ts
3+
////class Foo {
4+
//// /**
5+
//// * Foo constructor documentation
6+
//// */
7+
//// constructor(value: number) {}
8+
//// /**
9+
//// * Foo#method1 documentation
10+
//// */
11+
//// static method1() {}
12+
//// /**
13+
//// * Foo#method2 documentation
14+
//// */
15+
//// method2() {}
16+
//// /**
17+
//// * Foo#property1 documentation
18+
//// */
19+
//// property1: string;
20+
////}
21+
////interface Baz {
22+
//// /** Baz#property1 documentation */
23+
//// property1: string;
24+
//// /**
25+
//// * Baz#property2 documentation
26+
//// */
27+
//// property2: object;
28+
////}
29+
////class Bar extends Foo implements Baz {
30+
//// ctorValue: number;
31+
//// /** @inheritDoc */
32+
//// constructor(value: number) {
33+
//// super(value);
34+
//// this.ctorValue = value;
35+
//// }
36+
//// /** @inheritDoc */
37+
//// static method1() {}
38+
//// method2() {}
39+
//// /** @inheritDoc */
40+
//// property1: string;
41+
//// /**
42+
//// * Bar#property2
43+
//// * @inheritDoc
44+
//// */
45+
//// property2: object;
46+
////}
47+
////const b = new Bar/*1*/(5);
48+
////b.method2/*2*/();
49+
////Bar.method1/*3*/();
50+
////const p1 = b.property1/*4*/;
51+
////const p2 = b.property2/*5*/;
52+
53+
verify.quickInfoAt("1", "constructor Bar(value: number): Bar", undefined); // constructors aren't actually inherited
54+
verify.quickInfoAt("2", "(method) Bar.method2(): void", "Foo#method2 documentation"); // use inherited docs only
55+
verify.quickInfoAt("3", "(method) Bar.method1(): void", undefined); // statics aren't actually inherited
56+
verify.quickInfoAt("4", "(property) Bar.property1: string", "Foo#property1 documentation"); // use inherited docs only
57+
verify.quickInfoAt("5", "(property) Bar.property2: object", "Baz#property2 documentation\nBar#property2"); // include local and inherited docs

0 commit comments

Comments
 (0)