diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 9c8b085d19848..d60b9a0bcc7d7 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -19,6 +19,7 @@ namespace ts.JsDoc { "fileOverview", "function", "ignore", + "inheritDoc", "inner", "lends", "link", diff --git a/src/services/services.ts b/src/services/services.ts index cd6fe8677113a..37dd2a183053d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -345,9 +345,29 @@ namespace ts { return this.declarations; } - getDocumentationComment(): SymbolDisplayPart[] { + getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] { if (this.documentationComment === undefined) { - this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations); + if (this.declarations) { + this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations); + + if (this.documentationComment.length === 0 || this.declarations.some(hasJSDocInheritDocTag)) { + if (checker) { + for (const declaration of this.declarations) { + const inheritedDocs = findInheritedJSDocComments(declaration, this.getName(), checker); + if (inheritedDocs.length > 0) { + if (this.documentationComment.length > 0) { + inheritedDocs.push(ts.lineBreakPart()); + } + this.documentationComment = concatenate(inheritedDocs, this.documentationComment); + break; + } + } + } + } + } + else { + this.documentationComment = []; + } } return this.documentationComment; @@ -476,7 +496,23 @@ namespace ts { getDocumentationComment(): SymbolDisplayPart[] { if (this.documentationComment === undefined) { - this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations([this.declaration]) : []; + if (this.declaration) { + this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations([this.declaration]); + + if (this.documentationComment.length === 0 || hasJSDocInheritDocTag(this.declaration)) { + const inheritedDocs = findInheritedJSDocComments(this.declaration, this.declaration.symbol.getName(), this.checker); + if (this.documentationComment.length > 0) { + inheritedDocs.push(ts.lineBreakPart()); + } + this.documentationComment = concatenate( + inheritedDocs, + this.documentationComment + ); + } + } + else { + this.documentationComment = []; + } } return this.documentationComment; @@ -491,6 +527,58 @@ namespace ts { } } + /** + * Returns whether or not the given node has a JSDoc "inheritDoc" tag on it. + * @param node the Node in question. + * @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`. + */ + function hasJSDocInheritDocTag(node: Node) { + return ts.getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc"); + } + + /** + * Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses + * implemented interfaces until a symbol is found with the same name and with documentation. + * @param declaration The possibly-inherited declaration to find comments for. + * @param propertyName The name of the possibly-inherited property. + * @param typeChecker A TypeChecker, used to find inherited properties. + * @returns A filled array of documentation comments if any were found, otherwise an empty array. + */ + function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] { + let foundDocs = false; + return flatMap(getAllSuperTypeNodes(declaration), superTypeNode => { + if (foundDocs) { + return emptyArray; + } + const superType = typeChecker.getTypeAtLocation(superTypeNode); + if (!superType) { + return emptyArray; + } + const baseProperty = typeChecker.getPropertyOfType(superType, propertyName); + if (!baseProperty) { + return emptyArray; + } + const inheritedDocs = baseProperty.getDocumentationComment(typeChecker); + foundDocs = inheritedDocs.length > 0; + return inheritedDocs; + }); + } + + /** + * Finds and returns the `TypeNode` for all super classes and implemented interfaces given a declaration. + * @param declaration The possibly-inherited declaration. + * @returns A filled array of `TypeNode`s containing all super classes and implemented interfaces if any exist, otherwise an empty array. + */ + function getAllSuperTypeNodes(declaration: Declaration): ReadonlyArray { + const container = declaration.parent; + if (!container || (!isClassDeclaration(container) && !isInterfaceDeclaration(container))) { + return emptyArray; + } + const extended = getClassExtendsHeritageClauseElement(container); + const types = extended ? [extended] : emptyArray; + return isClassLike(container) ? concatenate(types, getClassImplementsHeritageClauseElements(container)) : types; + } + class SourceFileObject extends NodeObject implements SourceFile { public kind: SyntaxKind.SourceFile; public _declarationBrand: any; @@ -1390,7 +1478,7 @@ namespace ts { kindModifiers: ScriptElementKindModifier.none, textSpan: createTextSpan(node.getStart(), node.getWidth()), displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)), - documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined, + documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined, tags: type.symbol ? type.symbol.getJsDocTags() : undefined }; } diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index 7e8e748bb172d..36c83f5f4af38 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -400,7 +400,7 @@ namespace ts.SignatureHelp { suffixDisplayParts, separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()], parameters: signatureHelpParameters, - documentation: candidateSignature.getDocumentationComment(), + documentation: candidateSignature.getDocumentationComment(typeChecker), tags: candidateSignature.getJsDocTags() }; }); @@ -420,7 +420,7 @@ namespace ts.SignatureHelp { return { name: parameter.name, - documentation: parameter.getDocumentationComment(), + documentation: parameter.getDocumentationComment(typeChecker), displayParts, isOptional: typeChecker.isOptionalParameter(parameter.valueDeclaration) }; @@ -438,4 +438,4 @@ namespace ts.SignatureHelp { }; } } -} \ No newline at end of file +} diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index d210a04551377..c3a1465197d25 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -438,7 +438,7 @@ namespace ts.SymbolDisplay { } if (!documentation) { - documentation = symbol.getDocumentationComment(); + documentation = symbol.getDocumentationComment(typeChecker); tags = symbol.getJsDocTags(); if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) { // For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo` @@ -455,7 +455,7 @@ namespace ts.SymbolDisplay { continue; } - documentation = rhsSymbol.getDocumentationComment(); + documentation = rhsSymbol.getDocumentationComment(typeChecker); tags = rhsSymbol.getJsDocTags(); if (documentation.length > 0) { break; @@ -522,7 +522,7 @@ namespace ts.SymbolDisplay { displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads")); displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); } - documentation = signature.getDocumentationComment(); + documentation = signature.getDocumentationComment(typeChecker); tags = signature.getJsDocTags(); } diff --git a/src/services/types.ts b/src/services/types.ts index 2d629041d1f8f..63138b33a1e89 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -32,7 +32,7 @@ namespace ts { getEscapedName(): __String; getName(): string; getDeclarations(): Declaration[] | undefined; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } @@ -55,7 +55,7 @@ namespace ts { getTypeParameters(): TypeParameter[] | undefined; getParameters(): Symbol[]; getReturnType(): Type; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } diff --git a/tests/baselines/reference/APISample_jsdoc.js b/tests/baselines/reference/APISample_jsdoc.js index c74e188f38b22..f28b16d88b60c 100644 --- a/tests/baselines/reference/APISample_jsdoc.js +++ b/tests/baselines/reference/APISample_jsdoc.js @@ -21,7 +21,7 @@ function parseCommentsIntoDefinition(this: any, } // the comments for a symbol - let comments = symbol.getDocumentationComment(); + let comments = symbol.getDocumentationComment(undefined); if (comments.length) { 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) { return; } // the comments for a symbol - var comments = symbol.getDocumentationComment(); + var comments = symbol.getDocumentationComment(undefined); if (comments.length) { definition.description = comments.map(function (comment) { return comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n"); }).join(""); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 7fbff83009fc0..4f46b4f3be4b9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3817,7 +3817,7 @@ declare namespace ts { getEscapedName(): __String; getName(): string; getDeclarations(): Declaration[] | undefined; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } interface Type { @@ -3838,7 +3838,7 @@ declare namespace ts { getTypeParameters(): TypeParameter[] | undefined; getParameters(): Symbol[]; getReturnType(): Type; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } interface SourceFile { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 73d069757dfef..0b1d46490b64f 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3817,7 +3817,7 @@ declare namespace ts { getEscapedName(): __String; getName(): string; getDeclarations(): Declaration[] | undefined; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } interface Type { @@ -3838,7 +3838,7 @@ declare namespace ts { getTypeParameters(): TypeParameter[] | undefined; getParameters(): Symbol[]; getReturnType(): Type; - getDocumentationComment(): SymbolDisplayPart[]; + getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } interface SourceFile { diff --git a/tests/cases/compiler/APISample_jsdoc.ts b/tests/cases/compiler/APISample_jsdoc.ts index 2f4e08931d619..d40673f1d9ed0 100644 --- a/tests/cases/compiler/APISample_jsdoc.ts +++ b/tests/cases/compiler/APISample_jsdoc.ts @@ -25,7 +25,7 @@ function parseCommentsIntoDefinition(this: any, } // the comments for a symbol - let comments = symbol.getDocumentationComment(); + let comments = symbol.getDocumentationComment(undefined); if (comments.length) { definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join(""); diff --git a/tests/cases/fourslash/commentsInheritance.ts b/tests/cases/fourslash/commentsInheritance.ts index 7afe18f400ce4..985c55c7947bb 100644 --- a/tests/cases/fourslash/commentsInheritance.ts +++ b/tests/cases/fourslash/commentsInheritance.ts @@ -263,8 +263,8 @@ verify.quickInfos({ }); goTo.marker('6'); -verify.completionListContains("i1_p1", "(property) c1.i1_p1: number", ""); -verify.completionListContains("i1_f1", "(method) c1.i1_f1(): void", ""); +verify.completionListContains("i1_p1", "(property) c1.i1_p1: number", "i1_p1"); +verify.completionListContains("i1_f1", "(method) c1.i1_f1(): void", "i1_f1"); verify.completionListContains("i1_l1", "(property) c1.i1_l1: () => void", ""); verify.completionListContains("i1_nc_p1", "(property) c1.i1_nc_p1: number", ""); 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" verify.completionListContains("nc_f1", "(method) c1.nc_f1(): void", "c1_nc_f1"); verify.completionListContains("nc_l1", "(property) c1.nc_l1: () => void", ""); goTo.marker('7'); -verify.currentSignatureHelpDocCommentIs(""); +verify.currentSignatureHelpDocCommentIs("i1_f1"); goTo.marker('8'); verify.currentSignatureHelpDocCommentIs(""); goTo.marker('9'); @@ -294,7 +294,7 @@ verify.currentSignatureHelpDocCommentIs(""); verify.quickInfos({ "6iq": "var c1_i: c1", - "7q": "(method) c1.i1_f1(): void", + "7q": ["(method) c1.i1_f1(): void", "i1_f1"], "8q": "(method) c1.i1_nc_f1(): void", "9q": ["(method) c1.f1(): void", "c1_f1"], "10q": ["(method) c1.nc_f1(): void", "c1_nc_f1"], @@ -515,7 +515,7 @@ verify.quickInfos({ "39q": ["(method) i2.f1(): void", "i2 f1"], "40q": "(method) i2.nc_f1(): void", "l37q": "(property) i2.i2_l1: () => void", - "l38q": "(property) i2.i2_nc_l1: () => void", + "l38q": "(property) i2.i2_nc_l1: () => void", "l39q": "(property) i2.l1: () => void", "l40q": "(property) i2.nc_l1: () => void", }); diff --git a/tests/cases/fourslash/jsDocInheritDoc.ts b/tests/cases/fourslash/jsDocInheritDoc.ts new file mode 100644 index 0000000000000..8a19bd0c14e26 --- /dev/null +++ b/tests/cases/fourslash/jsDocInheritDoc.ts @@ -0,0 +1,57 @@ +/// +// @Filename: inheritDoc.ts +////class Foo { +//// /** +//// * Foo constructor documentation +//// */ +//// constructor(value: number) {} +//// /** +//// * Foo#method1 documentation +//// */ +//// static method1() {} +//// /** +//// * Foo#method2 documentation +//// */ +//// method2() {} +//// /** +//// * Foo#property1 documentation +//// */ +//// property1: string; +////} +////interface Baz { +//// /** Baz#property1 documentation */ +//// property1: string; +//// /** +//// * Baz#property2 documentation +//// */ +//// property2: object; +////} +////class Bar extends Foo implements Baz { +//// ctorValue: number; +//// /** @inheritDoc */ +//// constructor(value: number) { +//// super(value); +//// this.ctorValue = value; +//// } +//// /** @inheritDoc */ +//// static method1() {} +//// method2() {} +//// /** @inheritDoc */ +//// property1: string; +//// /** +//// * Bar#property2 +//// * @inheritDoc +//// */ +//// property2: object; +////} +////const b = new Bar/*1*/(5); +////b.method2/*2*/(); +////Bar.method1/*3*/(); +////const p1 = b.property1/*4*/; +////const p2 = b.property2/*5*/; + +verify.quickInfoAt("1", "constructor Bar(value: number): Bar", undefined); // constructors aren't actually inherited +verify.quickInfoAt("2", "(method) Bar.method2(): void", "Foo#method2 documentation"); // use inherited docs only +verify.quickInfoAt("3", "(method) Bar.method1(): void", undefined); // statics aren't actually inherited +verify.quickInfoAt("4", "(property) Bar.property1: string", "Foo#property1 documentation"); // use inherited docs only +verify.quickInfoAt("5", "(property) Bar.property2: object", "Baz#property2 documentation\nBar#property2"); // include local and inherited docs