Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use documentation comments from inherited properties when @inheritDoc is present #18804

Merged
8 commits merged into from
Nov 6, 2017

Conversation

sjbarag
Copy link
Contributor

@sjbarag sjbarag commented Sep 27, 2017

The JSDoc @ineheritDoc tag "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

@msftclas
Copy link

msftclas commented Sep 27, 2017

CLA assistant check
All CLA requirements met.

@mhegazy mhegazy requested a review from a user October 9, 2017 21:54
@mhegazy
Copy link
Contributor

mhegazy commented Oct 9, 2017

@Andy-MS can you please review this change.

@sjbarag
Copy link
Contributor Author

sjbarag commented Oct 17, 2017

For posterity: rebased on fcb48dd and re-ran gulp baseline-accept to accept changes to api/tsserverlibrary.d.ts and api/typescript.d.ts. I haven't changed the diff otherwise though 😄

@sjbarag sjbarag force-pushed the support-jsdoc-inheritdoc branch from df96aba to 6ff0aed Compare October 17, 2017 16:12
if (this.documentationComment === undefined) {
this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations([this.declaration]) : [];

if (this.declaration && this.documentationComment.length === 0 && hasJSDocInheritDocTag(this) && typeChecker) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might whole block might be neater as if (this.declaration) { ... } else { this.documentationComment = []; }

* @returns `true` if `symbol` has a JSDoc "inheritDoc" tag on it, otherwise `false`.
*/
function hasJSDocInheritDocTag(symbol: Signature | Symbol) {
return !!find(symbol.getJsDocTags(), tag => tag.name === "inheritDoc");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

symbol.getJsDocTags().some(...)

@@ -70,7 +71,7 @@ namespace ts.JsDoc {
const tags: JSDocTagInfo[] = [];
forEachUnique(declarations, declaration => {
for (const tag of getJSDocTags(declaration)) {
if (tag.kind === SyntaxKind.JSDocTag) {
if (tag.kind === SyntaxKind.JSDocTag || tag.kind === SyntaxKind.JSDocInheritDocTag) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think you need to modify this function for what you want; use ts.getJSDocTags (from compiler/utilities.ts) instead, which returns tags of all kinds.

function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] {
let documentationComment: SymbolDisplayPart[] = [];

if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer an early return. Also, use isClassLike.

let documentationComment: SymbolDisplayPart[] = [];

if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) {
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just declare this outside (as a Node) and rely on isClassDeclaration and isInterfaceDeclaration working as type predicates.

const container: ClassDeclaration | InterfaceDeclaration = declaration.parent;
const baseTypeNode = getClassExtendsHeritageClauseElement(container);

if (baseTypeNode) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return


if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) {
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent;
const baseTypeNode = getClassExtendsHeritageClauseElement(container);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a function getBaseTypes in checker.ts that might be better suited. You could expose it internally by adding /* @internal */ getBaseTypes(): ReadonlyArray<BaseType>; to interface TypeChecker.
The advantages are that this gets you both classes and interfaces, and you don't have to call getTypeAtLocation.

////const p1 = b.property1/*4*/;
////const p2 = b.property2/*5*/;

verify.baselineQuickInfo();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be breifer using just verify.quickInfoAt -- baselineQuickInfo is more so we can check all the displayParts, but that's not what we're testing here.

@ghost
Copy link

ghost commented Oct 17, 2017

What if we just inherited documentation by default without requiring users to add /** @inheritDoc */ to everything?

@sjbarag
Copy link
Contributor Author

sjbarag commented Oct 17, 2017

Thanks for the review, @Andy-MS! I'll get this fixed up today or tomorrow.

What if we just inherited documentation by default without requiring users to add /** @inheritDoc */ to everything?

I think it'd be pretty intuitive to be honest. I'd considered doing that here but didn't want to take too big of a step 😃 Should be relatively simple to get working, so I'll switch to that approach.

Thanks again! ❤️

@sjbarag
Copy link
Contributor Author

sjbarag commented Oct 26, 2017

Got a few updates here based on your feedback!

  1. Rebased on 9c96eee
  2. Added early returns
  3. Used ts.getJSDocTags
  4. Used verify.quickInfoAt in tests

I unfortunately don't see a way to get typeChecker.getBaseType to work, since it accepts an InterfaceType. Perhaps I'm missing something though?

Thanks for your patience while I updated this 😄

@ghost
Copy link

ghost commented Oct 26, 2017

OK, I was wrong about getBaseTypes -- it will only return the extends type, not any interfaces.
I've managed to eliminate duplicate code between the extends and implements cases though:

function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] {
    return flatMap(getAllSuperTypeNodes(declaration), superType => {
        const baseProperty = typeChecker.getPropertyOfType(typeChecker.getTypeAtLocation(superType), propertyName);
        return baseProperty && baseProperty.getDocumentationComment(typeChecker);
    });
}

function getAllSuperTypeNodes(declaration: Declaration): ReadonlyArray<TypeNode> {
    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;
}

if (this.declarations) {
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations);

if (this.documentationComment.length === 0 || this.declarations.some(dec => hasJSDocInheritDocTag(dec))) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: dec => hasJSDocInheritDocTag(dec) -- just use hasJSDocInheritDocTag directly

if (this.declaration) {
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations([this.declaration]);

if (this.documentationComment.length === 0) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a case where I think we should use both inherited and own doc comments:

///<reference path="fourslash.ts" />

////interface I {
////    /** Interface doc */
////    m(): void;
////}
////
////class C implements I {
////    /**
////     * Class doc
////     * @inheritDoc
////     */
////    m/**/() {}
////}

verify.quickInfoAt("", "(method) C.m(): void", "Inferface doc\nClass doc");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point! I agree. This would make TypeScript's handling of @inheritDoc different than vanilla JSDoc's though, which ignores documentation on anything with @inheritDoc, e.g.:

/** @class */
class Foo {
    /** Foo docs. */
    test() {
    }
}

/**
 * @class
 * @extends Foo
 */
class Bar {
    /**
     * Bar docs.
     * @inheritDoc
     */
    test() {
    }
}

results in JSDoc for Bar#test of "Foo docs" only.

That said, I think it's helpful for TypeScript users and I'd love to have it. As long as you're okay with differing from upstream here, I'm 100% fine with it 👍

Copy link

@ghost ghost Oct 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, http://usejsdoc.org/tags-inheritdoc.html says:

The @inheritdoc tag indicates that a symbol should inherit its documentation from its parent class. Any other tags that you include in the JSDoc comment will be ignored.
This tag is provided for compatibility with Closure Compiler. By default, if you do not add a JSDoc comment to a symbol, the symbol will inherit documentation from its parent.
The presence of the @inheritdoc tag implies the presence of the @OverRide tag.

This makes it a rather strange tag -- one whose sole purpose is to make all documentation on the current node ignored! Presumably people who don't want to see that comment in quick info should just make it a regular comment and not a doc comment. I think it would be a more useful tag if it meant to add docs from a parent in addition to anything on the child.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes perfect sense to me. I've updated the PR to support that :)

@@ -6380,6 +6380,9 @@ namespace ts {
case "constructor":
tag = parseClassTag(atToken, tagName);
break;
case "inheritDoc":
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to be needed as its own SyntaxKind any more?

@@ -32,7 +32,7 @@ namespace ts {
getEscapedName(): __String;
getName(): string;
getDeclarations(): Declaration[] | undefined;
getDocumentationComment(): SymbolDisplayPart[];
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on #19455 (comment) maybe it would be better to just take a compile-time breaking change and make this typeChecker: TypeChecker | undefined. @mhegazy

@sjbarag sjbarag force-pushed the support-jsdoc-inheritdoc branch 2 times, most recently from 351dcbf to 424606c Compare October 26, 2017 23:42
@sjbarag
Copy link
Contributor Author

sjbarag commented Oct 27, 2017

Sorry! 424606c failed to build because I forgot to re-accept the baselines after switching from typeChecker?: TypeChecker to typeChecker: TypeChecker | undefined. a839a80 should build 🤞

@sjbarag
Copy link
Contributor Author

sjbarag commented Oct 27, 2017

Well this is embarrassing, I forgot to update APISample_jsdoc.ts to match the interface change. Should be fixed now (for real (really (I mean it 😅)))

…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 microsoft#8912
@sjbarag sjbarag force-pushed the support-jsdoc-inheritdoc branch from 23ac1ed to 5ba044a Compare November 1, 2017 23:50
@sjbarag
Copy link
Contributor Author

sjbarag commented Nov 1, 2017

Rebased on 1a7a587! Dang, master moves quick 😄

@ghost
Copy link

ghost commented Nov 6, 2017

Thanks!

@ghost ghost merged commit a46d270 into microsoft:master Nov 6, 2017
@cliffkoh
Copy link

cliffkoh commented Nov 7, 2017

Thank you @sjbarag, this is super awesome!

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for JSDoc's @inheritDoc for TypeScript
4 participants